软定时器和延迟函数

定时器是一种软件功能,即允许在将来的某个时刻,函数在给定的时间间隔用完被调用。超时表示与定时器相关的时间间隔已经用完的那个时刻。

内核和进程广泛使用定时器。大多数设备驱动程序利用定时器检测反常情况,例如,软盘驱动程序使用定时器在软盘暂时不被访问后就关闭的发动机,而并行打印机设备利用定时器检测错误的打印机情况。

编程人员也经常利用定时器在将来某一时刻强制执行特定的函数。

相对来说,实现一个定时器并不难。每个定时器都包含一个字段,表示定时器需要多长时间才到期。这个字段的初值就是jiffies的当前值加上合适的节拍数。这个字段的值不再改变。每当内核检查定时器时,就把这个到期字段和当前这一时刻jiffies的值相比较,当jiffies大于或等于这个字段存放的值时,定时器到期。

Linux考虑两种类型的定时器,即动态定时器和间隔定时器。第一种类型由内核使用,而间隔定时器可以由进程的用户态创建。

这里是有关Linux定时器的警告;因为对定时器函数的检查总是由可延迟函数进行,而可延迟函数被激活以后很长时间才能被执行,因此,内核不能确保定时器正好在定时到期时开始执行,而只能保证在适当的时间执行它们,或者假定延迟到几百毫秒之后执行它们。因此,对于必须严格遵守定时时间的那些实时性应用而,定时器不适合。

除了软定时器外,内核还使用了延迟函数,它执行一个紧凑的指令循环直到指定的时间间隔用完。我们将在后面的”延迟函数”一节对它们讨论。


动态定时器

动态定时器被动态地创建和撤消,对当前活动动态定时器的个数没有限制。动态定时器存放在下列timer_list结构中:

struct timer_list{

struct list_headentry;

unsigned longexpires;

spinlock_t lock;

unsigned longmagic;

void(*function)(unsigned long);

unsigned long data;

tvec_base_t *base;

};

function字段包含定时器到期时执行函数的地址。data字段指定传递给定时器函数的参数。正是由于data字段,就可以定义一个单独的通用函数来处理多个设备驱动程序的超时问题,在data字段可以存放设备ID,或其它有意义的数据,定时器函数可以用这些数据区分不同的设备。

expires字段给出定时器到期时间,时间用节拍数表示,其值为系统启动以来所经历过的节拍数。当expries的值小于或等于jiffies的值时,就说明计时器到期或终止。

entry字段用于将软定时器插入双向循环链表队列中,该链表根据定时器expires字段的值将它们分组存放。我们将在本章后面描述使用这些链表的算法。

为了创建并激活一个动态定时器,内核必须:

  1. 如果需要,创建一个新的timer_list对象,比如说设为t。这可以通过以下几种方式来进行:

a.在代码中定义一个静态全局变量

b.在函数内定义一个局部变量;在这情况下,这个对象存放在内核堆栈中。

c.在动态分配的描述符中包含这个对象。

  1. 调用init_timer(&t)函数初始化这个对象。实际上是把t.base指针字段置为NULL并把t.lock自旋锁设为”打开”.

  2. 把定时器到期时激活函数的地址存入funciton字段。如果需要,把传递给函数的参数值存入data字段。

  3. 如果动态定时器还没有被插入到链表中,给expires字赋一个合适的值并调用add_timer(&t)函数把t元素插入到合适的链表中。

  4. 否则,如果动态定时器已经插入到链表中,则调用mod_timer()函数来更新expires字段,这样也能将对象插入到合适的链表中。

一旦定时器到期,内核就自动把元素t从它的链表中删除。不过,有时进程应该用del_timer()del_timer_sync()del_singleshot_timer_syn()函数显式地从定时器链表中删除一个定时器。事实上,在定时器到时期之前,睡眠的进程可能被唤醒,在这种情况下,唤醒的进程就可以选定撤消某个定时器。虽然从链表中已删除的定时器上调用del_timer()没什么害处。不过,在定时器函数内删除定时器是一种的习惯做法。

linux2.6中,动态定时器需要CPU来激活,也就是说,定时器函数总会在每一个执行add_timer()或稍后执行mod_timer()函数的那同一个CPU上运行。不过,del_timer()及与其类似的函数能使所有动态定时器无效,即使该定时器并不依赖于本地CPU激活。

动态定时器与竞争条件

被异步激活的的动态定时器有参与竞争条件的倾向。例如,考虑一个动态定时器,它的函数作用于可丢弃的资源。如果在定时器函数被激活时资源不存在,那么不停止定时器就释放资源势必导致数据结构的崩溃。因此,一种凭经验的做法就是在释放资源前停止定时器:

del_timer(&t);

X_release_Resources();

然后,在多处理器系统上,这段代码是不安全的,因为当调用del_timer()函数时,定时器函数可能已经在其它CPU上运行了。结果,当定时器函数还作用在资源上时,资源可能被释放。为了避免这种竞争条件,内核提供了del_timer_sync()函数。这个函数从链表中删除定时器,然后检查定时器函数是否还在其它CPU上运行;如果是,del_timer_sync()就等待,直到定时器函数结束。

del_timer_sync()函数相当复杂,而且执行速度慢,因为它必须小心考虑这种情况:定时函数重新激活它自己。如果内核开发者知道定时器从不重新激活定时器,她就能使用更简单更快速的del_singleshot_timer_sync()函数来使定时器无效,并等直到定时器函数结束。

当然,也存在其它种类的竞争条件。例如,修改已激活定时器expires字段的正确方法是调用mod_timer(),而不是删除定时器随后又创建它。在后一种方法中,要修改同一定时器expires字段的两个内核控制路径可能糟糕地交错在一起。定时器函数在SMP上的安全实现是通过每个timer_list对象包含的lock自旋锁达到的:每当内核必须访问动态定时器的链表时,就禁止中断并猎取这个自旋锁。

动态定时器的数据结构

选择合适的数据结构实现动态定时器并不是件容易的事。把所有定时器放在一个单独的链表中会降低系统的性能,因为在每个时钟节拍去扫描一个定时器的长链表太费时。另一方面,维护一个排序的链表效率也不高,因为插入和删除操作也也非常费时。

解决的办法基于一种巧妙的数据结构,即把expires值划分成不同的大小,并允许动态定时器从大expires值的链表效率到小expires值的链表进行有效的过滤。此外,在多处理器系统中活动的动态定时器集合被分配到各个不同的CPU中。

动态定时器的主要数据结构是一个叫做tvec_bases的每CPU变量;它包括NR_CPUS个元素,系统中每个CPU各有一个。每个元素是一个tvec_base_t类型的数据结构,它包含相应CPU中处理动态定时器需要的所有数据。

Typedef structtvec_t_base_s{

spinlock_t lock;

unsigned longtimer_jiffies;

struct timer_list*running_timer;

tvec_root_t tvl

tvec_t tv2;

tvec_t tv3;

tvec_t tv4;

tvec_t tv5;

}tver_base_t;

字段tv1的数据结构为tvec_root_t类型,它包含一个vec数组,这个数组由256list_head元素组成。这个结构包含了在紧接着到来的255个节拍内将要到期的所有动态定时器。

字段tv2tv3tv4的数据结构都是tvec_t类型,该类型有一个数组vec。这些链表包含在紧接着到来的214次减1等几个节拍内将要到期的所有动态定时器。

字段tv5与前面的字段几乎相同,但唯一区别就是vec数组的最后一项是一个大expires字段值的动态定时器链表。tv5从不需要从其它的数组补充。图6-1用图例说明了5个链表组。

timer_jiffies字段的值表示需要检查的动态定时器的最早到期时间;如果这个值与jiffies的值一样,说明可延迟函数没有积压;如果这个值小于jiffies,说明前几个节拍相关的可延迟函数必须处理。该字段在系统启动时被设置成jiffies的值,且只能由run_timer_softirq()函数增加它的值。注意当处理动态定时器的可延迟函数在很长一段时间内都没有被执行时,timer_jiffies字段的值表示需要检查的动态定时器的最早到期时间;如果这个值与jiffies的值一样,说明可延迟函数没有积压;如果这个值小于jiffies,说明前几个节拍相关的可延迟函数必须处理。该字段在系统启动时被设置成jiffies的值,且只能由run_timer_softirq()函数增加它的值。注意当处理动态定时器的可延迟函数在很长一段时间内都没有被执行时,timer_jiffies字段可能会落后jiffies许多。

在多处理器系统中,字段running_timer指向由本地CPU当前正处理的动态定时器timer_list数据结构。

动态定时器处理

尽管软定时器具有巧妙的数据结构,但是对其处理是一种耗时的活动,所以不应该被时钟中断处理程序执行。在Linux.6中该活动由延迟函数来执行,也就是由TIMER_SOFTIRQ软中断行。

run_timer_softirq()函数是与TIMER_SOFTIRQ软中断请求相关的可延迟函数。它实质上执行如下操作:

  1. 把与本地CPU相关的tvec_base_t数据结构的地址存放到base本地变量中。

  2. 获得base->lock自旋锁并禁止本地中断

  3. 开始执行一个while循环,当base->timer_jiffies大于jiffies的值时终止,在每一次循环过程中,执行下列子步骤:

a.计算base->tv1中链表的索引,该索引保存着下一次将要处理的定时器:

index =base->timer_jiffies &255

b.如果索引值为0,说明base->tv1中的所有链表已经被检查过了,所以为空;于是该函数通过调用cascade()来过虑动态定时器

考虑每一次调用cascade函数的情况:它接收base的地址、base->tv2的地址、base->tv2中链表的索引作为参数。该索引值是通过观察base->timer_jiffies的特殊位上的值是决定的。cascade()函数将base->tv2中链表上的所有动态定时器移动base->tv1的适当链表上。然后,如果所有base->tv2中链表不为空,它返回一个正值。如base->tv2中的链表为空,cascade()将再次被调用,把base->tv3中的某个链表上包含的定时器填充到base->tv2上,如此等等。

  1. 使base->timer_jiffies的值加1

    1. 对于base->tv1.vec[index]链表上的每一个定时器,执行它所对应的定时器函数。特别说明的时,链表上的每个timer_list元素t实质上执行以下步骤。

e.链表上的所有定时器已经被处理。继续执行最外层while循环的下一次循环。

  1. 最外层的while循环结束,这就意味着所有到期的定时器已经被处理了。在多处器系统中,设置base->running_timerNULL

  2. 释放base->lock自旋锁并允许本地中断。

由于jiffiestimer_jiffies的值经常是一样的,所以最外层的while循环常常只执行一次。一般情况下,最外层循环会连续执行jiffies-base->timer_jiffies+1次。此外,如果在run_timer_softirq()正在执行时发生了时钟中断,那么也得考虑在这个节拍所出现的到期动态定时器,因为jiffies变量的值是由全局时钟中断处理程序异步增加的。

请注意,就在进入最外层循环前,run_timer_softirq()要禁止中断并获取base->lock自旋锁;调用每个动态定时器函数前,激活中断并释放自旋锁,直到函数执行结束,这就保证了动态定时器的数据结构不被交错执行的内核控制路径所破坏。

综上所述可知,这种相当复杂的算法确保了极好的性能。让我们来看看为什么,为了简单起见,假定TIMER_SOFTIRQ软中断正好在相应的时钟中断发生后执行。那么,在256次中出现的255次时钟中断,run_imter_softirq()仅仅运行到期定时器的函数,为了周期性地补充base->tv1.vec.64次补充当中,63次足以把base->tv2指向的链表分成base->tv1指向的256个链表。依次地,base->tv2.vec数组必须在0.006%的情况下得到补充,即使16.4秒一次。类似地,每1728秒补充一次base->tv3.vec,18小时38分补充一次base->tv4.vec,base->tv5.vec不需要补充。



  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值