关于定时器

定时器概述

通过定时器可以实现给服务器注册定时事件,这是服务器上经常要处理的一类事件,比如3秒后关闭一个连接,或是定期检测一个客户端的连接状态。

定时事件依赖于Linux提供的定时机制,它是驱动定时事件的原动力,目前Linux提供了以下几种可供程序利用的定时机制:

1. alarm()或setitimer(),这俩的本质都是先设置一个超时时间,然后等SIGALARM信号触发,通过捕获信号来判断超时

2. 套接字超时选项,对应SO_RECVTIMEO和SO_SNDTIMEO,通过errno来判断超时

3. 多路复用超时参数,select/poll/epoll都支持设置超时参数,通过判断返回值为0来判断超时

4. timer_create系统接口,实质也是借助信号,参考man 2 timer_create

5. timerfd_create系列接口,通过判断文件描述符可读来判断超时,可配合IO多路复用,参考man 2 timerfd_create

服务器程序通常需要处理众多定时事件,如何有效地组织与管理这些定时事件对服务器的性能至关重要。为此,我们要将每个定时事件分别封装成定时器,并使用某种容器类数据结构,比如链表、排序链表和时间轮,将所有定时器串联起来,以实现对定时事件的统一管理。

每个定时器通常至少包含两个成员:一个超时时间(相对时间或绝对时间)和一个任务回调函数。除此外,定时器还可以包括回调函数参数及是否自动重启等信息。

有两种高效管理定时器的容器:时间轮和时间堆

几种定时器实现

基于升序链表的定时器

1. 所有定时器组织成链表结构,链表成员包含超时时间,回调函数,回调函数参数,以及链表指针域。

2. 定时器在链表中按超时时间进行升序排列,超时时间短的在前,长的在后。每次添加定时器时,都要按超时时间将定时器插入到链表的指定位置。

3. 程序运行后维护一个周期性触发的tick信号,比如利用alarm函数周期性触发ALARM信号,在信号处理函数中从头遍历定时器链表,判断定时器是否超时。如果定时器超时,则记录下该定时器,然后将其从链表中删除。

4. 执行所有超时的定时器的回调函数。

以上就是一个基于升序链表的定时器实现,这种方式添加定时器的时间复杂度是O(n),删除定时器的时间复杂度是O(1),执行定时任务的时间复杂度是O(1)。

tick信号的周期对定时器的性能有较大的影响,当tick信号周期较小时,定时器精度高,但CPU负担较高,因为要频繁执行信号处理函数;当tick信号周期较大时,CPU负担小,但定时精度差。

当定时器数量较多时,链表插入操作开销比较大。

时间轮

与上面的升序链表实现方式类似,也需要维护一个周期性触发的tick信号,但不同的是,定时器不再组织成单链表结构,而是按照超时时间,通过散列分布到不同的时间轮上,像下面这样:

上面的时间轮包含N个槽位,每个槽位上都有一个定时器链表。时间轮以恒定的速度顺时针转动,每转一步,表盘上的指针就指向下一个槽位。每次转动对应一个tick,它的周期为si,一个共有N个槽,所以它运转一周的时间是N*si。

每个槽位都有一条定时器链表,同一条链表上的每个定时器都具有相同的特征:前后节点的定时时间相差N*si的整数倍。时间轮正是利用这个关系将定时器散列到不同的链表上。假如现在指针指向槽cs,我们要添加一个定时时间为ti的定时器,则该定时器将被插入槽ts(time slot)对应的链表中:

ts = (cs + (ti / si)) % N

时间轮通过哈希表的思想,将定时器散列到不同的链表上,每个链表的定时器数目都明显少于原来的排序链表,插入效率基本不受定时器数目的影响。

和升序链表一样,tick的周期将影响定时器精度和CPU负载,除此外,时间轮上的槽数量N还对定时器的效率有影响,N越大,则散列越均匀,插入效率越高,N越小,则散列越容易冲突,至N等于1时,时间轮将完全退化成升序链表。

上面的时间轮只有一个轮子,而复杂的时间轮可能有多个轮子,不同的轮子拥有不同的粒度。相邻的两个轮子,精度高的转一圈,精度低的仅往前移动一槽,就像水表一样。

注意点:

单个槽上的定时器链表仍然是按升序链表来组织的,只不过前后两个节点的时间差一定是N*si的整数倍。注意这里前后节点的时间差不一定是1个N*si,也有可能是好几个N*si,所以不能通过定时器所在的槽位和链表位置直接推算出定时器的超时时间。或者换个说法,表盘指针转到某个槽时,仍需要按升序链表的方式遍历这个链表的节点,并判断是否超时。

时间堆

上面的两种定时器设计都依赖一个固定周期触发的tick信号。设计定时器的另一种实现思路是直接将超时时间当作tick周期,具体操作是每次都取出所有定时器中超时时间最小的超时值作为一个tick,这样,一旦tick触发,超时时间最小的定时器必然到期。处理完已超时的定时器后,再从剩余的定时器中找出超时时间最小的一个,并将这个最小时间作为下一个tick,如此反复,就可以实现较为精确的定时。

最小堆很适合处理这种定时方案,将所有定时器按最小堆来组织,可以很方便地获取到当前的最小超时时间

以下是一个使用C语言的POSIX定时器的示例代码,它不依赖于任何GUI库。 ```c #include <stdio.h> #include <stdlib.h> #include <signal.h> #include <time.h> #define TIMER_INTERVAL 1 // 定时器间隔(秒) void timer_handler(int signum) { printf("Timer expired.\n"); } int main() { struct sigevent sev; struct itimerspec its; timer_t timerid; // 创建定时器事件 sev.sigev_notify = SIGEV_SIGNAL; sev.sigev_signo = SIGUSR1; sev.sigev_value.sival_ptr = &timerid; if (timer_create(CLOCK_REALTIME, &sev, &timerid) == -1) { perror("timer_create"); exit(EXIT_FAILURE); } // 设置定时器间隔 its.it_value.tv_sec = TIMER_INTERVAL; its.it_value.tv_nsec = 0; its.it_interval.tv_sec = TIMER_INTERVAL; its.it_interval.tv_nsec = 0; if (timer_settime(timerid, 0, &its, NULL) == -1) { perror("timer_settime"); exit(EXIT_FAILURE); } // 注册定时器信号处理函数 struct sigaction sa; sa.sa_flags = 0; sa.sa_handler = timer_handler; sigemptyset(&sa.sa_mask); if (sigaction(SIGUSR1, &sa, NULL) == -1) { perror("sigaction"); exit(EXIT_FAILURE); } while(1) { // 等待定时器信号 pause(); } return 0; } ``` 在上面的代码中,我们使用`timer_create`函数创建一个定时器。接着,我们使用`timer_settime`函数设置定时器的间隔。然后,我们注册了一个信号处理函数,用于处理定时器信号。最后,我们进入无限循环,等待定时器信号的到来。 当定时器信号到来时,定时器的信号处理函数将被调用。在本例中,我们只是简单地打印一条消息。 注意,我们使用了`pause`函数来等待定时器信号。`pause`函数会挂起进程,直到接收到一个信号。在本例中,我们将进程挂起,直到接收到定时器信号。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值