前言
一般在游戏开发或者网络程序中会使用到定时器,当然最先接触定时器的还是JS中setTimeout和setInterval。同理其他语言中有时也需要使用到定时器。而在Linux中提供了三种定时方法,它们是:
- socket选项SO_RCVTIMEO和SO_SNDTIMEO。
- SIGALRM信号。
- I/O复用系统调用的超时参数。
定时方法
socket选项SO_RCVTIMEO和SO_SNDTIMEO
SO_RCVTIMEO和SO_SNDTIMEO是分别用来设置socket接收数据超时时间和发送数据超时时间。因此,这两个选项仅对与数据接收和发送相关的socket专用系统调用有效,这些系统调用包括send、sendmsg、recv、recvmsg、accept和connect。
系统调用 | 有效选项 | 系统调用超时后的行为 |
---|---|---|
send | SO_SNDTIMEO | 返回-1,设置errno为EAGAIN或EWOULDBLOCK |
sendmsg | SO_SNDTIMEO | 返回-1,设置errno为EAGAIN或EWOULDBLOCK |
recv | SO_RCVTIMEO | 返回-1,设置errno为EAGAIN或EWOULDBLOCK |
recvmsg | SO_RCVTIMEO | 返回-1,设置errno为EAGAIN或EWOULDBLOCK |
accept | SO_RCVTIMEO | 返回-1,设置errno为EAGAIN或EWOULDBLOCK |
connect | SO_SNDTIMEO | 返回-1,设置errno为EINPROGRESS |
在程序中,我们可以根据系统调用(send、sendmsg、recv、recvmsg、accept和connect)的返回值以及errno来判断超时时间是否已到,进而决定是否开始处理定时任务。
SIGALRM信号
由alarm和 setitimer函数设置的实时闹钟一旦超时,将触发SIGALRM信号。因此,我们可以利用该信号的信号处理函数来处理定时任务。但是,如果要处理多个定时任务,我们就需要不断地触发SIGALRM信号,并在其信号处理函数中执行到期的任务。
I/O复用系统调用的超时参数
Linux下的3组IO复用系统调用都带有超时参数,因此它们不仅能统一处理信号和IO事件,也能统一处理定时事件。但是由于IO复用系统调用可能在超时时间到期之前就返回(有IO事件发生),所以如果我们要利用它们来定时,就需要不断更新定时参数以反映剩余的时间。
#define TIMEOUT 5000
int timeout = TIMEOUT;
time_t start = time(NULL);
time_t end = time(NULL);
while(1)
{
printf("the timeout is now %d mil-seconds\n",timeout);
start = time(NULL);
int number = epoll_wait(epollfd,evenets,MAX_EVENT_NUMBER,timeout);
if((number < 0) && (errno != EINTR))
{
printf("epoll failure\n");
break;
}
//如果epoll_wait成功返回0,则说明超时时间到,此时便可处理定时任务,并重置定时时间
if(number == 0)
{
timeout = TIMEOUT;
continue;
}
end = time(NULL);
//如果epoll_wait的返回值大于0,则本次epoll_wait调用持续的时间是(end-start)*1000ms,我们需要将定时时间timeout减去这段时间,以获得下次epoll_wait调用的超时参数
timeout -= (end-start)*1000;
//重新计算之后的timeout值有可能等于0,说明本次epoll_wait调用返回时,不仅有文件描述符就绪,而且其超时时间也刚好到达,此时我们也要处理定时任务,并重置定时时间
if(timeout <= 0)
{
timeout = TIMEOUT;
}
}
高性能定时器
时间轮
如下图所示的时间轮部内,(实线)指针指向轮子上的一个槽(slot)。它以恒定的速度顺时针转动,每转动一步就指向下一个槽(虚线指针指向的槽),每次转动称为一个滴答(tick)
。一个滴答的时间称为时间轮的槽间隔si(slot interval),它实际上就是心搏时间。该时间轮共有N个槽,因此它运转一周的时间是Nsi。每个槽指向一条定时器链表,每条链表上的定时器具有相同的特征:它们的定时时间相差Nsi的整数倍。时间轮正是利用这个关系将定时器散列到不同的链表中。
基于排序链表的定时器使用唯一的一条链表来管理所有定时器,所以插入操作的效率随着定时器数目的增多而降低。而时间轮使用哈希表的思想,将定时器散列到不同的链表上。这样每条链表上的定时器数目都将明显少于原来的排序链表上的定时器数目,插入操作的效率基本不受定时器数目的影响。
对于时间轮而言,要提高定时精度,就要使槽间隔si足够小;要提高执行效率,则要求槽的个数N足够大。
时间堆
前面讨论的定时方案都是以固定的频率调用心搏函数tick,并在其中依次检测到期的定时器,然后执行到期定时器上的回调函数。
设计定时器的另外一种思路是:将所有定时器中的超时时间最小的一个定时器的超时值作为心搏间隔。
这样,一旦心搏函数tick被调用,超时时间最小的定时器必然到期,我们就可以在tick函数中处理该定时器。然后,再次从剩余的定时器中找出超时时间最小的一个,并将这段最小时间设置为下一次心搏间隔。如此反复,就实现了较为精确的定时。
资料
《Linux高性能服务器编程》