怎样在Linux下实现精确linux 多定时器实现 c

select实现高精度定时器
select实现高精度定时器
此博文禁止转载,谢谢
因为各种各样的原因,有时候必须把超时和socket的事件放在一个线程里,也就是说不可避免的要用select做超时功能,
经过测试,select在windows下的timeout功能精度太低,大概在15ms左右,在linux下精度在1ms,我记得几年前就是这种情况,几年过去了,MS还是这个德性,一点长进都没有。
select函数本身限制够多的,比如可查询的handle数,linux下不重新编译内核的话,是1024,但是linux下有没有这个限制,而且性能更高的函数可选择。windows根本就没辙,就这样了,自己想点其他办法。
因为这个原因,几年前毅然选择了linux。
但是现在没辙了,大环境在windows,只能想点歪门邪道了。
在windows下,经测试select超时精度在15ms,timeval.tv_usec设为1,,都一样,14ms多超时,说明精度在15ms,但是为0的话,一般10us内超时。
但是我的要求是select的精度在1ms。得想办法来。
select的句柄有事件的话,select的返回挺快的,这是不是有可利用的地方,答案就是这个。
create两个dummy
handle与windows的1ms精度的多媒体定时器配合使用,使select超时达到1ms的精度。
伪代码如下:
1. create ReadDummySocket
2. bind& ReadDummySocket
3. create WriteDummySocket
4. call timeGetDevCaps() get windows multimedia timer
resolution
5.& steps in select timeout function
5.1 calculate timeout for select()
5.2 call timeSetEvent() function to set a windows multimedia
5.3 add ReadDummySocket to readset fdset, and call select() to
query handle's events or timeout
5.4 send a dummy packet to ReadDummySocket through WriteDummySocket
in the CALLBACK functin of windows multimedia timer
5.5 select return when ReadDummySocket has a readable event
5.6 call timeKillEvent() to kill the windows multimedia timer id
return in step 5.2
parts of .h file
int dummyReadS
int dummyWriteS
unsigned uTimeR
unsigned nTimeId;
parts of .cpp file
extern "C" void CALLBACK TestTimeProc(UINT id, UINT msg, DWORD
dwUser, DWORD dw1, DWORD dw2 )
&NotifyDummyReadSocket();
void InitMMTimer()
&dummyReadSocket = socket(AF_INET, SOCK_DGRAM,
&dummyWriteSocket = socket(AF_INET, SOCK_DGRAM,
&struct sockaddr_in
&readAddr.sin_family = AF_INET;
&readAddr.sin_addr.s_addr =
inet_addr("127.0.0.1");
&readPort = -1;
&for( int i=31313;
i&35000; i++)
&&readAddr.sin_port =
== bind(dummyReadSocket, (struct
sockaddr*)&readAddr, sizeof(readAddr)) )
&&&readPort =
&FD_SET((unsigned)dummyReadSocket,
&fReadSet);
&if( TIMERR_NOERROR ==
timeGetDevCaps(&tc, sizeof(tc)) )
&&uTimeResolution =
min(max(tc.wPeriodMin,1), tc.wPeriodMax);
&&timeBeginPeriod(uTimeResolution);
void SetMMTimer(UINT microSecond)
&UINT milliSeccond = microSecond /
&if( milliSeccond == 0
&if( milliSeccond
& uTimeResolution )
&&milliSeccond =
&if( milliSeccond
&&milliSeccond = 320;
&nTimeId =
timeSetEvent(milliSeccond, uTimeResolution, TestTimeProc,
(DWORD_PTR)this, TIME_ONESHOT);
void KillMMTimer()
&if( nTimeId != NULL )
&&timeKillEvent(nTimeId);
void NotifyDummyReadSocket()
&if( readPort == -1 )
&struct sockaddr_in
&addr.sin_family = AF_INET;
&addr.sin_addr.s_addr =
inet_addr("127.0.0.1");
&addr.sin_port = htons(readPort);
&sendto(dummyWriteSocket,
"1", 1, 0, (struct sockaddr*)&addr,
sizeof(addr));
void SelectThreadProc()
& unsigned uTimeout = tv_timeToDelay.tv_sec *
1000000 + tv_timeToDelay.tv_
SetMMTimer(uTimeout);
& //int nDiff = 0;
& //gettimeofday(&start,
& int selectResult =
select(fMaxNumSockets, &readSet,
&writeSet, &exceptionSet,
&tv_timeToDelay);
& KillMMTimer();
& if( FD_ISSET(dummyReadSocket,
&readSet) )
&& char buf[128];
&& recvfrom(dummyReadSocket, buf,
128, 0, NULL, NULL);
已投稿到:
以上网友发言只代表其个人观点,不代表新浪网的观点或立场。我转载的原文:
========== & &我是分割线 & ==========
在编写程序时,我们经常回用到定时器。本文讲述如何使用select实现超级时钟。使用select函数,我们能实现微妙级别精度的定时器。同时,select函数也是我们在编写非阻塞程序时经常用到的一个函数。
首先看看select函数原型如下:
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
参数说明:
slect的第一个参数nfds为fdset集合中最大描述符值加1,fdset是一个位数组,其大小限制为__FD_SETSIZE(1024),位数组的每一位代表其对应的描述符是否需要被检查。select的第二三四个参数表示需要关注读、写、错误事件的文件描述符位数组,这些参数既是输入参数也是输出参数,可能会被内核修改用于标示哪些描述符上发生了关注的事件。所以每次调用select前都需重新初始化fdset。timeout参数为超时时间,该结构会被内核修改,其值为超时剩余的时间。
利用select实现定时器,需要利用其timeout参数,注意到:
&1)select函数使用了一个结构体timeval作为其参数。
&2)select函数会更新timeval的值,timeval保持的值为剩余时间。
如果我们指定了参数timeval的值,而将其他参数都置为0或者NULL,那么在时间耗尽后,select函数便返回,基于这一点,我们可以利用select实现精确定时。
timeval的结构如下:
struct timeval{
long tv_sec;/*secons*
long tv_/*microseconds*/
我们可以看出其精确到microseconds也即微妙。
一、秒级定时器
void seconds_sleep(unsigned seconds){
tv.tv_sec=
tv.tv_usec=0;
err=select(0,NULL,NULL,NULL,&tv);
}while(err&0 && errno==EINTR);
&二、毫秒级别定时器
void milliseconds_sleep(unsigned long mSec){
tv.tv_sec=mSec/1000;
tv.tv_usec=(mSec%1000)*1000;
err=select(0,NULL,NULL,NULL,&tv);
}while(err&0 && errno==EINTR);
&三、微妙级别定时器
void microseconds_sleep(unsigned long uSec){
tv.tv_sec=uSec/1000000;
tv.tv_usec=uSec%1000000;
err=select(0,NULL,NULL,NULL,&tv);
}while(err&0 && errno==EINTR);
现在我们来编写几行代码看看定时效果吧。
#include &stdio.h&
#include &sys/time.h&
#include &errno.h&
int main()
for(i=0;i&5;++i){
printf(&%d\n&,i);
//seconds_sleep(1);
//milliseconds_sleep(1500);
microseconds_sleep(1900000);
&注:timeval结构体中虽然指定了一个微妙级别的分辨率,但内核支持的分别率往往没有这么高,很多unix内核将超时值向上舍入成10ms的倍数。此外,加上内核调度延时现象,即定时器时间到后,内核还需要花一定时间调度相应进程的运行。因此,定时器的精度,最终还是由内核支持的分别率决定。
参考知识库
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:209472次
积分:5620
积分:5620
排名:第3697名
原创:347篇
转载:93篇
评论:31条
(3)(2)(1)(8)(14)(37)(3)(2)(7)(15)(35)(17)(12)(16)(21)(56)(66)(31)(16)(8)(14)(20)(8)(7)(24)(4)(1)7404人阅读
一、jiffies定时器,HZ=100,精度只能达到10ms。
注:采用jiffies+msecs_to_jiffies(xx&ms);可做到ms级,不过精度不够
#include&&linux/jiffies.h&//DO--&jiffies调用头文件
#include&&linux/timer.h&&&//DO--&timer_list结构体
static&struct&timer_list&ms_//DO--&定义timer_list结构体
static&void&ms_timer_handler(void)//DO--&定义定时器处理函数
&&&&printk(&DO_DEBUG-----------&%s\n&,__func__);
&&&//&ms_timer.expires=jiffies+HZ;
&&&&ms_timer.expires=jiffies+msecs_to_jiffies(10);
&&&&ms_timer.function=&ms_timer_
&&&&add_timer(&ms_timer);
static&int32_t&xxx_init(void)
//&hrtimer_init_module();
&&&&init_timer(&ms_timer);&&&&&&&&&&&&&&&&&&&&&&&&&&//DO--&初始化定时器
&&&&ms_timer.expires=jiffies+msecs_to_jiffies(10);&&//DO--&定义中断时间:10ms进入中断
&&&&//ms_timer.expires=jiffies+HZ;&&
&&&&//ms_timer.data=(unsigned&long)ms_//区分不同定时器,未验证
&&&&ms_timer.function=&ms_timer_&&&&&&&&&&&&//DO--&定义定时器中断处理函数
&&&&add_timer(&ms_timer);&&&&&&&&&&&&&&&&&&&&&&&&&&&//DO--&增加注册定时器,使定时器生效
二、hrtimer高精度定时器,可做到ns级,此处做到毫秒如下例:
注:实际是为纳秒级,由此处ktime_set(const&long&secs,&const&unsigned&long&nsecs)决定的,参数下此处参数即可实现纳秒级。
#include&&linux/dma-mapping.h&&//DO--&hrtimer包含以下三个头文件&/*&DMA&APIs&&&&&&&&&&&&&*/&&&
#include&&linux/hrtimer.h&&&
#include&&linux/time.h&&&&&&&&&&&&/*&struct&timespec&&&&*/
#define&KER_PRINT(fmt,&...)&printk(&&ker-driver&&fmt,&##__VA_ARGS__);&&&
static&struct&hrtimer&vibe_&
static&struct&work_struct&vibe_&&
static&int&value&=&2000;&&&/*注:以毫秒ms为单位&Time&out&setting,2&seconds&*/
static&enum&hrtimer_restart&vibrator_timer_func(struct&hrtimer&*timer)&&//DO--&回调函数,中断时调用&
&&&&struct&timespec&
&&&&do_posix_clock_monotonic_gettime(&uptime);&&
&&&&KER_PRINT(&Time:%lu.%02lu\n&,&
&&&&&&&&&&&&(unsigned&long)&uptime.tv_sec,&&
&&&&&&&&&&&&(uptime.tv_nsec&/&(NSEC_PER_SEC&/&1000)));&
&&&&KER_PRINT(&vibrator_timer_func\n&);&&&
&&&&schedule_work(&vibe_work);&&
&&&&return&HRTIMER_NORESTART;&
static&void&vibe_work_func(struct&work_struct&*work)&&//DO--&工作队列函数
&&&&KER_PRINT(&'vibe_work_func'--&work\n&);&&
&&&//&msleep(50);&/*&CPU&sleep&*/
&&&&vibe_timer.function&=&vibrator_timer_&&
&&&&hrtimer_start(&vibe_timer,&
&&&&&&&&ktime_set(value&/&1000,&(value&%&1000)&*&1000000),HRTIMER_MODE_REL);&&
static&void&ker_driver_init(void)&&&&&&&&&&&&&&&&&&&&&&&&//DO--&hrtimer高精度定时器初始化函数
&&&&struct&timespec&&
&&&&KER_PRINT(&ker_driver_init\n&);&&
&&&&hrtimer_init(&vibe_timer,&CLOCK_MONOTONIC,&HRTIMER_MODE_REL);&&//DO--&hrtimer定时器初始化
&&&&vibe_timer.function&=&vibrator_timer_&&&&&&&&&&&&&&&&&&&&&//DO--&hrtimer定时器回调函数
&&&&hrtimer_start(&vibe_timer,&&
&&&&&&&&ktime_set(value&/&1000,&(value&%&1000)&*&1000000),HRTIMER_MODE_REL);&&//DO--&hrtimer定时器时间初始化,其中ktime_set(秒,纳秒)
&&&&do_posix_clock_monotonic_gettime(&uptime);&&&&//线程建立时间,用于比较看(定时器)此时时间
&&&&KER_PRINT(&Time:%lu.%02lu\n&,&
&&&&&&&&&&&&(unsigned&long)&uptime.tv_sec,&&
&&&&&&&&&&&&(uptime.tv_nsec&/&(NSEC_PER_SEC&/&1000)));&
&&&&INIT_WORK(&vibe_work,&vibe_work_func);&&/*&Intialize&the&work&queue&*/&&//初始化工作队列
static&int32_t&xxxx_init(void)
&&&&ker_driver_init();
参考知识库
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:33798次
排名:千里之外
原创:10篇
评论:10条
(1)(1)(3)(9)linux使用select实现精确定时器详解
字体:[ ] 类型:转载 时间:
本文讲述如何使用select实现超级时钟。使用select函数,我们能实现微妙级别精度的定时器。同时,select函数也是我们在编写非阻塞程序时经常用到的一个函数
在编写程序时,我们经常会用到定时器。首先看看select函数原型如下: 代码如下:int select(int nfds, fd_set *readfds, fd_set *writefds,&&&&&&&&&&&&&&&&& fd_set *exceptfds, struct timeval *timeout);参数说明:slect的第一个参数nfds为fdset集合中最大描述符值加1,fdset是一个位数组,其大小限制为__FD_SETSIZE(1024),位数组的每一位代表其对应的描述符是否需要被检查。select的第二三四个参数表示需要关注读、写、错误事件的文件描述符位数组,这些参数既是输入参数也是输出参数,可能会被内核修改用于标示哪些描述符上发生了关注的事件。所以每次调用select前都需重新初始化fdset。timeout参数为超时时间,该结构会被内核修改,其值为超时剩余的时间。利用select实现定时器,需要利用其timeout参数,注意到:&1)select函数使用了一个结构体timeval作为其参数。&2)select函数会更新timeval的值,timeval保持的值为剩余时间。如果我们指定了参数timeval的值,而将其他参数都置为0或者NULL,那么在时间耗尽后,select函数便返回,基于这一点,我们可以利用select实现精确定时。timeval的结构如下: 代码如下:struct timeval{long tv_sec;/*secons*long tv_/*microseconds*/}我们可以看出其精确到microseconds也即微妙。一、秒级定时器 代码如下:void seconds_sleep(unsigned seconds){&&&&&& tv.tv_sec=&&& tv.tv_usec=0;&&&&&& do{&&&&&& err=select(0,NULL,NULL,NULL,&tv);&&& }while(err&0 && errno==EINTR);}&二、毫秒级别定时器 代码如下:void milliseconds_sleep(unsigned long mSec){&&&&&& tv.tv_sec=mSec/1000;&&& tv.tv_usec=(mSec%;&&&&&& do{&&&&&& err=select(0,NULL,NULL,NULL,&tv);&&& }while(err&0 && errno==EINTR);}&三、微妙级别定时器 代码如下:void microseconds_sleep(unsigned long uSec){&&&&&& tv.tv_sec=uSec/1000000;&&& tv.tv_usec=uSec%1000000;&&&&&& do{&&&&&&& err=select(0,NULL,NULL,NULL,&tv);&&& }while(err&0 && errno==EINTR);}现在我们来编写几行代码看看定时效果吧。 代码如下:#include &stdio.h&#include &sys/time.h&#include &errno.h&int main(){&&&&&& for(i=0;i&5;++i){&&& printf("%d\n",i);&&& //seconds_sleep(1);&&& //milliseconds_sleep(1500);&&& microseconds_sleep(1900000);&&& }}&注:timeval结构体中虽然指定了一个微妙级别的分辨率,但内核支持的分别率往往没有这么高,很多unix内核将超时值向上舍入成10ms的倍数。此外,加上内核调度延时现象,即定时器时间到后,内核还需要花一定时间调度相应进程的运行。因此,定时器的精度,最终还是由内核支持的分别率决定。分类: Linux
您可能感兴趣的文章:
大家感兴趣的内容
12345678910
最近更新的内容
常用在线小工具developerWorks 社区
定时器属于基本的基础组件,不管是用户空间的程序开发,还是内核空间的程序开发,很多时候都需要有定时器作为基础组件的支持,但使用场景的不同,对定时器的实现考虑也不尽相同,本文讨论了在 Linux 环境下,应用层和内核层的定时器的各种实现方法,并分析了各种实现方法的利弊以及适宜的使用环境。
(), 开发工程师, Pixelworks
赵军,2005 年毕业于华中科技大学电气与电子工程学院,有 3 年基于 Linux 的 Router 开发经验,开发过基于 Linux 的高清 / 标清视频解码器,现在则开发基于 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_ /* next value */
struct timeval it_
/* current value */
struct timeval {
/* seconds */
/* microseconds */
};setitimer() 以 new_value 设置特定的定时器,如果 old_value 非空,则它返回 which 类型时间间隔定时器的前一个值。定时器从 it_value 递减到零,然后产生一个信号,并重新设置为 it_interval,如果此时 it_interval 为零,则该定时器停止。任何时候,只要 it_value 设置为零,该定时器就会停止。由于 setitimer() 不支持在同一进程中同时使用多次以支持多个定时器,因此,如果需要同时支持多个定时实例的话,需要由实现者来管理所有的实例。用 setitimer() 和链表,可以构造一个在进程环境下支持多个定时器实例的 Timer,在一般的实现中的 PerTickBookkeeping 时,会递增每个定时器的 elapse 值,直到该值递增到最初设定的 interval 则表示定时器到期。基于链表实现的定时器可以定义为:清单 3. 基于链表的定时器定义typedef int timer_
* The type of callback function to be called by timer scheduler when a timer
* has expired.
* @param id
The timer id.
* @param user_data
The user data.
* $param len
The length of user data.
typedef int timer_expiry(timer_id id, void *user_data, int len);
* The type of the timer
struct timer {
LIST_ENTRY(timer)/**& list entry
/**& timer id
/**& timer interval(second) */
/**& 0 -& interval
timer_expiry *
/**& call if expiry
void *user_
/**& callback arg
/**& user_data length
};定时器的时间间隔以 interval 表示,而 elapse 则在 PerTickBookkeeping() 时递增,直到 interval 表示定时器中止,此时调用回调函数 cb 来执行相关的行为,而 user_data 和 len 为用户可以传递给回调函数的参数。所有的定时器实例以链表来管理:清单 4. 定时器链表/**
* The timer list
struct timer_list {
LIST_HEAD(listheader, timer)
/**& list header
/**& timer entry number */
/**& max entry number
void (*old_sigfunc)(int);
/**& save previous signal handler */
void (*new_sigfunc)(int);
/**& our signal handler
/**& old timer value */
/**& our internal timer value */
};这里关于链表的实现使用了 BSD 风格关于链表的一组宏,避免了再造轮子;该结构中,old_sigfunc 在 init_timer 初始定时器链表时候用来保存系统对 SIGALRM 的处理函数,在定时器系统 destory 时用来恢复到之前的处理函数; ovalue 的用途与此类似。清单 5. 定时器链表的创建和 Destroy/**
* Create a timer list.
* @param count
The maximum number of timer entries to be supported initially.
0 means ok, the other means fail.
int init_timer(int count)
int ret = 0;
if(count &=0 || count & MAX_TIMER_NUM) {
printf("the timer max number MUST less than %d.\n", MAX_TIMER_NUM);
return -1;
memset(&timer_list, 0, sizeof(struct timer_list));
LIST_INIT(&timer_list.header);
timer_list.max_num =
/* Register our internal signal handler and store old signal handler */
if ((timer_list.old_sigfunc = signal(SIGALRM, sig_func)) == SIG_ERR) {
return -1;
timer_list.new_sigfunc = sig_
/*Setting our interval timer for driver our mutil-timer and store old timer value*/
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);
* Destroy the timer list.
0 means ok, the other means fail.
int destroy_timer(void)
struct timer *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);
/* Free node */
printf("Remove id %d\n", node-&id);
free(node-&user_data);
free(node);
memset(&timer_list, 0, sizeof(struct timer_list));
}添加定时器的动作非常的简单,本质只是一个链表的插入而已:清单 6. 向定时器链表中添加定时器/**
* Add a timer to timer list.
* @param interval
The timer interval(second).
* @param cb
When cb!= NULL and timer expiry, call it.
* @param user_data Callback's param.
* @param len
The length of the user_data.
The timer ID, if == INVALID_TIMER_ID, add timer fail.
add_timer(int interval, timer_expiry *cb, void *user_data, int len)
struct timer *node = NULL;
if (cb == NULL || interval &= 0) {
return INVALID_TIMER_ID;
if(timer_list.num & timer_list.max_num) {
timer_list.num++;
return INVALID_TIMER_ID;
if((node = malloc(sizeof(struct timer))) == NULL) {
return INVALID_TIMER_ID;
if(user_data != NULL || len != 0) {
node-&user_data = malloc(len);
memcpy(node-&user_data, user_data, len);
node-&len =
node-&cb =
node-&interval =
node-&elapse = 0;
node-&id = timer_list.
LIST_INSERT_HEAD(&timer_list.header, node, entries);
return node-&
}注册的信号处理函数则用来驱动定时器系统:清单 7. 信号处理函数驱动定时器/* Tick Bookkeeping */
static void sig_func(int signo)
struct timer *node = timer_list.header.lh_
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 开始,已经开始支持 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 。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. POSIX timer 接口中的信号和事件定义union sigval {
int sival_
void *sival_
struct sigevent {
int sigev_ /* Notification method */
int sigev_ /* Timer expiration signal */
union sigval sigev_ /* 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 for notification thread
(SIGEV_THREAD) */
pid_t sigev_notify_thread_
/* 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 基本哲学之一 ---- “一切皆文件”之下。最小堆实现的定时器最小堆指的是满足除了根节点以外的每个节点都不小于其父节点的堆。这样,堆中的最小值就存放在根节点中,并且在以某个结点为根的子树中,各节点的值都不小于该子树根节点的值。一个最小堆的例子如下图 2:图 2. 最小堆一个最小堆,一般支持以下几种操作:Insert(TimerHeap, Timer): 在堆中插入一个值,并保持最小堆性质,具体对应于定时器的实现,则是把定时器插入到定时器堆中。根据最小堆的插入算法分析,可以知道该操作的时间复杂度为 O(lgn) 。Minimum(TimerHeap): 获取最小堆的中最小值;在定时器系统中,则是返回定时器堆中最先可能终止的定时器。由于是最小堆,只需返回堆的 root 即可。此时的算法复杂度为 O(1) 。ExtractMin(TimerHeap): 在定时器到期后,执行相关的动作,它的算法复杂度为 O(1) 。最小堆本质上是一种最小优先级队列 (min-priority queue) 。定时可以作为最小优先级队列的一个应用,该优先级队列把定时器的时间间隔值转化为一个绝对时间来处理,ExtractMin 操则是在所有等待的定时器中,找出最先超时的定时器。在任何时候,一个新的定时器实例都可通过 Insert 操作加入到定时器队列中去。在 pjsip 项目的基础库 pjlib 中,有基于最小堆实现的定时器,它主要提供了以下的几个接口:清单 10. pjlib 提供的基于最小堆的定时器接口/**
* Create a timer heap.
PJ_DECL(pj_status_t) pj_timer_heap_create( pj_pool_t *pool,
pj_size_t count,
pj_timer_heap_t **ht);
* Destroy the timer heap.
PJ_DECL(void) pj_timer_heap_destroy( pj_timer_heap_t *ht );
* Initialize a timer entry. Application should call this function at least
* once before scheduling the entry to the timer heap, to properly initialize
* the timer entry.
PJ_DECL(pj_timer_entry*) pj_timer_entry_init( pj_timer_entry *entry,
void *user_data,
pj_timer_heap_callback *cb );
* Schedule a timer entry which will expire AFTER the specified delay.
PJ_DECL(pj_status_t) pj_timer_heap_schedule( pj_timer_heap_t *ht,
pj_timer_entry *entry,
const pj_time_val *delay);
* Cancel a previously registered timer.
PJ_DECL(int) pj_timer_heap_cancel( pj_timer_heap_t *ht,
pj_timer_entry *entry);
* Poll the timer heap, check for expired timers and call the callback for
* each of the expired timers.
PJ_DECL(unsigned) pj_timer_heap_poll( pj_timer_heap_t *ht,
pj_time_val *next_delay);pjlib 中的定时器在内部使用数组的方式实现堆,这样对于内存空间的使用将更加的紧凑;它的实现还可在定时器的数量超过预先设定的最大数量时会自己增加最大定时器数量。文件 pjlib/src/pjlib-test/timer.c 是它的一个单元测试。与基于链表方式的实现相比较,明显它的时间复杂度要低一些,这样可以支持更多的定时器实例。基于时间轮 (Timing-Wheel) 方式实现的定时器时间轮 (Timing-Wheel) 算法类似于一以恒定速度旋转的左轮手枪,枪的撞针则撞击枪膛,如果枪膛中有子弹,则会被击发;与之相对应的是:对于 PerTickBookkeeping,其最本质的工作在于以 Tick 为单位增加时钟,如果发现有任何定时器到期,则调用相应的 ExpiryProcessing 。设定一个循环为 N 个 Tick 单元,当前时间是在 S 个循环之后指向元素 i (i&=0 and i&= N - 1),则当前时间 (Current Time)Tc 可以表示为:Tc = S*N + i ;如果此时插入一个时间间隔 (Time Interval) 为 Ti 的定时器,设定它将会放入元素 n(Next) 中,则 n
= (Tc + Ti)mod N = (S*N + i + Ti) mod N = (i + Ti) mod N 。如果我们的 N 足够的大,显然 StartTimer,StopTimer,PerTickBookkeeping 时,算法复杂度分别为 O(1),O(1),O(1) 。在 [5] 中,给出了一个简单定时器轮实现的定时。下图 3 是一个简单的时间轮定时器:图 3. 简单时间轮如果需要支持的定时器范围非常的大,上面的实现方式则不能满足这样的需求。因为这样将消耗非常可观的内存,假设需要表示的定时器范围为:0 – 2^3-1ticks,则简单时间轮需要 2^32 个元素空间,这对于内存空间的使用将非常的庞大。也许可以降低定时器的精度,使得每个 Tick 表示的时间更长一些,但这样的代价是定时器的精度将大打折扣。现在的问题是,度量定时器的粒度,只能使用唯一粒度吗?想想日常生活中常遇到的水表,如下图 4:图 4. 水表在上面的水表中,为了表示度量范围,分成了不同的单位,比如 ,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,这样,在 256+64+64+64+64 = 512 个桶中,表示的范围为 2^32 。有了这样的实现,驱动内核定时器的机制也可以通过水表的例子来理解了,就像水表,每个粒度上都有一个指针指向当前时间,时间以固定 tick 递增,而当前时间指针则也依次递增,如果发现当前指针的位置可以确定为一个注册的定时器,就触发其注册的回调函数。 Linux 内核定时器本质上是 Single-Shot Timer,如果想成为 Repeating Timer,可以在注册的回调函数中再次的注册自己。内核定时器如下图 5:图 5. Linux 时间轮结论由上面的分析,可以看到各种定时器实现算法的复杂度:表 1. 定时器实现算法复杂度实现方式StartTimerStopTimerPerTickBookkeeping基于链表O(1)O(n)O(n)基于排序链表O(n)O(1)O(1)基于最小堆O(lgn)O(1)O(1)基于时间轮O(1)O(1)O(1)如果需要能在线程环境中使用的定时器,对于基于链表的定时器,可能需要很小心的处理信号的问题;而 POSIX
timer [ 2 ]接口的定时器,只具有进程的语义,如果想在多线程环境下也 n 能使用,可以使用 Linux 提供的 timerfd_create(2) 接口。如果需要支持的定时器数量非常的大,可以考虑使用基于最小堆和时间轮的方式来实现。
下载描述名字大小样例代码7KB
参考资料 George Varghese , Tony Lauck. Hashed and Hierarchical Timing Wheels: Efficient Data Structures for Implementing a Timer Facility ,这篇论文分析了各种定时器实现方法,并提出了时间轮的概念。如果你想很好的实现一个定时器,这篇论文你需要反复阅读。 ,如果你对 UNIX 的标准有兴趣,推荐你在该网站下一份最新的 Specifications,并且,这份 Specifications 的描述相当容易读,linux 应用层的编程,它是我除了 Steven 的书之外的第一选择。 是一个 Open Source 的 VoIP 项目,如果你不关心 VoIP,该项目中提供的 pjlib 库也值得关注,它是一个在嵌入式环境下的通用基础库实现的典范。 最出名的事件通知库之一,它的内部使用基于最小堆实现的优先级队列处理包括时间,信号等各种事件。Bo Berry 在文章“” 中描述了一个简单时间轮算法实现的定时器。。Ingo Molnar 对 linux 中的时间轮的实现做了最好的解释。Thomas H. Cormen,Charles E. Leiserson,Ronald L. Rivest,Clifford Stein 著.算法导论 ( 第二版 ) .北京:机械工业出版社, 。
developerWorks: 登录
标有星(*)号的字段是必填字段。
保持登录。
单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件。
在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。
所有提交的信息确保安全。
选择您的昵称
当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。昵称长度在 3 至 31 个字符之间。
您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。
标有星(*)号的字段是必填字段。
(昵称长度在 3 至 31 个字符之间)
单击提交则表示您同意developerWorks 的条款和条件。 .
所有提交的信息确保安全。
文章、教程、演示,帮助您构建、部署和管理云应用。
立即加入来自 IBM 的专业 IT 社交网络。
免费下载、试用软件产品,构建应用并提升技能。
static.content.url=/developerworks/js/artrating/SITE_ID=10Zone=LinuxArticleID=442490ArticleTitle=Linux 下定时器的实现方式分析publish-date=}

我要回帖

更多关于 linux 定时器实现 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信