linux内核定时器实现机制剖析.docx
《linux内核定时器实现机制剖析.docx》由会员分享,可在线阅读,更多相关《linux内核定时器实现机制剖析.docx(16页珍藏版)》请在冰豆网上搜索。
linux内核定时器实现机制剖析
内核定时器,分级结构,定时器迁移刷新,DEFINE_TIMER,init_timer,setup_timer,add_timer,mod_timer,del_timer
1内核定时器概述
Linux内核2.4版中去掉了老版本内核中的静态定时器机制,而只留下动态定时器。
动态定时器与静态定时器这二个概念是相对于Linux内核定时器机制的可扩展功能而言的,动态定时器是指内核的定时器队列是可以动态变化的,然而就定时器本身而言,二者并无本质的区别。
考虑到静态定时器机制的能力有限,因此Linux内核2.4版中完全去掉了以前的静态定时器机制。
2.6内核为了支持SMP及CPU热插拔,对定时器相关结构又做了改动。
本文所有代码基于2.6.19内核(摘自)
Linux
11structlist_headentry;
12unsignedlongexpires;
13
14void(*function)(unsignedlong);
15unsignedlongdata;
16
17structtvec_t_base_s*base;
18};各数据成员的含义如下:
双向链表元素entry:
用来将多个定时器连接成一条双向循环队列。
expires:
指定定时器到期的时间,这个时间被表示成自系统启动以来的时钟滴答计数(也即时钟节拍数)。
当一个定时器的expires值小于或等于jiffies变量时,我们就说这个定时器已经超时或到期了。
在初始化一个定时器后,通常把它的expires域设置成当前expires变量的当前值加上某个时间间隔值(以时钟滴答次数计)。
函数指针function:
指向一个可执行函数。
当定时器到期时,内核就执行function所指定的函数。
data域:
被内核用作function函数的调用参数。
base:
当前timer所属的base。
由于考虑了SMP的情况,每个CPU都含有一个base。
2动态内核定时器的组织结构
Linux是怎样为其内核定时器机制提供动态扩展能力的呢?
其关键就在于“定时器向量”的概念。
所谓“定时器向量”就是指这样一条双向循环定时器队列(队列中的每一个元素都是一个timer_list结构):
对列中的所有定时器都在同一个时刻到期,也即对列中的每一个timer_list结构都具有相同的expires值。
显然,可以用一个timer_list结构类型的指针来表示一个定时器向量。
显然,定时器expires成员的值与jiffies变量的差值决定了一个定时器将在多长时间后到期。
在32位系统中,这个时间差值的最大值应该是0xffffffff。
因此如果是基于“定时器向量”基本定义,内核将至少要维护0xffffffff个timer_list结构类型的指针,这显然是不现实的。
另一方面,从内核本身这个角度看,它所关心的定时器显然不是那些已经过期而被执行过的定时器(这些定时器完全可以被丢弃),也不是那些要经过很长时间才会到期的定时器,而是那些当前已经到期或者马上就要到期的定时器(注意!
时间间隔是以滴答次数为计数单位的)。
基于上述考虑,并假定一个定时器要经过interval个时钟滴答后才到期(interval=expires-jiffies),则Linux采用了下列思想来实现其动态内核定时器机制:
对于那些0≤interval≤255的定时器,Linux严格按照定时器向量的基本语义来组织这些定时器,也即Linux内核最关心那些在接下来的255个时钟节拍内就要到期的定时器,因此将它们按照各自不同的expires值组织成256个定时器向量。
而对于那些256≤interval≤0xffffffff的定时器,由于他们离到期还有一段时间,因此内核并不关心他们,而是将它们以一种扩展的定时器向量语义(或称为“松散的定时器向量语义”)进行组织。
所谓“松散的定时器向量语义”就是指:
各定时器的expires值可以互不相同的一个定时器队列。
各定时器向量数据结构定义在kernel/timer.c文件中,如下述代码段所示:
///////////////////////////////////////////////2.4.19内核///////////////////////////////////////////////
structtimer_vec{
intindex;
structlist_headvec[TVN_SIZE];
};
structtimer_vec_root{
intindex;
structlist_headvec[TVR_SIZE];
};
staticstructtimer_vectv5;
staticstructtimer_vectv4;
staticstructtimer_vectv3;
staticstructtimer_vectv2;
staticstructtimer_vec_roottv1;
staticstructtimer_vec*consttvecs[]={
(structtimer_vec*)&tv1,&tv2,&tv3,&tv4,&tv5
};
staticstructlist_head*run_timer_list_running;
staticunsignedlongtimer_jiffies;
/*Initializebothexplicitly-let'strytohavetheminthesamecacheline*/
spinlock_ttimerlist_lock=SPIN_LOCK_UNLOCKED;
volatilestructtimer_list*volatilerunning_timer;
///////////////////////////////////////////////2.4.19内核///////////////////////////////////////////////
///////////////////////////////////////////////2.6.19内核///////////////////////////////////////////////51#defineTVN_BITS(CONFIG_BASE_SMALL?
4:
6)
52#defineTVR_BITS(CONFIG_BASE_SMALL?
6:
8)
53#defineTVN_SIZE(1
54#defineTVR_SIZE(1
55#defineTVN_MASK(TVN_SIZE-1)
56#defineTVR_MASK(TVR_SIZE-1)
58typedefstructtvec_s{
59structlist_headvec[TVN_SIZE];
60}tvec_t;
61
62typedefstructtvec_root_s{
63structlist_headvec[TVR_SIZE];
64}tvec_root_t;
65
66structtvec_t_base_s{
67spinlock_tlock;
68structtimer_list*running_timer;
69unsignedlongtimer_jiffies;
70tvec_root_ttv1;
71tvec_ttv2;
72tvec_ttv3;
73tvec_ttv4;
74tvec_ttv5;
75}____cacheline_aligned_in_smp;
76
77typedefstructtvec_t_base_stvec_base_t;
78
79tvec_base_tboot_tvec_bases;
80EXPORT_SYMBOL(boot_tvec_bases);
lock:
由于内核动态定时器链表是一种系统全局共享资源,为了实现对它的互斥访问,Linux定义了专门的自旋锁lock成员来保护。
任何想要访问动态定时器链表的代码段都首先必须先持有该自旋锁,并且在访问结束后释放该自旋锁。
running_timer:
用于SMPtimer_jiffies:
定时器是在软中断中执行的,从触发到真正执行这段时间内可能会有几次时钟中断发生。
因此内核必须记住上一次运行定时器机制是什么时候,也即内核必须保存上一次运行定时器机制时的jiffies值。
²tv1:
0-255第一级定时器队列
²tv2:
。
。
。
。
。
///////////////////////////////////////////////2.6.19
与2.4内核的区别:
无index域。
利用timer_jiffies求余后即可自动获得每个tvi当前的index;
将零散的tvi变量组织到了一起,将数组tvecs更改为了新的结构体变量;
将lock、running_timer、timer_jiffies等变量封装在结构内部,体现了更好的面向对象的特性;
²2.6内核支持CPU热插拔,此时定时器可以在各个CPU间转换,因此需要多组定时器结构变量。
原有的单个变量形式无法满足需求。
3定时器的组织原则
具体的组织方案可以分为两大部分:
(1)对于内核最关心的、interval值在[0,255]之间的前256个定时器向量,内核是这样组织它们的:
这256个定时器向量被组织在一起组成一个定时器向量数组,并作为数据结构timer_vec_root的一部分。
基于数据结构timer_vec_root,Linux定义了一个成员tv1,以表示内核所关心的前256个定时器向量。
这样内核在处理是否有到期定时器时,它就只从定时器向量数组tv1.vec[256]中的某个定时器向量内进行扫描。
而利用timer_jiffies对TVR_SIZE求余后即可自动获得每个tv1当前处理的向量,也即tv1.vec[]数组的索引index,其初值为0,最大值为255(以256为模)。
每个时钟节拍时timer_jiffies字段都会加1。
显然,index字段所指定的定时器向量tv1.vec[index]中包含了当前时钟节拍内已经到期的所有动态定时器。
而定时器向量tv1.vec[index+k]则包含了接下来第k个时钟节拍时刻将到期的所有动态定时器。
当timer_jiffies求余后又重新变为0时,就意味着内核已经扫描了tv1变量中的所有256个定时器向量。
在这种情况下就必须将那些以松散定时器向量语义来组织的定时器向量补充到tv1中来。
(2)而对于内核不关心的、interval值在[0xff,0xffffffff]之间的定时器,它们的到期紧迫程度也随其interval值的不同而不同。
显然interval值越小,定时器紧迫程度也越高。
因此在将它们以松散定时器向量进行组织时也应该区别对待。
通常,定时器的interval值越小,它所处的定时器向量的松散度也就越低(也即向量中的各定时器的expires值相差越小);而interval值越大,它所处的定时器向量的松散度也就越大(也即向量中的各定时器的expires值相差越大)。
内核规定,对于那些满足条件:
0x100≤interval≤0x3fff的定时器,只要表达式(interval>>8)具有相同值的定时器都将被组织在同一个松散定时器向量中,即以1》8=256为一个基本单位。
因此,为组织所有满足条件0x100≤interval≤0x3fff的定时器,就需要2^6=64个松散定时器向量。
同样地,为方便起见,这64个松散定时器向量也放在一起形成数组,并作为数据结构timer_vec的一部分。
基于数据结构timer_vec,Linux定义了成员tv2,来表示这64条松散定时器向量。
如上述代码段所示。
对于那些满足条件0x4000≤interval≤0xfffff的定时器,只要表达式(interval>>8+6)的值相同的定时器都将被放在同一个松散定时器向量中。
同样,要组织所有满足条件0x4000≤interval≤0xfffff的定时器,也需要2^6=64个松散定时器向量。
类似地,这64个松散定时器向量也可以用一个timer_vec结构来描述,相应地Linux定义了成员tv3来表示这64个松散定时器向量。
对于那些满足条件0x100000≤interval≤0x3ffffff的定时器,只要表达式(interval>>8+6+6)的值相同的定时器都将被放在同一个松散定时器向量中。
同样,要组织所有满足条件0x100000≤interval≤0x3ffffff的定时器,也需要2^6=64个松散定时器向量。
类似地,这64个松散定时器向量也可以用一个timer_vec结构来描述,相应地Linux定义了tv4成员来表示这64个松散定时器向量。
对于那些满足条件0x4000000≤interval≤0xffffffff的定时器,只要表达式(interval>>8+6+6+6)的值相同的定时器都将被放在同一个松散定时器向量中。
同样,要组织所有满足条件0x4000000≤interval≤0xffffffff的定时器,也需要2^6=64个松散定时器向量。
类似地,这64个松散定时器向量也可以用一个timer_vec结构来描述,相应地Linux定义了tv5成员来表示这64个松散定时器向量。
最后,为了引用方便,Linux定义了一个整体的数据结构tvec_base_t,以此统一处理各个定时器向量。
4动态定时器的内部实现机制
在内核动态定时器机制的实现中,有三个操作时非常重要的:
²将一个定时器插入到它应该所处的定时器向量中。
²定时器的迁移,也即将一个定时器从它原来所处的定时器向量迁移到另一个定时器向量中。
²扫描并执行当前已经到期的定时器。
4.1动态定时器机制的初始化
函数init_timers_cpu()实现对动态定时器机制的初始化。
该函数被sched_init()初始化例程所调用。
动态定时器机制初始化过程的主要任务就是将tv1、tv2、…、tv5这5个成员变量中的定时器向量指针数组vec[]初始化为NULL。
对于SMP,bootCPU使用静态定义的boot_tvec_bases,而其他CPU都是动态申请的。
如下所示(kernel/timer.c):
staticint__devinitinit_timers_cpu(intcpu)
1351{
1352intj;
1353tvec_base_t*base;
1354staticchar__devinitdatatvec_base_done[NR_CPUS];
1355
1356if(!
tvec_base_done[cpu]){
1357staticcharboot_done;
1358
1359if(boot_done){
1360/*
1361*TheAPsusethispathlaterinboot
1362*/
1363base=kmalloc_node(sizeof(*base),GFP_KERNEL,
1364cpu_to_node(cpu));
1365if(!
base)
1366return-ENOMEM;
1367memset(base,0,sizeof(*base));
1368per_cpu(tvec_bases,cpu)=base;
1369}else{
1370/*
1371*ThisisforthebootCPU-weusecompile-time
1372*staticinitialisationbecauseper-cpumemoryisn't
1373*readyyetandbecausethememoryallocatorsarenot
1374*initialisedeither.
1375*/
1376boot_done=1;
1377base=&boot_tvec_bases;
1378}
1379tvec_base_done[cpu]=1;
1380}else{
1381base=per_cpu(tvec_bases,cpu);
1382}
1383
1384spin_lock_init(&base->lock);
1385lockdep_set_class(&base->lock,base_lock_keys+cpu);
1386
1387for(j=0;j
1388INIT_LIST_HEAD(base->tv5.vec+j);
1389INIT_LIST_HEAD(base->tv4.vec+j);
1390INIT_LIST_HEAD(base->tv3.vec+j);
1391INIT_LIST_HEAD(base->tv2.vec+j);
1392}
1393for(j=0;j
1394INIT_LIST_HEAD(base->tv1.vec+j);
1395
1396base->timer_jiffies=jiffies;
1397return0;
1398}
4.2将一个定时器插入到链表中
函数internal_add_timer()用于将一个不处于任何定时器向量中的定时器插入到它应该所处的定时器向量中去(根据定时器的expires值来决定)。
如下所示(kernel/timer.c):
staticvoidinternal_add_timer(tvec_base_t*base,structtimer_list*timer)
92{
93unsignedlongexpires=timer->expires;
94unsignedlongidx=expires-base->timer_jiffies;
95structlist_head*vec;
96
97if(idx
98inti=expires&TVR_MASK;
99vec=base->tv1.vec+i;
100}elseif(idx
101inti=(expires>>TVR_BITS)&TVN_MASK;
102vec=base->tv2.vec+i;
103}elseif(idx
104inti=(expires>>(TVR_BITS+TVN_BITS))&TVN_MASK;
105vec=base->tv3.vec+i;
106}elseif(idx
107inti=(expires>>(TVR_BITS+2*TVN_BITS))&TVN_MASK;
108vec=base->tv4.vec+i;
109}elseif((signedlong)idx
110/*
111*Canhappenifyouaddatimerwithexpires==jiffies,
112*oryousetatimertogooffinthepast,thenreturncurrenttimerlist
113*/
114vec=base->tv1.vec+(base->timer_jiffies&TVR_MASK);
115}else{
116inti;
117/*Ifthetimeoutislargerthan0xffffffffon64-bit
118*architecturesthenweusethemaximumtimeout:
119*/
120if(idx>0xffffffffUL){
121idx=0xffffffffUL;
122expires=idx+base->timer_jiffies;
123}
124i=(expires>>(TVR_BITS+3*TVN_BITS))&TVN_MASK;
125vec=base->tv5.vec+i;
126}
127/*
128*TimersareFIFO:
129*/
130list_add_tail(&timer->entry,vec);
131}
从最小值开始,根据TVR_BITS及TVN_BITS的值依次求商(通过移位实现),来获得对应分组的list首地址,然后将定时器添加到对应list的尾部。
详细流程如下:
²首先,计算定时器的expires值与timer_jiffies的差值(注意!
这里应该使用动态定时器自己的时间基准),这个差值就表示这个定时器相对于上一次运行定时器机制的那个时刻还需要多长时间间隔才到期。
局部变量idx保存这个差值。
²根据idx的值确定这个定时器应被插入到哪一个定时器分组中。
而由expires确定的i值决定了对应定时器分组的哪个向量中。
定时器向量的头部指针vec表示这个定时器应该所处的定时器向量链表头部,其指针域指向有效的定时器。
²最后,调用list_add()函数将定时器插入到vec指针所指向的定时器队列的尾部。
4.3定时器迁移
由于一个定时器的interval值会随着时间的不断流逝(即jiffies值的不断增大)而不断变小,因此那些原本到期紧迫程度较低的定时器会随着jiffies值的不断增大而成为即将马上到期的定时器。
比如定时器向量tv2.vec[0]中的定时器在经过256个时钟滴答后会成为未来256个时钟滴答内会到期的定时器。
因此,定时器在内核动态定时器链表中的位置也应相应地随着改变。
改变的规则是:
²当timer_jiffies%TVR_SIZE重新变为0时,则意味着tv1中的256个定时器向量都已被内核扫描一遍了,从而使tv1中的256个定时器向量变为空(此处不考虑中途添加的定时器),此时需要用tv2.vec[(timer_jiffies>>TVR_BITS)&TVN_MASK]定时器向量中的定时器去填充tv1。
²随着timer_jiffies增加TVR_SIZE后,timer_jiffies>>TVR_BITS)&TVN_MASK自动加1,当timer_jiffies>>TVR_BITS)&TVN_MASK==TVN_MASK时,意味着tv2中的64个定时器向量都已经被全部填充到tv1中去了,从而使得tv2变为空,则用tv3.vec[(timer_jiffies>>(TVR_BITS+TVN_BITS)&TVN_MASK]定时器向量中的定时器去填充