http://www.linux-cn.com/html/linux/kernel/20070412/1886.shtml
http://linux.ccidnet.com/art/741/20061116/952655_1.html
Linux内核2.4版中去掉了老版本内核中的静态定时器机制,而只留下动态定时器。相应地在timer_bh()函数中也不再通
过run_old_timers()函数来运行老式的静态定时器。动态定时器与静态定时器这二个概念是相对于Linux内核定时器机
制的可扩展功能而言的,动态定时器是指内核的定时器队列是可以动态变化的,然而就定时器本身而言,二者并无本质的
区别。考虑到静态定时器机制的能力有限,因此Linux内核2.4版中完全去掉了以前的静态定时器机制。
7.6.1 Linux内核对定时器的描述
Linux在include/linux/timer.h头文件中定义了数据结构timer_list来描述一个内核定时器:
|
各数据成员的含义如下:
(1)双向链表元素list:用来将多个定时器连接成一条双向循环队列。
(2)expires:指定定时器到期的时间,这个时间被表示成自系统启动以来的时钟滴答计数(也即时钟节拍数)。当一个
定时器的expires值小于或等于jiffies变量时,我们就说这个定时器已经超时或到期了。在初始化一个定时器后,通常
把它的expires域设置成当前expires变量的当前值加上某个时间间隔值(以时钟滴答次数计)。
(3)函数指针function:指向一个可执行函数。当定时器到期时,内核就执行function所指定的函数。而data域则
被内核用作function函数的调用参数。
内核函数init_timer()用来初始化一个定时器。实际上,这个初始化函数仅仅将结构中的list成员初始化为空。如下所示(include/linux/timer.h):
|
由于定时器通常被连接在一个双向循环队列中等待执行(此时我们说定时器处于pending状态)。因此函数time_pending()
就可以用list成员是否为空来判断一个定时器是否处于pending状态。如下所示
|
时间比较操作
在定时器应用中经常需要比较两个时间值,以确定timer是否超时,所以Linux内核在timer.h头文件中定义了4个时间
关系比较操作宏。这里我们说时刻a在时刻b之后,就意味着时间值a≥b。Linux强烈推荐用户使用它所定义的下列4个
时间比较操作宏(include/linux/timer.h):
|
7.6.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值可以互不相同的一个定时器队列。
具体的组织方案可以分为两大部分:
(1)对于内核最关心的、interval值在[0,255]之间的前256个定时器向量,内核是这样组织它们的:这256个定
时器向量被组织在一起组成一个定时器向量数组,并作为数据结构timer_vec_root的一部分,该数据结构定义
在kernel/timer.c文件中,如下述代码段所示:
|
基于数据结构timer_vec_root,Linux定义了一个全局变量tv1,以表示内核所关心的前256个定时器向量。这样
内核在处理是否有到期定时器时,它就只从定时器向量数组tv1.vec[256]中的某个定时器向量内进行扫描。
而tv1的index字段则指定当前正在扫描定时器向量数组tv1.vec[256]中的哪一个定时器向量,也即该数组的索引,
其初值为0,最大值为255(以256为模)。每个时钟节拍时index字段都会加1。显然,index字段所指定的定时器
向量tv1.vec[index]中包含了当前时钟节拍内已经到期的所有动态定时器。而定时器向量tv1.vec[index+k]则
包含了接下来第k个时钟节拍时刻将到期的所有动态定时器。当index值又重新变为0时,就意味着内核已经扫描
了tv1变量中的所有256个定时器向量。在这种情况下就必须将那些以松散定时器向量语义来组织的定时器向量补充到tv1中来。
(2)而对于内核不关心的、interval值在[0xff,0xffffffff]之间的定时器,它们的到期紧迫程度也随其interval值
的不同而不同。显然interval值越小,定时器紧迫程度也越高。因此在将它们以松散定时器向量进行组织时也应该区别
对待。通常,定时器的interval值越小,它所处的定时器向量的松散度也就越低(也即向量中的各定时器的expires值
相差越小);而interval值越大,它所处的定时器向量的松散度也就越大(也即向量中的各定时器的expires值相差越大)。
内核规定,对于那些满足条件:0x100≤interval≤0x3fff的定时器,只要表达式(interval>>8)具有相同值的定时
器都将被组织在同一个松散定时器向量中。因此,为组织所有满足条件0x100≤interval≤0x3fff的定时器,就需
要26=64个松散定时器向量。同样地,为方便起见,这64个松散定时器向量也放在一起形成数组,并作为数据结
构timer_vec的一部分。基于数据结构timer_vec,Linux定义了全局变量tv2,来表示这64条松散定时器向量。如
上述代码段所示。
对于那些满足条件0x4000≤interval≤0xfffff的定时器,只要表达式(interval>>8+6)的值相同的定时器都将被
放在同一个松散定时器向量中。同样,要组织所有满足条件0x4000≤interval≤0xfffff的定时器,也需要26=64个
松散定时器向量。类似地,这64个松散定时器向量也可以用一个timer_vec结构来描述,相应地Linux定义了tv3全
局变量来表示这64个松散定时器向量。
对于那些满足条件0x100000≤interval≤0x3ffffff的定时器,只要表达式(interval>>8+6+6)的值相同的定时
器都将被放在同一个松散定时器向量中。同样,要组织所有满足条件0x100000≤interval≤0x3ffffff的定时器,也
需要26=64个松散定时器向量。类似地,这64个松散定时器向量也可以用一个timer_vec结构来描述,相应地Linux
定义了tv4全局变量来表示这64个松散定时器向量。
对于那些满足条件0x4000000≤interval≤0xffffffff的定时器,只要表达式(interval>>8+6+6+6)的值相同的
定时器都将被放在同一个松散定时器向量中。同样,要组织所有满足条件0x4000000≤interval≤0xffffffff的定时器,
也需要26=64个松散定时器向量。类似地,这64个松散定时器向量也可以用一个timer_vec结构来描述,相应地Linux定
义了tv5全局变量来表示这64个松散定时器向量。
最后,为了引用方便,Linux定义了一个指针数组tvecs[],来分别指向tv1、tv2、…、tv5结构变量。如上述代码所示。
7.6.3 内核动态定时器机制的实现
在内核动态定时器机制的实现中,有三个操作时非常重要的:
(1)将一个定时器插入到它应该所处的定时器向量中。
(2)定时器的迁移,也即将一个定时器从它原来所处的定时器向量迁移到另一个定时器向量中。
(3)扫描并执行当前已经到期的定时器。
7.6.3.1 动态定时器机制的初始化
函数init_timervecs()实现对动态定时器机制的初始化。该函数仅被sched_init()初始化例程所调用。动态定时
器机制初始化过程的主要任务就是将tv1、tv2、…、tv5这5个结构变量中的定时器向量指针数组vec[]初始化
为NULL。如下所示(kernel/timer.c):
|
上述函数中的宏TVN_SIZE是指timer_vec结构类型中的定时器向量指针数组vec[]的大小,值为64。
宏TVR_SIZE是指timer_vec_root结构类型中的定时器向量数组vec[]的大小,值为256。
7.6.3.2 动态定时器的时钟滴答基准timer_jiffies
由于动态定时器是在时钟中断的Bottom Half中被执行的,而从TIMER_BH向量被激活到其timer_bh()函数真正
执行这段时间内可能会有几次时钟中断发生。因此内核必须记住上一次运行定时器机制是什么时候,也即内核必须
保存上一次运行定时器机制时的jiffies值。为此,Linux在kernel/timer.c文件中定义了全局变量timer_jiffies来表
示上一次运行定时器机制时的jiffies值。该变量的定义如下所示:
|
7.6.3.3 对内核动态定时器链表的保护
由于内核动态定时器链表是一种系统全局共享资源,为了实现对它的互斥访问,Linux定义了专门的自旋锁timerlist_lock
来保护。任何想要访问动态定时器链表的代码段都首先必须先持有该自旋锁,并且在访问结束后释放该自旋锁。其定义
如下(kernel/timer.c):
|
7.6.3.4 将一个定时器插入到链表中
函 数add_timer()用来将参数timer指针所指向的定时器插入到一个合适的定时器链表中。它首先调用timer_pending()函
数判断所指 定的定时器是否已经位于在某个定时器向量中等待执行。如果是,则不进行任何操作,只是打印一条内核告警
信息就返回了;如果不是,则调用 internal_add_timer()函数完成实际的插入操作。其源码如下(kernel/timer.c):
|
函数internal_add_timer()用于将一个不处于任何定时器向量中的定时器插入到它应该所处的定时器向量中去(根据定
时器的expires值来决定)。如下所示(kernel/timer.c):
|
对该函数的注释如下:
(1)首先,计算定时器的expires值与timer_jiffies的插值(注意!这里应该使用动态定时器自己的时间基准),这个差值
就表示这个定时器相对于上一次运行定时器机制的那个时刻还需要多长时间间隔才到期。局部变量idx保存这个差值。
(2)根据idx的值确定这个定时器应被插入到哪一个定时器向量中。其具体的确定方法我们在7.6.2节已经说过了,这里不再
详述。最后,定时器向量的头部指针vec表示这个定时器应该所处的定时器向量链表头部。
(3)最后,调用list_add()函数将定时器插入到vec指针所指向的定时器队列的尾部。
7.6.3.5 修改一个定时器的expires值
当一个定时器已经被插入到内核动态定时器链表中后,我们还可以修改该定时器的expires值。函数mod_timer()实现这一点。
如下所示(kernel/timer.c):
|
该函数首先根据参数expires值更新定时器的expires成员。然后调用detach_timer()函数将该定时器从它原来所属的链表
中删除。最后调用internal_add_timer()函数将该定时器根据它新的expires值重新插入到相应的链表中。
函 数detach_timer()首先调用timer_pending()来判断指定的定时器是否已经处于某个链表中,如果定时器原来就不处于任
何链表中, 则detach_timer()函数什么也不做,直接返回0值,表示失败。否则,就调用list_del()函数将定时器从它原来所
处的链表中摘除。如下 所示(kernel/timer.c):
|
7.6.3.6 删除一个定时器
函数del_timer()用来将一个定时器从相应的内核定时器队列中删除。该函数实际上是对detach_timer()函数的高层封装。
如下所示(kernel/timer.c):
|
7.6.3.7 定时器迁移操作
由于一个定时器的interval值会随着时间的不断流逝(即jiffies值的不断增大)而不断变小,因此那些原本到期紧迫程度较
低的定时器会随着jiffies值的不断增大而成为既把马上到期的定时器。比如定时器向量tv2.vec[0]中的定时器在经过256个
时钟滴答后会成为未来256个时钟滴答内会到期的定时器。因此,定时器在内核动态定时器链表中的位置也应相应地随着改
变。改变的规则是:当tv1.index重新变为0时(意味着tv1中的256个定时器向量都已被内核扫描一遍了,从而使tv1中的
256个定时器向量变为空),则用tv2.vec[index]定时器向量中的定时器去填充tv1,同时使tv2.index加1(它以64为模)。
当tv2.index重新变为0(意味着tv2中的64个定时器向量都已经被全部填充到tv1中去了,从而使得tv2变为空),则
用tv3.vec[index]定时器向量中的定时器去填充tv2。如此一直类推下去,直到tv5。
函数cascade_timers()完成这种定时器迁移操作,该函数只有一个timer_vec结构类型指针的参数tv。这个函数把把定时
器向量tv->vec[tv->index]中的所有定时器重新填充到上一层定时器向量中去。如下所示(kernel/timer.c):
|
对该函数的注释如下:
(1)首先,用指针head指向定时器头部向量头部的list_head结构。指针curr指向定时器向量中的第一个定时器。
(2)然后,用一个while{}循环来遍历定时器向量tv->vec[tv->index]。由于定时器向量是一个双向循环队列,因此循环
的终止条件是curr=head。对于每一个被扫描的定时器,循环体都先调用list_del()函数把当前定时器从链表中摘除,然后调
用internal_add_timer()函数重新确定该定时器应该被放到哪个定时器向量中去。
(3)当从while{}循环退出后,定时器向量tv->vec[tv->index]中所有的定时器都已被迁移到其它地方(到它们该呆的地
方:-),因此它本身就成为一个空队列。这里我们显示地调用INIT_LIST_HEAD()宏来把定时器向量的表头结构初始化为空。
(4)最后,把tv->index值加1,当然它是以64为模。 以上文章内容均为网络转载或者本站会员原创,"Linux-cn.com不对
文章内容做任何保证。
7.6.4.8 扫描并执行当前已经到期的定时器
函数run_timer_list()完成这个功能。如前所述,该函数是被timer_bh()函数所调用的,因此内核定时器是在时钟中
断的Bottom Half中被执行的。记住这一点非常重要。全局变量timer_jiffies表示了内核上一次执行run_timer_list()函
数的时间,因此jiffies与timer_jiffies的差值就表示了自从上一次处理定时器以来,期间一共发生了多少次时钟中断,显
然run_timer_list()函数必须为期间所发生的每一次时钟中断补上定时器服务。该函数的源码如下(kernel/timer.c):
|
函数run_timer_list()的执行过程主要就是用一个大while{}循环来为时钟中断执行定时器服务,每一次循环服务一次时钟
中断。因此一共要执行(jiffies-timer_jiffies+1)次循环。循环体所执行的服务步骤如下:
(1)首先,判断tv1.index是否为0,如果为0则需要从tv2中补充定时器到tv1中来。但tv2也可能为空而需要从tv3中补充
定时器,因此用一个do{}while循环来调用cascade_timer()函数来依次视需要从tv2中补充tv1,从tv3中补充tv2、…、
从tv5中补充tv4。显然如果tvi.index=0(2≤i≤5),则对于tvi执行cascade_timers()函数后,tvi.index肯定为1。
反过来讲,如果对tvi执行过cascade_timers()函数后tvi.index不等于1,那么可以肯定在未对tvi执行cascade_timers()函
数之前,tvi.index值肯定不为0,因此这时tvi不需要从tv(i+1)中补充定时器,这时就可以终止do{}while循环。
(2)接下来,就要执行定时器向量tv1.vec[tv1.index]中的所有到期定时器。因此这里用一个goto repeat循环从头到尾
依次扫描整个定时器对列。由于在执行定时器的关联函数时并不需要关CPU中断,所以在用detach_timer()函数把当前定时
器从对列中摘除后,就可以调用spin_unlock_irq()函数进行解锁和开中断,然后在执行完当前定时器的关联函数后重新用
spin_lock_irq()函数加锁和关中断。
(3)当执行完定时器向量tv1.vec[tv1.index]中的所有到期定时器后,tv1.vec[tv1.index]应该是个空队列。至此这一次
定时器服务也就宣告结束。
(4)最后,把timer_jiffies值加1,把tv1.index值加1,当然它的模是256。然后,回到while循环开始下一次定时器服务。