#defineTVN_MASK(TVN_SIZE-1)
#defineTVR_MASK(TVR_SIZE-1)
structtvec{
structlist_headvec[TVN_SIZE];
};
structtvec_root{
structlist_headvec[TVR_SIZE];
};
structtvec_base{
spinlock_tlock;
structtimer_list*running_timer;
unsignedlongtimer_jiffies;
unsignedlongnext_timer;
structtvec_roottv1;
structtvectv2;
structtvectv3;
structtvectv4;
structtvectv5;
}____cacheline_aligned;
以CONFIG_BASE_SMALL定义为0为例,TVR_SIZE=256,TVN_SIZE=64,这样
可以得到如图1所示的timerwheel的结构。
图1.Timerwheel的逻辑结构
list_head的作用
list_head是Linux内核使用的一个双向循环链表表头。
任何一个需要使用链表的数据结构可以通过内嵌list_head的方式,将其链接在一起从而形成一个双向链表。
参见list_head在include/Linux/list.h中的定义和实现。
在timerwheel的框架下,所有系统正在使用的timer并不是顺序存放在一个平坦的链表中,因为这样做会使得查找,插入,删除等操作效率低下。
Timerwheel提供了5个timer数组,数组之间存在着类似时分秒的进位关系。
TV1为第一个timer数组,其中存放着从timer_jiffies(当前到期的jiffies)到timer_jiffies+255共256个tick对应的timerlist。
因为在一个tick上可能同时有多个timer等待超时处理,timerwheel使用list_head将所有timer串成一个链表,以便在超时时顺序处理。
TV2有64个单元,每个单元都对应着256个tick,因此TV2所表示的超时时间范围从timer_jiffies+256到timer_jiffies+256*64–1。
依次类推TV3,TV4,TV5。
以HZ=1000为例,每1ms产生一次中断,TV1就会被访问一次,但是TV2要每256ms才会被访问一次,TV3要16s,TV4要17分钟,TV5甚至要19小时才有机会检查一次。
最终,timerwheel可以管理的最大超时值为2^32。
一共使用了512个list_head(256+64+64+64+64)。
如果CONFIG_BASE_SMALL定义为1,则最终使用的list_head个数为128个(64+16+16+16+16),占用的内存更少,更适合嵌入式系统使用。
Timerwheel的处理逻辑如清单2所示:
清单2.timerwheel的核心处理函数
staticinlinevoid__run_timers(structtvec_base*base)
{
structtimer_list*timer;
spin_lock_irq(&base->lock);
while(time_after_eq(jiffies,base->timer_jiffies)){
structlist_headwork_list;
structlist_head*head=&work_list;
intindex=base->timer_jiffies&TVR_MASK;
/*
*Cascadetimers:
*/
if(!
index&&
(!
cascade(base,&base->tv2,INDEX(0)))&&
(!
cascade(base,&base->tv3,INDEX
(1)))&&
!
cascade(base,&base->tv4,INDEX
(2)))
cascade(base,&base->tv5,INDEX(3));
++base->timer_jiffies;
list_replace_init(base->tv1.vec+index,&work_list);
while(!
list_empty(head)){
void(*fn)(unsignedlong);
unsignedlongdata;
timer=list_first_entry(head,structtimer_list,entry);
fn=timer->function;
data=timer->data;
....
fn(data);
....
}
base->timer_jiffies用来记录在TV1中最接近超时的tick的位置。
index是用来遍历TV1的索引。
每一次循环index会定位一个当前待处理的tick,并处理这个tick下所有超时的timer。
base->timer_jiffies会在每次循环后增加一个jiffy,index也会随之向前移动。
当index变为0时表示TV1完成了一次完整的遍历,此时所有在TV1中的timer都被处理了,因此需要通过cascade将后面TV2,TV3等timerlist中的timer向前移动,类似于进位。
这种层叠的timerlist实现机制可以大大降低每次检查超时timer的时间,每次中断只需要针对TV1进行检查,只有必要时才进行cascade。
即便如此,timerwheel的实现机制仍然存在很大弊端。
一个弊端就是cascade开销过大。
在极端的条件下,同时会有多个TV需要进行cascade处理,会产生很大的时延。
这也是为什么说timeout类型的定时器是timerwheel的主要应用环境,或者说timerwheel是为timeout类型的定时器优化的。
因为timeout类型的定时器的应用场景多是错误条件的检测,这类错误发生的机率很小,通常不到超时就被删除了,因此不会产生cascade的开销。
另一方面,由于timerwheel是建立在HZ的基础上的,因此其计时精度无法进一步提高。
毕竟一味的通过提高HZ值来提高计时精度并无意义,结果只能是产生大量的定时中断,增加额外的系统开销。
因此,有必要将高精度的timer与低精度的timer分开,这样既可以确保低精度的timeout类型的定时器应用,也便于高精度的timer类型定时器的应用。
还有一个重要的因素是timerwheel的实现与jiffies的耦合性太强,非常不便于扩展。
因此,自从2.6.16开始,一个新的timer子系统hrtimer被加入到内核中。
回页首
hrtimer(High-resolutionTimer)
hrtimer首先要实现的功能就是要克服timerwheel的缺点:
低精度以及与内核其他模块的高耦合性。
在正式介绍hrtimer之前,有必要先介绍几个常用的基本概念:
时钟源设备(clock-sourcedevice)
系统中可以提供一定精度的计时设备都可以作为时钟源设备。
如TSC,HPET,ACPIPM-Timer,PIT等。
但是不同的时钟源提供的时钟精度是不一样的。
像TSC,HPET等时钟源既支持高精度模式(high-resolutionmode)也支持低精度模式(low-resolutionmode),而PIT只能支持低精度模式。
此外,时钟源的计时都是单调递增的(monotonically),如果时钟源的计时出现翻转(即返回到0值),很容易造成计时错误,内核的一个patch(commitid:
ff69f2)就是处理这类问题的一个很好示例。
时钟源作为系统时钟的提供者,在可靠并且可用的前提下精度越高越好。
在Linux中不同的时钟源有不同的rating,具有更高rating的时钟源会优先被系统使用。
如图2所示:
表1.时钟源中rating的定义
1~99100~199200~299300~399400~499
非常差的时钟源,只能作为最后的选择。
如jiffies基本可以使用但并非理想的时钟源。
如PIT正确可用的时钟源。
如ACPIPMTimer,HPET快速并且精确的时钟源。
如TSC理想时钟源。
如kvm_clock,xen_clock
时钟事件设备(clock-eventdevice)
系统中可以触发one-shot(单次)或者周期性中断的设备都可以作为时钟事件设备。
如HPET,CPULocalAPICTimer等。
HPET比较特别,它既可以做时钟源设备也可以做时钟事件设备。
时钟事件设备的类型分为全局和per-CPU两种类型。
全局的时钟事件设备虽然附属于某一个特定的CPU上,但是完成的是系统相关的工作,例如完成系统的tick更新;per-CPU的时钟事件设备主要完成LocalCPU上的一些功能,例如对在当前CPU上运行进程的时间统计,profile,设置LocalCPU上的下一次事件中断等。
和时钟源设备的实现类似,时钟事件设备也通过rating来区分优先级关系。
tickdevice
Tickdevice用来处理周期性的tickevent。
Tickdevice其实是时钟事件设备的一个wrapper,因此tickdevice也有one-shot和周期性这两种中断触发模式。
每注册一个时钟事件设备,这个设备会自动被注册为一个tickdevice。
全局的tickdevice用来更新诸如jiffies这样的全局信息,per-CPU的tickdevice则用来更新每个CPU相关的特定信息。
broadcast
CPU的C-STATE
CPU在空闲时会根据空闲时间的长短选择进入不同的睡眠级别,称为C-STATE。
C0为正常运行状态,C1到C7为睡眠状态,数值越大,睡眠程度越深,也就越省电。
CPU空闲越久,进入睡眠的级别越高,但是唤醒所需的时间也越长。
唤醒也是需要消耗能源的,因此,只有选择合适的睡眠级别才能确保节能的最大化。
Broadcast的出现是为了应对这样一种情况:
假定CPU使用LocalAPICTimer作为per-CPU的tickdevice,但是某些特定的CPU(如Intel的Westmere之前的CPU)在进入C3+的状态时LocalAPICTimer也会同时停止工作,进入睡眠状态。
在这种情形下broadcast可以替代LocalAPICTimer继续完成统计进程的执行时间等有关操作。
本质上broadcast是发送一个IPI(Inter-processorinterrupt)中断给其他所有的CPU,当目标CPU收到这个IPI中断后就会调用原先LocalAPICTimer正常工作时的中断处理函数,从而实现了同样的功能。
目前主要在x86以及MIPS下会用到broadcast功能。
Timekeeping>OD(GenericTime-of-Day)
Timekeeping(可以理解为时间测量或者计时)是内核时间管理的一个核心组成部分。
没有Timekeeping,就无法更新系统时间,维持系统“心跳”。
GTOD是一个通用的框架,用来实现诸如设置系统时间gettimeofday或者修改系统时间settimeofday等工作。
为了实现以上功能,Linux实现了多种与时间相关但用于不同目的的数据结构。
structtimespec{
__kernel_time_ttv_sec;/*seconds*/
longtv_nsec;/*nanoseconds*/
};
timespec精度是纳秒。
它用来保存从00:
00:
00GMT,1January1970开始经过的时间。
内核使用全局变量xtime来记录这一信息,这就是通常所说的“WallTime”或者“RealTime”。
与此对应的是“SystemTime”。
SystemTime是一个单调递增的时间,每次系统启动时从0开始计时。
structtimeval{
__kernel_time_ttv_sec;/*seconds*/
__kernel_suseconds_ttv_usec;/*microseconds*/
};
timeval精度是微秒。
timeval主要用来指定一段时间间隔。
unionktime{
s64tv64;
#ifBITS_PER_LONG!
=64&&!
defined(CONFIG_KTIME_SCALAR)
struct{
#ifdef__BIG_ENDIAN
s32sec,nsec;
#else
s32nsec,sec;
#endif
}tv;
#endif
};
ktime_t是hrtimer主要使用的时间结构。
无论使用哪种体系结构,ktime_t始终保持64bit的精度,并且考虑了大小端的影响。
typedefu64cycle_t;
cycle_t是从时钟源设备中读取的时钟类型。
为了管理这些不同的时间结构,Linux实现了一系列辅助函数来完成相互间的转换。
ktime_to_timespec,ktime_to_timeval,ktime_to_ns/ktime_to_us,反过来有诸如ns_to_ktime等类似的函数。
timeval_to_ns,timespec_to_ns,反过来有诸如ns_to_timeval等类似的函数。
timeval_to_jiffies,timespec_to_jiffies,msecs_to_jiffies,usecs_to_jiffies,clock_t_to_jiffies反过来有诸如ns_to_timeval等类似的函数。
clocksource_cyc2ns/cyclecounter_cyc2ns
有了以上的介绍,通过图3可以更加清晰的看到这几者之间的关联。
图2.内核时钟子系统的结构关系
时钟源设备和时钟事件设备的引入,将原本放在各个体系结构中重复实现的冗余代码封装到各自的抽象层中,这样做不但消除了原来timerwheel与内核其他模块的紧耦合性,更重要的是系统可以在运行状态动态更换时钟源设备和时钟事件设备而不影响系统正常使用,譬如当CPU由于睡眠要关闭当前使用的时钟源设备或者时钟事件设备时系统可以平滑的切换到其他仍处于工作状态的设备上。
Timekeeping/GTOD在使用时钟源设备的基础上也采用类似的封装实现了体系结构的无关性和通用性。
hrtimer则可以通过timekeeping提供的接口完成定时器的更新,通过时钟事件设备提供的事件机制,完成对timer的管理。
在图3中还有一个重要的模块就是tickdevice的抽象,尤其是dynamictick。
Dynamictick的出现是为了能在系统空闲时通过停止tick的运行以达到降低CPU功耗的目的。
使用dynamictick的系统,只有在有实际工作时才会产生tick,否则tick是处于停止状态。
下文会有专门的章节进行论述。
hrtimer的实现机制
hrtimer是建立在per-CPU时钟事件设备上的,对于一个SMP系统,如果只有全局的时钟事件设备,hrtimer无法工作。
因为如果没有per-CPU时钟事件设备,时钟中断发生时系统必须产生必要的IPI中断来通知其他CPU完成相应的工作,而过多的IPI中断会带来很大的系统开销,这样会令使用hrtimer的代价太大,不如不用。
为了支持hrtimer,内核需要配置CONFIG_HIGH_RES_TIMERS=y。
hrtimer有两种工作模式:
低精度模式(low-resolutionmode)与高精度模式(high-resolutionmode)。
虽然hrtimer子系统是为高精度的timer准备的,但是系统可能在运行过程中动态切换到不同精度的时钟源设备,因此,hrtimer必须能够在低精度模式与高精度模式下自由切换。
由于低精度模式是建立在高精度模式之上的,因此即便系统只支持低精度模式,部分支持高精度模式的代码仍然会编译到内核当中。
在低精度模式下,hrtimer的核心处理函数是hrtimer_run_queues,每一次tick中断都要执行一次。
如清单3所示。
这个函数的调用流程为:
update_process_times
run_local_timers
hrtimer_run_queues
raise_softirq(TIMER_SOFTIRQ)
清单3.低精度模式下hrtimer的核心处理函数
voidhrtimer_run_queues(void)
{
structrb_node*node;
structhrtimer_cpu_base*cpu_base=&__get_cpu_var(hrtimer_bases);
structhrtimer_clock_base*base;
intindex,gettime=1;
if(hrtimer_hres_active())
return;
for(index=0;indexbase=&cpu_base->clock_base[index];
if(!
base->first)
continue;
if(gettime){
hrtimer_get_softirq_time(cpu_base);
gettime=0;
}
raw_spin_lock(&cpu_base->lock);
while((node=base->first)){
structhrtimer*timer;
timer=rb_entry(node,structhrtimer,node);
if(base->softirq_time.tv64<=
hrtimer_get_expires_tv64(timer))
break;
__run_hrtimer(timer,&base->softirq_time);
}
raw_spin_unlock(&cpu_base->lock);
}
}
hrtimer_bases是实现hrtimer的核心数据结构,通过hrtimer_bases,hrtimer可以管理挂在每一个CPU上的所有timer。
每个CPU上的timerlist不再使用timerwheel中多级链表的实现方式,而是采用了红黑树(Red-BlackTree)来进行管理。
hrtimer_bases的定义如清单4所示:
清单4.hrtimer_bases的定义
DEFINE_PER_CPU(structhrti