博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

Linux 下定时器的实现方式分析 时间轮

Posted on 2016-04-08 14:43  bw_0927  阅读(1910)  评论(0)    收藏  举报

http://www.cnblogs.com/my_life/articles/5310361.html

http://www.ibm.com/developerworks/cn/linux/l-cn-timers/

http://blog.csdn.net/walkingman321/article/details/6101536

http://m.oschina.net/blog/222287

http://www.cnblogs.com/zhanghairong/p/3757656.html

 

定时器的实现,需要具备以下几个行为,这也是在后面评判各种定时器实现的一个基本模型 [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

 

 

基于链表和信号实现定时器 (2.4 版内核情况下 )

在 2.4 的内核中,并没有提供 POSIX timer [ 2 ]的支持,要在进程环境中支持多个定时器,只能自己来实现,好在 Linux 提供了 setitimer(2) 的接口。它是一个具有间隔功能的定时器 (interval timer),但如果想在进程环境中支持多个计时器,不得不自己来管理所有的计时器。 setitimer(2) 的定义如下:

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 信号

 

 

由于 setitimer() 不支持在同一进程中同时使用多次以支持多个定时器,因此,如果需要同时支持多个定时实例的话,需要由实现者来管理所有的实例。用 setitimer() 和链表,可以构造一个在进程环境下支持多个定时器实例的 Timer

 

基于 2.6 版本内核定时器的实现 (Posix 实时定时器 )

Linux 自 2.6 开始,已经开始支持 POSIX timer [ 2 ]所定义的定时器,它主要由下面的接口构成 :

清单 8. POSIX timer 接口
#include <signal.h> 
 #include <time.h> 

 int timer_create(clockid_t clockid, struct sigevent *evp, 
 timer_t *timerid); 
 int timer_settime(timer_t timerid, int flags, 
 const struct itimerspec *new_value, 
 struct itimerspec * old_value); 
 int timer_gettime(timer_t timerid, struct itimerspec *curr_value); 
 int timer_getoverrun(timer_t timerid); 
 int timer_delete(timer_t timerid);

这套接口是为了让操作系统对实时有更好的支持,在链接时需要指定 -lrt 。

 

清单 9. POSIX timer 接口中的信号和事件定义
union sigval { 
 int sival_int; 
 void *sival_ptr; 
 }; 

 struct sigevent { 
 int sigev_notify; /* Notification method */ 
 int sigev_signo; /* Timer expiration signal */ 
 union sigval sigev_value; /* Value accompanying signal or 
 passed to thread function */ 
 void (*sigev_notify_function) (union sigval); 
 /* Function used for thread 
 notifications (SIGEV_THREAD) */ 
 void *sigev_notify_attributes; 
 /* Attributes for notification thread 
 (SIGEV_THREAD) */ 
 pid_t sigev_notify_thread_id; 
 /* ID of thread to signal (SIGEV_THREAD_ID) */ 
 };

其中,sigev_notify 指明了通知的方式 :

SIGEV_NONE

当定时器到期时,不发送异步通知,但该定时器的运行进度可以使用 timer_gettime(2) 监测。

SIGEV_SIGNAL

当定时器到期时,发送 sigev_signo 指定的信号。

SIGEV_THREAD

当定时器到期时,以 sigev_notify_function 开始一个新的线程。该函数使用 sigev_value 作为其参数,当 sigev_notify_attributes 非空,则制定该线程的属性。注意,由于 Linux 上线程的特殊性,这个功能实际上是由 glibc 和内核一起实现的。

SIGEV_THREAD_ID (Linux-specific)

仅推荐在实现线程库时候使用。

 

如果 evp 为空的话,则该函数的行为等效于:sigev_notify = SIGEV_SIGNAL,sigev_signo = SIGVTALRM,sigev_value.sival_int = timer ID 。

由于 POSIX timer [ 2 ]接口支持在一个进程中同时拥有多个定时器实例,所以在上面的基于 setitimer() 和链表的 PerTickBookkeeping 动作就交由 Linux 内核来维护,这大大减轻了实现定时器的负担。由于 POSIX timer [ 2 ]接口在定时器到期时,有更多的控制能力,因此,可以使用实时信号避免信号的丢失问题,并将 sigev_value.sival_int 值指定为 timer ID,这样,就可以将多个定时器一起管理了。需要注意的是,POSIX timer [ 2 ]接口只在进程环境下才有意义 (fork(2) 和 exec(2) 也需要特殊对待 ),并不适合多线程环境。与此相类似的,Linux 提供了基于文件描述符的相关定时器接口:

 

清单 10. Linux 提供的基于文件描述符的定时器接口
#include <sys/timerfd.h> 

 int timerfd_create(int clockid, int flags); 
 int timerfd_settime(int fd, int flags, 
			 const struct itimerspec *new_value, 
			 struct itimerspec *old_value); 
 int timerfd_gettime(int fd, struct itimerspec *curr_value);

这样,由于基于文件描述符,使得该接口可以支持 select(2),poll(2) 等异步接口,使得定时器的实现和使用更加的方便,更重要的是,支持 fork(2),exec(2) 这样多进程的语义,因此,可以用在多线程环境之中,它们的使用比 POSIX timer [ 2 ]更加的灵活,其根本原因在于定时器的管理统一到了 unix/linux 基本哲学之一 ---- “一切皆文件”之下。

 

最小堆实现的定时器

基于时间轮 (Timing-Wheel) 方式实现的定时器

如果需要支持的定时器范围非常的大,时间轮的实现方式则不能满足这样的需求。

因为这样将消耗非常可观的内存,假设需要表示的定时器范围为:0 – 2^31ticks,则简单时间轮需要 2^32 个元素空间,这对于内存空间的使用将非常的庞大。也许可以降低定时器的精度,使得每个 Tick 表示的时间更长一些,但这样的代价是定时器的精度将大打折扣。现在的问题是,度量定时器的粒度,只能使用唯一粒度吗?想想日常生活中常遇到的水表,如下图 :

 :

在上面的水表中,为了表示度量范围,分成了不同的单位,比如 1000,100,10 等等,相似的,表示一个 32bits 的范围,也不需要 2^32 个元素的数组。实际上,Linux 的内核把定时器分为 5 组,每组的粒度(对应水表例子的单位)分别表示为:1 jiffies,256 jiffies,256*64 jiffies,256*64*64 jiffies,256*64*64*64 jiffies,每组中桶的数量分别为:256,64,64,64,64,能表示的范围为 2^32 。有了这样的实现,驱动内核定时器的机制也可以通过水表的例子来理解了,就像水表,每个粒度上都有一个指针指向当前时间,时间以固定 tick 递增,而当前时间指针则也依次递增,如果发现当前指针的位置可以确定为一个注册的定时器,就触发其注册的回调函数。 Linux 内核定时器本质上是 Single-Shot Timer,如果想成为 Repeating Timer,可以在注册的回调函数中再次的注册自己。以下是实现代码:

 

 http://blog.csdn.net/walkingman321/article/details/6101536

常用的定时器实现算法有两种:红黑树和时间轮(timing wheel)。

在Linux2.6的代码中,kernel/timer.c文件实现了一个通用定时器机制,使用的是时间轮算法。

 

每一个CPU都有一个struct tvec_base结构,代表这个CPU使用的时间轮。

struct tvec_base

{

       spinlock_t lock;                                  // 同步锁

       struct timer_list * running_timer;         // 当前正在运行的定时器

       unsigned long timer_jiffies;                  // 当前运行到的jiffies

       struct tvec_root tv1;                          //个位

       struct tvec tv2;  //十位

       struct tvec tv3;   //百位

       struct tvec tv4;   //千位

       struct tvec tv5;

}

 

struct tvec_root与struct tvec都是数组,数组中的每一项都指定一个链表。struct tvec_root定义的数组大小是256(2的8次方);struct tvec_root定义的数组大小是64(2的6次方)。所以,tv1~6定义的数组总大小是2的(8 + 4*6 = 32)次方,正好对应32位处理器中jiffies的定义(unsigned long)。

 

因为使用的是wheel算法,tv1~5就代表5个wheel。

tv1是转速最快的wheel,所有在256个jiffies内到期的定时器都会挂在tv1的某个链表头中。

tv2是转速第二快的wheel,里面挂的定时器超时jiffies在2^8 ~ 2^(8+6)之间。

tv3是转速第三快的wheel,超时jiffies在2^(8+6) ~ 2^(8+2*6)之间。

tv4、tv5类似。

 

http://www.cnblogs.com/zhanghairong/p/3757656.html