Linux 下定时器的实现方式分析.docx
《Linux 下定时器的实现方式分析.docx》由会员分享,可在线阅读,更多相关《Linux 下定时器的实现方式分析.docx(16页珍藏版)》请在冰豆网上搜索。
Linux下定时器的实现方式分析
概论
定时器属于基本的基础组件,不管是用户空间的程序开发,还是内核空间的程序开发,很多时候都需要有定时器作为基础组件的支持,但使用场景的不同,对定时器的实现考虑也不尽相同,本文讨论了在Linux环境下,应用层和内核层的定时器的各种实现方法,并分析了各种实现方法的利弊以及适宜的使用环境。
首先,给出一个基本模型,定时器的实现,需要具备以下几个行为,这也是在后面评判各种定时器实现的一个基本模型[1]:
StartTimer(Interval,TimerId,ExpiryAction)
注册一个时间间隔为Interval后执行ExpiryAction的定时器实例,其中,返回TimerId以区分在定时器系统中的其他定时器实例。
StopTimer(TimerId)
根据TimerId找到注册的定时器实例并执行Stop。
PerTickBookkeeping()
在一个Tick内,定时器系统需要执行的动作,它最主要的行为,就是检查定时器系统中,是否有定时器实例已经到期。
注意,这里的Tick实际上已经隐含了一个时间粒度(granularity)的概念。
ExpiryProcessing()
在定时器实例到期之后,执行预先注册好的ExpiryAction行为。
上面说了基本的定时器模型,但是针对实际的使用情况,又有以下2种基本行为的定时器:
Single-ShotTimer
这种定时器,从注册到终止,仅仅只执行一次。
RepeatingTimer
这种定时器,在每次终止之后,会自动重新开始。
本质上,可以认为RepeatingTimer是在Single-ShotTimer终止之后,再次注册到定时器系统里的Single-ShotTimer,因此,在支持Single-ShotTimer的基础上支持RepeatingTimer并不算特别的复杂。
回页首
基于链表和信号实现定时器(2.4版内核情况下)
在2.4的内核中,并没有提供POSIXtimer[2]的支持,要在进程环境中支持多个定时器,只能自己来实现,好在Linux提供了setitimer
(2)的接口。
它是一个具有间隔功能的定时器(intervaltimer),但如果想在进程环境中支持多个计时器,不得不自己来管理所有的计时器。
setitimer
(2)的定义如下:
清单1.setitimer的原型
#include
intsetitimer(intwhich,conststructitimerval*new_value,structitimerval*old_value);
setitimer能够在Timer到期之后,自动再次启动自己,因此,用它来解决Single-ShotTimer和RepeatingTimer的问题显得很简单。
该函数可以工作于3种模式:
ITIMER_REAL以实时时间(realtime)递减,在到期之后发送SIGALRM信号
ITIMER_VIRTUAL仅进程在用户空间执行时递减,在到期之后发送SIGVTALRM信号
ITIMER_PROF进程在用户空间执行以及内核为该进程服务时(典型如完成一个系统调用)都会递减,与ITIMER_VIRTUAL共用时可度量该应用在内核空间和用户空间的时间消耗情况,在到期之后发送SIGPROF信号
定时器的值由下面的结构定义:
清单2.setitimer定时器的值定义
structitimerval{
structtimevalit_interval;/*nextvalue*/
structtimevalit_value;/*currentvalue*/
};
structtimeval{
longtv_sec;/*seconds*/
longtv_usec;/*microseconds*/
};
setitimer()以new_value设置特定的定时器,如果old_value非空,则它返回which类型时间间隔定时器的前一个值。
定时器从it_value递减到零,然后产生一个信号,并重新设置为it_interval,如果此时it_interval为零,则该定时器停止。
任何时候,只要it_value设置为零,该定时器就会停止。
由于setitimer()不支持在同一进程中同时使用多次以支持多个定时器,因此,如果需要同时支持多个定时实例的话,需要由实现者来管理所有的实例。
用setitimer()和链表,可以构造一个在进程环境下支持多个定时器实例的Timer,在一般的实现中的PerTickBookkeeping时,会递增每个定时器的elapse值,直到该值递增到最初设定的interval则表示定时器到期。
基于链表实现的定时器可以定义为:
清单3.基于链表的定时器定义
typedefinttimer_id;
/**
*Thetypeofcallbackfunctiontobecalledbytimerschedulerwhenatimer
*hasexpired.
*
*@paramidThetimerid.
*@paramuser_dataTheuserdata.
*$paramlenThelengthofuserdata.
*/
typedefinttimer_expiry(timer_idid,void*user_data,intlen);
/**
*Thetypeofthetimer
*/
structtimer{
LIST_ENTRY(timer)entries;/**timer_idid;/**intinterval;/**intelapse;/**<0->interval*/
timer_expiry*cb;/**void*user_data;/**intlen;/**};
定时器的时间间隔以interval表示,而elapse则在PerTickBookkeeping()时递增,直到interval表示定时器中止,此时调用回调函数cb来执行相关的行为,而user_data和len为用户可以传递给回调函数的参数。
所有的定时器实例以链表来管理:
清单4.定时器链表
/**
*Thetimerlist
*/
structtimer_list{
LIST_HEAD(listheader,timer)header;/**intnum;/**intmax_num;/**void(*old_sigfunc)(int);/**void(*new_sigfunc)(int);/**structitimervalovalue;/**structitimervalvalue;/**};
这里关于链表的实现使用了BSD风格关于链表的一组宏,避免了再造轮子;该结构中,old_sigfunc在init_timer初始定时器链表时候用来保存系统对SIGALRM的处理函数,在定时器系统destory时用来恢复到之前的处理函数;ovalue的用途与此类似。
清单5.定时器链表的创建和Destroy
/**
*Createatimerlist.
*
*@paramcountThemaximumnumberoftimerentriestobesupportedinitially.
*
*@return0meansok,theothermeansfail.
*/
intinit_timer(intcount)
{
intret=0;
if(count<=0||count>MAX_TIMER_NUM){
printf("thetimermaxnumberMUSTlessthan%d.\n",MAX_TIMER_NUM);
return-1;
}
memset(&timer_list,0,sizeof(structtimer_list));
LIST_INIT(&timer_list.header);
timer_list.max_num=count;
/*Registerourinternalsignalhandlerandstoreoldsignalhandler*/
if((timer_list.old_sigfunc=signal(SIGALRM,sig_func))==SIG_ERR){
return-1;
}
timer_list.new_sigfunc=sig_func;
/*Settingourintervaltimerfordriverourmutil-timerandstoreoldtimervalue*/
timer_list.value.it_value.tv_sec=TIMER_START;
timer_list.value.it_value.tv_usec=0;
timer_list.value.it_interval.tv_sec=TIMER_TICK;
timer_list.value.it_interval.tv_usec=0;
ret=setitimer(ITIMER_REAL,&timer_list.value,&timer_list.ovalue);
returnret;
}
/**
*Destroythetimerlist.
*
*@return0meansok,theothermeansfail.
*/
intdestroy_timer(void)
{
structtimer*node=NULL;
if((signal(SIGALRM,timer_list.old_sigfunc))==SIG_ERR){
return-1;
}
if((setitimer(ITIMER_REAL,&timer_list.ovalue,&timer_list.value))<0){
return-1;
}
while(!
LIST_EMPTY(&timer_list.header)){/*Delete.*/
node=LIST_FIRST(&timer_list.header);
LIST_REMOVE(node,entries);
/*Freenode*/
printf("Removeid%d\n",node->id);
free(node->user_data);
free(node);
}
memset(&timer_list,0,sizeof(structtimer_list));
return0;
}
添加定时器的动作非常的简单,本质只是一个链表的插入而已:
清单6.向定时器链表中添加定时器
/**
*Addatimertotimerlist.
*
*@paramintervalThetimerinterval(second).
*@paramcbWhencb!
=NULLandtimerexpiry,callit.
*@paramuser_dataCallback'sparam.
*@paramlenThelengthoftheuser_data.
*
*@returnThetimerID,if==INVALID_TIMER_ID,addtimerfail.
*/
timer_idadd_timer(intinterval,timer_expiry*cb,void*user_data,intlen)
{
structtimer*node=NULL;
if(cb==NULL||interval<=0){
returnINVALID_TIMER_ID;
}
if(timer_list.numtimer_list.num++;
}else{
returnINVALID_TIMER_ID;
}
if((node=malloc(sizeof(structtimer)))==NULL){
returnINVALID_TIMER_ID;
}
if(user_data!
=NULL||len!
=0){
node->user_data=malloc(len);
memcpy(node->user_data,user_data,len);
node->len=len;
}
node->cb=cb;
node->interval=interval;
node->elapse=0;
node->id=timer_list.num;
LIST_INSERT_HEAD(&timer_list.header,node,entries);
returnnode->id;
}
注册的信号处理函数则用来驱动定时器系统:
清单7.信号处理函数驱动定时器
/*TickBookkeeping*/
staticvoidsig_func(intsigno)
{
structtimer*node=timer_list.header.lh_first;
for(;node!
=NULL;node=node->entries.le_next){
node->elapse++;
if(node->elapse>=node->interval){
node->elapse=0;
node->cb(node->id,node->user_data,node->len);
}
}
}
它主要是在每次收到SIGALRM信号时,执行定时器链表中的每个定时器elapse的自增操作,并与interval相比较,如果相等,代表注册的定时器已经超时,这时则调用注册的回调函数。
上面的实现,有很多可以优化的地方:
考虑另外一种思路,在定时器系统内部将维护的相对interval转换成绝对时间,这样,在每PerTickBookkeeping时,只需将当前时间与定时器的绝对时间相比较,就可以知道是否该定时器是否到期。
这种方法,把递增操作变为了比较操作。
并且上面的实现方式,效率也不高,在执行StartTimer,StopTimer,PerTickBookkeeping时,算法复杂度分别为O
(1),O(n),O(n),可以对上面的实现做一个简单的改进,在StartTimer时,即在添加Timer实例时,对链表进行排序,这样的改进,可以使得在执行StartTimer,StopTimer,PerTickBookkeeping时,算法复杂度分别为O(n),O
(1),O
(1)。
改进后的定时器系统如下图1:
图1.基于排序链表的定时器
回页首
基于2.6版本内核定时器的实现(Posix实时定时器)
Linux自2.6开始,已经开始支持POSIXtimer[2]所定义的定时器,它主要由下面的接口构成:
清单8.POSIXtimer接口
#include
#include
inttimer_create(clockid_tclockid,structsigevent*evp,timer_t*timerid);
inttimer_settime(timer_ttimerid,intflags,conststructitimerspec*new_value,
structitimerspec*old_value);
inttimer_gettime(timer_ttimerid,structitimerspec*curr_value);
inttimer_getoverrun(timer_ttimerid);
inttimer_delete(timer_ttimerid);
这套接口是为了让操作系统对实时有更好的支持,在链接时需要指定-lrt。
timer_create
(2):
创建了一个定时器。
timer_settime
(2):
启动或者停止一个定时器。
timer_gettime
(2):
返回到下一次到期的剩余时间值和定时器定义的时间间隔。
出现该接口的原因是,如果用户定义了一个1ms的定时器,可能当时系统负荷很重,导致该定时器实际上10ms后才超时,这种情况下,overrun=9ms。
timer_getoverrun
(2):
返回上次定时器到期时超限值。
timer_delete
(2):
停止并删除一个定时器。
上面最重要的接口是timer_create
(2),其中,clockid表明了要使用的时钟类型,在POSIX中要求必须实现CLOCK_REALTIME类型的时钟。
evp参数指明了在定时到期后,调用者被通知的方式。
该结构体定义如下:
清单9.POSIXtimer接口中的信号和事件定义
unionsigval{
intsival_int;
void*sival_ptr;
};
structsigevent{
intsigev_notify;/*Notificationmethod*/
intsigev_signo;/*Timerexpirationsignal*/
unionsigvalsigev_value;//Valueaccompanyingsignalorpassedtothreadfunction
void(*sigev_notify_function)(unionsigval);
/*Functionusedforthreadnotifications(SIGEV_THREAD)*/
void*sigev_notify_attributes;
/*Attributesfornotificationthread(SIGEV_THREAD)*/
pid_tsigev_notify_thread_id;
/*IDofthreadtosignal(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=timerID。
由于POSIXtimer[2]接口支持在一个进程中同时拥有多个定时器实例,所以在上面的基于setitimer()和链表的PerTickBookkeeping动作就交由Linux内核来维护,这大大减轻了实现定时器的负担。
由于POSIXtimer[2]接口在定时器到期时,有更多的控制能力,因此,可以使用实时信号避免信号的丢失问题,并将sigev_value.sival_int值指定为timerID,这样,就可以将多个定时器一起管理了。
需要注意的是,POSIXtimer[2]接口只在进程环境下才有意义(fork
(2)和exec
(2)也需要特殊对待),并不适合多线程环境。
与此相类似的,Linux提供了基于文件描述符的相关定时器接口:
清单10.Linux提供的基于文件描述符的定时器接口
#include//CentOS5.3中没有该头文件
inttimerfd_create(intclockid,intflags);
inttimerfd_settime(intfd,intflags,
conststructitimerspec*new_value,
structitimerspec*old_value);
inttimerfd_gettime(intfd,structitimerspec*curr_value);
这样,由于基于文件描述