定时器


Linux 3种定时方法

  1. socket 选项 SO_RCVTIMEO 和 SO_SNDTIMEO
    分别用来设置 socket 接收数据超时时间和发送数据超时时间。
    因此,这2个选项仅对数据的接收和发送相关的 socket 专用的系统调用有效。
  2. SIGALRM 信号
    基于升序链表的定时器
    处理非活动连接
  3. IO 复用系统调用的超时参数
    Linux 下的3组 IO 系统调用都带有超时参数,因此它们不仅能统一处理信号和IO事件,也能统一
    处理定时事件。但是由于IO复用函数系统调用可能在超时时间到期之前就返回(有IO事件发生),如果
    我们要利用它们来定时,就需要不断更新定时参数以反映剩余的时间。

定时器通常至少包含2个成员:

  • ​ 一个是超时时间(相对时间或者绝对时间)
  • ​ 一个任务回调函数
  • ​ 还可能包括回调函数需要的参数
    ​ 以及是否重启定时器等信息

两种高效的管理定时器的容器:

  1. 时间轮
    固定的频率调用心博函数 tick, 并以此检查到期的定时器,然后执行定时器上的回调函数。

  2. 时间堆
    将所有定时器中超时时间最小的一个定时器的超时时间作为心博间隔。

socket 选项 SO_RCVTIMEO 和 SO_SNDTIMEO

SO_RCVTIMEO 和 SO_SNDTIMEO 对相关系统调用的影响如下表:

image-20220214172849145

SIGALRM 信号

由alarm和setitimer函数设置的实时闹钟一旦超时,将触发SIGALRM信号。因此,我们可以利用该信号的信号处理函数来处理定时任务。但是,如果要处理多个定时任务,我们就需要不断触发SIGALRM信号,并在其信号处理函数中执行到期的任务。一般而言,SIGALRM信号按照固定频率生成,即由alarm或setitimer函数设计的定时周期T保持不变。如果某个定时任务的超时时间不是T的整数倍,那么它实际被执行的时间和预期的时间将略有偏差。因此定时周期T反映了定时的精度。

IO 复用系统调用的超时参数

Linux下的3I/O复用系统调用都带有超时参数,因此他们不仅能同意处理信号和I/O事件,也能统一处理定时事件。但是由于I/O复用系统可能在超时时间到期之前就返回,所以如果我们能要利用它们来定时,就需要不断更新定时参数以反映剩余的时间

高性能定时器:

时间轮

基于排序链表的定时器有一个特定就是添加定时器的效率偏低

image-20220215152746503

上图所示的时间轮,实现指针指向轮子的一个槽。它以恒定的速度顺时转动,每转动一步就指向下一个槽,每次转动称为一个滴答。一个滴答的时间称为时间轮的槽间隔si,它时间上就是心搏时间。该时间轮共有N个槽,因此转一圈时间是N*si。每个槽指向一跳定时器链表,每条链表上的定时器具有相同的特征:他们的定时时间差JN*si的整数倍。很显然,对时间轮而言,要提高定时精度,就要使si值足够小;要提高执行效率,则要求N值足够大。

时间堆

前面讨论的定时方案都是以固定是频率调用心搏函数tick,并在其中一次检测到期的定时器,然后执行到期定时器上的回调函数。设计定时器的另一种思路是:将所有定时器中超时时间最小的一个定时器的超时值作为心搏间隔。这样,一旦心搏函数tick被调用,超时时间最小的定时器必然到期,我们就可以在tick函数中处理该定时器。然后,再次从剩余的定时器中找出超时时间最小的一个,并将这段最小时间设置为下一次心搏间隔。时间堆就是利用最小堆来是实现上述方案。最小堆是指每个节点的值都小于或等于其子节点的完全二叉树。