网络程序需要处理的第三类事件是定时事件,两种高效的管理定时器的容器:时间轮和时间堆
11.1 socket选项so_rcvtimeo和so_sndtimeo
SO_RCVTIMEO和SO_SNDTIMEO选项分别用来设置socket接收数据超时时间和发送数据超时时间。因此这两个选项仅对数据接收和发送相关的socket专用系统调用有效,这些系统调用包括send、sendmsg、recv、recvmsg、accept和connect
1 ret = setsockopt( sockfd, SOL_SOCKET, SO_SNDTIMEO, &timeout, len ); 2 assert( ret != -1 ); 3 4 ret = connect( sockfd, ( struct sockaddr* )&address, sizeof( address ) ); 5 if ( ret == -1 ) 6 { 7 if( errno == EINPROGRESS ) 8 { 9 printf( "connecting timeout " ); 10 return -1; 11 } 12 printf( "error occur when connecting to server " ); 13 return -1; 14 }
11.2 sigalrm信号
由alarm和setitimer函数设置的实时闹钟一旦超时,将触发SIGALRM信号。因此,我们可以利用该信号的信号处理函数来处理定时任务。但是,如果要处理多个定时任务,我们就需要不断触发SIGALRM信号,并在其信号处理函数中执行到期的任务。一般而言,SIGALRM信号按照固定频率生成,即由alarm或setitimer函数设计的定时周期T保持不变。如果某个定时任务的超时时间不是T的整数倍,那么它实际被执行的时间和预期的时间将略有偏差。因此定时周期T反映了定时的精度。
11.3 i/o复用系统调用的超时参数
Linux下的3组I/O复用系统调用都带有超时参数,因此他们不仅能统一处理信号和I/O事件,也能统一处理定时事件。但是由于I/O复用系统可能在超时时间到期之前就返回,所以如果我们能要利用它们来定时,就需要不断更新定时参数以反映剩余的时间
11.4 高性能定时器
11.4.1 时间轮
基于排序链表的定时器存在一个问题:添加定时器的效率偏低。下面我们要讨论的时间轮解决了这个问题,一种简单的时间轮如图所示:
图所示的时间轮,实现指针指向轮子的一个槽。它以恒定的速度顺时转动,每转动一步就指向下一个槽,每次转动称为一个滴答。一个滴答的时间称为时间轮的槽间隔si,它时间上就是心搏时间。该时间轮共有N个槽,因此转一圈时间是N*si。每个槽指向一跳定时器链表,每条链表上的定时器具有相同的特征:他们的定时时间差JN*si的整数倍。很显然,对时间轮而言,要提高定时精度,就要使si值足够小;要提高执行效率,则要求N值足够大。
11.4.2 时间堆
前面讨论的定时方案都是以固定是频率调用心搏函数tick,并在其中一次检测到期的定时器,然后执行到期定时器上的回调函数。设计定时器的另一种思路是:将所有定时器中超时时间最小的一个定时器的超时值作为心搏间隔。这样,一旦心搏函数tick被调用,超时时间最小的定时器必然到期,我们就可以在tick函数中处理该定时器。然后,再次从剩余的定时器中找出超时时间最小的一个,并将这段最小时间设置为下一次心搏间隔。时间堆就是利用最小堆来是实现上述方案