Linux 下定时器的实现方式分析
定时器属于基本的基础组件,不管是用户空间的程序开发,还是内核空间的程序开发,很多时候都需要有定时器作为基础组件的支持,但使用场景的不同,对定时器的实现考虑也不尽相同,本文讨论了在 Linux 环境下,应用层和内核层的定时器的各种实现方法,并分析了各种实现方法的利弊以及适宜的使用环境。
概论
定时器属于基本的基础组件,不管是用户空间的程序开发,还是内核空间的程序开发,很多时候都需要有定时器作为基础组件的支持,但使用场景的不同,对定时器的实现考虑也不尽相同,本文讨论了在 Linux 环境下,应用层和内核层的定时器的各种实现方法,并分析了各种实现方法的利弊以及适宜的使用环境。
首先,给出一个基本模型,定时器的实现,需要具备以下几个行为,这也是在后面评判各种定时器实现的一个基本模型 [1]:
StartTimer(Interval, TimerId, ExpiryAction)
注册一个时间间隔为 Interval 后执行 ExpiryAction 的定时器实例,其中,返回 TimerId 以区分在定时器系统中的其他定时器实例。
StopTimer(TimerId)
根据 TimerId 找到注册的定时器实例并执行 Stop 。
PerTickBookkeeping()
在一个 Tick 内,定时器系统需要执行的动作,它最主要的行为,就是检查定时器系统中,是否有定时器实例已经到期。注意,这里的 Tick 实际上已经隐含了一个时间粒度 (granularity) 的概念。
ExpiryProcessing()
在定时器实例到期之后,执行预先注册好的 ExpiryAction 行为。
上面说了基本的定时器模型,但是针对实际的使用情况,又有以下 2 种基本行为的定时器:
Single-Shot Timer
这种定时器,从注册到终止,仅仅只执行一次。
Repeating Timer
这种定时器,在每次终止之后,会自动重新开始。本质上,可以认为 Repeating Timer 是在 Single-Shot Timer 终止之后,再次注册到定时器系统里的 Single-Shot Timer,因此,在支持 Single-Shot Timer 的基础上支持 Repeating Timer 并不算特别的复杂。
基于链表和信号实现定时器 (2.4 版内核情况下 )
在 2.4 的内核中,并没有提供 POSIX timer [ 2 ]的支持,要在进程环境中支持多个定时器,只能自己来实现,好在 Linux 提供了 setitimer(2) 的接口。它是一个具有间隔功能的定时器 (interval timer),但如果想在进程环境中支持多个计时器,不得不自己来管理所有的计时器。 setitimer(2) 的定义如下:
清单 1. setitimer 的原型
#include <sys/time.h>
int setitimer(int which, const struct itimerval *new_value,struct itimerval *old_value);
setitimer 能够在 Timer 到期之后,自动再次启动自己,因此,用它来解决 Single-Shot Timer 和 Repeating Timer 的问题显得很简单。该函数可以工作于 3 种模式:
ITIMER_REAL 以实时时间 (real time) 递减,在到期之后发送 SIGALRM 信号
ITIMER_VIRTUAL 仅进程在用户空间执行时递减,在到期之后发送 SIGVTALRM 信号
ITIMER_PROF 进程在用户空间执行以及内核为该进程服务时 ( 典型如完成一个系统调用 ) 都会递减,与 ITIMER_VIRTUAL 共用时可度量该应用在内核空间和用户空间的时间消耗情况,在到期之后发送 SIGPROF 信号
定时器的值由下面的结构定义:
清单 2. setitimer 定时器的值定义
struct itimerval {
struct timeval it_interval; /* next value */
struct timeval it_value; /* current value */
};
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
setitimer() 以 new_value 设置特定的定时器,如果 old_value 非空,则它返回 which 类型时间间隔定时器的前一个值。定时器从 it_value 递减到零,然后产生一个信号,并重新设置为 it_interval,如果此时 it_interval 为零,则该定时器停止。任何时候,只要 it_value 设置为零,该定时器就会停止。
由于 setitimer() 不支持在同一进程中同时使用多次以支持多个定时器,因此,如果需要同时支持多个定时实例的话,需要由实现者来管理所有的实例。用 setitimer() 和链表,可以构造一个在进程环境下支持多个定时器实例的 Timer,在一般的实现中的 PerTickBookkeeping 时,会递增每个定时器的 elapse 值,直到该值递增到最初设定的 interval 则表示定时器到期。