信号与定时器

本文是《Linux 高性能服务器编程》阅读记录,供以后查阅参考。推荐阅读原书。

所有函数未标明需要包含什么头文件,可使用 man 命令自行查询。

1. 信号

信号是由用户、系统或者进程发送给目标进程的信息,以通知目标进程某个状态的改变或系统异常。服务器程序必须处理(或至少忽略)一些常见的信号,以免异常终止。

1.1 Linux 信号相关 API

1.1.1 发送信号

使用 kill 函数发送信号给某个进程,函数接口如下:

1
2
3
4
/* Send signal SIG to process number PID.  If PID is zero,
send SIG to all processes in the current process's process group.
If PID is < -1, send SIG to all processes in process group - PID. */
extern int kill (__pid_t __pid, int __sig);

pid 的取值会影响 kill 函数的行为,具体如下:

sig 表示信号。Linux 定义的信号值都大于 0,如果 sig 取值为 0,则 kill 函数不发送任何信号。

1.1.2 信号处理方式

目标进程在收到信号时,需要定义一个接收函数来处理之。信号处理函数的声明如下:

1
2
/* Type of a signal handler.  */
typedef void (*__sighandler_t) (int);

信号处理函数参数为 int 类型,指示信号类型。信号处理函数必须是可重入的,不能调用一些不安全的函数。

用户可以自定义信号处理函数,也可以使用系统提供的一些可选的默认处理方式:

1
2
#define	SIG_DFL	 ((__sighandler_t)  0)	/* Default action.  */
#define SIG_IGN ((__sighandler_t) 1) /* Ignore signal. */

默认处理方式包括::结束进程(Term)、忽略信号(Ign)、结束进程并生成核心转储文件(Core)、暂停进程(Stop),以及继续进程(Cont)。

1.1.3 Linux 信号

Linux 系统中定义的信号可使用 kill -l 命令查看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX

其中,标准信号含义及系统默认处理行为如下:

我们并不需要在代码中处理所有这些信号。其中与网络编程关系紧密的信号有:SIGHUPSIGPIPESIGURG。以及定时器相关的信号:SIGALRMSIGCHLD 等。

1.2 信号函数

1.2.1 signal

signal 函数用于为一个信号设置处理函数:

1
2
3
4
/* Set the handler for the signal SIG to HANDLER, returning the old
handler, or SIG_ERR on error.
By default `signal' has the BSD semantic. */
extern __sighandler_t signal (int __sig, __sighandler_t __handler);

1.2.2 sigaction

sigaction 是设置信号处理函数的更健壮的接口:

1
2
3
/* Get and/or set the action for signal SIG.  */
extern int sigaction (int __sig, const struct sigaction *__restrict __act,
struct sigaction *__restrict __oact);
  • sig:指定要捕获的信号类型
  • act:指定新的信号处理方式
  • oact:用于输出信号之前的处理方式

sigaction 结构体定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/* Structure describing the action to be taken when a signal arrives.  */
struct sigaction
{
/* Signal handler. */
#if defined __USE_POSIX199309 || defined __USE_XOPEN_EXTENDED
union
{
/* Used if SA_SIGINFO is not set. */
__sighandler_t sa_handler;
/* Used if SA_SIGINFO is set. */
void (*sa_sigaction) (int, siginfo_t *, void *);
}
__sigaction_handler;
# define sa_handler __sigaction_handler.sa_handler
# define sa_sigaction __sigaction_handler.sa_sigaction
#else
__sighandler_t sa_handler;
#endif

/* Additional set of signals to be blocked. */
__sigset_t sa_mask;

/* Special flags. */
int sa_flags;

/* Restore handler. */
void (*sa_restorer) (void);
};
  • sa_hander:指定信号处理函数。

  • sa_mask:设置进程的信号掩码(确切地说是在进程原有信号掩码的基础上增加信号掩码),以指定哪些信号不能发送给本进程。

  • sa_flags:用于设置程序收到信号时的行为,可选值如下:

1.3 信号集

1.3.1 信号集函数

Linux 使用数据结构 sigset_t 来表示一组信号,定义如下:

1
2
3
4
5
#define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))
typedef struct
{
unsigned long int __val[_SIGSET_NWORDS];
} __sigset_t;

sigset_t 实际上是一个长整型数组,数组的每个元素的每个位表示一个信号。这种定义方式和文件描述符集 fd_set 类似。Linux 提供了如下一组函数来设置、修改、删除和查询信号集:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* Clear all signals from SET.  */
extern int sigemptyset (sigset_t *__set) __THROW __nonnull ((1));

/* Set all signals in SET. */
extern int sigfillset (sigset_t *__set) __THROW __nonnull ((1));

/* Add SIGNO to SET. */
extern int sigaddset (sigset_t *__set, int __signo) __THROW __nonnull ((1));

/* Remove SIGNO from SET. */
extern int sigdelset (sigset_t *__set, int __signo) __THROW __nonnull ((1));

/* Return 1 if SIGNO is in SET, 0 if not. */
extern int sigismember (const sigset_t *__set, int __signo)
__THROW __nonnull ((1));

1.3.2 进程信号掩码

除了 sigaction 结构体的 sa_mask 成员可以用来设置进程的信号掩码,sigprocmask 函数也可用于设置或查看进程的信号掩码:

1
2
3
/* Get and/or change the set of blocked signals.  */
extern int sigprocmask (int __how, const sigset_t *__restrict __set,
sigset_t *__restrict __oset) __THROW;
  • set:指定新的信号掩码。

  • oset:若不为 NULL,用于输出原来的信号掩码。

  • set:若不为 NULL,指定设置进程信号掩码的方式,可选值如下表所示:

setNULL,信号掩码不变,oset 用于获取进程当前的信号掩码。

1.3.3 被挂起的信号

设置进程信号掩码后,被屏蔽的信号将不能被进程接收。如果给进程发送一个被屏蔽的信号,则操作系统将该信号设置为进程的一个被挂起的信号。如果我们取消对被挂起信号的屏蔽,则它能立即被进程接收到。如下函数可以获得进程当前被挂起的信号集:

1
2
/* Put in SET all signals that are blocked and waiting to be delivered.  */
extern int sigpending (sigset_t *__set) __THROW __nonnull ((1));

在多进程、多线程环境中,我们要以进程、线程为单位来处理信号和信号掩码。我们不能设想新创建的进程、线程具有和父进程、主线程完全相同的信号特征。比如,fork 调用产生的子进程将继承父进程的信号掩码,但具有一个空的挂起信号集。

1.4 统一事件源

信号是一种异步事件:信号处理函数和程序的主循环是两条不同的执行路线。信号处理函数需要尽可能快地执行完毕,以确保该信号不被屏蔽(前面提到过,为了避免一些竞态条件,信号在处理期间,系统不会再次触发它)太久。

一种典型的解决方案是:把信号的主要处理逻辑放到程序的主循环中,当信号处理函数被触发时,它只是简单地通知主循环程序接收到信号,并把信号值传递给主循环,主循环再根据接收到的信号值执行目标信号对应的逻辑代码。信号处理函数通常使用管道来将信号“传递”给主循环:信号处理函数往管道的写端写入信号值,主循环则从管道的读端读出该信号值。那么主循环怎么知道管道上何时有数据可读呢?这很简单,我们只需要使用 I/O 复用系统调用来监听管道的读端文件描述符上的可读事件。如此一来,信号事件就能和其他 I/O 事件一样被处理,即统一事件源。核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#define MAX_EVENT_NUMBER 1024
static int pipefd[2]; // 管道

// 信号处理函数
void sig_handler(int sig) {
int save_errno = errno;
int msg = sig;
send(pipefd[1], (char *)&msg, 1, 0); // 向管道写入信号
errno = save_errno;
}

// 主函数关键逻辑
int main(int argc, char *argv[]) {
// 省略部分代码 ......
epoll_event events[MAX_EVENT_NUMBER];
int epollfd = epoll_create(5);
assert(epollfd != -1);
addfd(epollfd, listenfd);

ret = socketpair(PF_UNIX, SOCK_STREAM, 0, pipefd);
assert(ret != -1);
setnonblocking(pipefd[1]);
addfd(epollfd, pipefd[0]); // 监听管道文件描述符

// 注册感兴趣信号的信号处理函数
addsig(SIGHUP);
addsig(SIGCHLD);
addsig(SIGTERM);
addsig(SIGINT);

bool stop_server = false;

while (!stop_server) {
int number = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
// 省略部分代码 ......

for (int i = 0; i < number; i++) {
int sockfd = events[i].data.fd;
if (sockfd == listenfd) {
// 省略部分代码 ......
} else if ((sockfd == pipefd[0]) && (events[i].events & EPOLLIN)) {
int sig;
char signals[1024];
ret = recv(pipefd[0], signals, sizeof(signals), 0);
if (ret == -1) {
continue;
} else if (ret == 0) {
continue;
} else {
// 处理信号
for (int i = 0; i < ret; ++i) {
// printf( "I caugh the signal %d\n", signals[i] );
switch (signals[i]) {
case SIGCHLD:
case SIGHUP: {
continue;
}
case SIGTERM:
case SIGINT: {
stop_server = true;
}
}
}
}
} else {
}
}
}

// 省略关闭文件描述符等操作 ......
}

1.5 网络编程相关信号

1.5.1 SIGHUP

当挂起进程的控制终端时,SIGHUP 信号将被触发。对于没有控制终端的网络后台程序而言,它们通常利用 SIGHUP 信号来强制服务器重读配置文件

1.5.2 SIGPIPE

默认情况下,往一个读端关闭的管道或 socket 连接中写数据将引发 SIGPIPE 信号。我们需要在代码中捕获并处理该信号,或者至少忽略它,因为程序接收到 SIGPIPE 信号的默认行为是结束进程,而我们绝对不希望因为错误的写操作而导致程序退出。引起 SIGPIPE 信号的写操作将设置 errnoEPIPE

1.5.3 SIGURG

在 Linux 环境下,内核通知应用程序带外数据到达主要有两种方法:一种是 I/O 复用技术,select 等系统调用在接收到带外数据时将返回,并向应用程序报告 socket 上的异常事件;另外一种方法就是使用 SIGURG 信号:

1
2
3
4
5
6
7
8
9
10
void sig_urg(int sig) {
int save_errno = errno;

char buffer[BUF_SIZE];
memset(buffer, '\0', BUF_SIZE);
int ret = recv(connfd, buffer, BUF_SIZE - 1, MSG_OOB); // 接收带外数据
printf("got %d bytes of oob data '%s'\n", ret, buffer);

errno = save_errno;
}

2. 定时器

2.1 socket 选项 SO_RCVTIMEO 和 SO_SNDTIMEO

socket 选项 so_RCVTIMEOSO_SNDTIMEO 用于设置 socket 接收数据超时时间和发送数据超时时间。这两个选项仅对 socket 相关的专用系统调用有效,具体影响如下:

使用 setsocketopt 设置 socket 定时属性示例代码如下:

1
2
3
4
5
6
7
8
9
int sockfd = socket(PF_INET, SOCK_STREAM, 0);
assert(sockfd >= 0);

struct timeval timeout;
timeout.tv_sec = time;
timeout.tv_usec = 0;
socklen_t len = sizeof(timeout);
ret = setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &timeout, len); // 设置发送数据定时时间
assert(ret != -1);

2.2 SIGALRM 信号

可以使用 alarmsetitimer 函数设置定时闹钟,超时时将触发 SIGALRM 信号,可以利用该信号的信号处理函数来处理任务。

2.2.1 基于升序链表的定时器

定时器通常至少要包含两个成员:一个超时时间(相对时间或者绝对时间)和一个任务回调函数。有的时候还可能包含回调函数被执行时需要传入的参数,以及是否重启定时器等信息。使用链表时,还需要维护指针信息。可以使用升序列表维护一些定时器,当收到 SIGALRM 信号后,执行回调函数,以执行定时任务。部分关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#define BUFFER_SIZE 64

class util_timer;

// 用户数据结构
struct client_data {
sockaddr_in address; // 客户端 socket 地址
int sockfd; // socket 文件描述符
char buf[BUFFER_SIZE]; // 读缓存
util_timer *timer; // 定时器
};

class util_timer {
public:
util_timer() : prev(nullptr), next(nullptr) {}

public:
time_t expire; // 任务的超时时间 绝对时间
void (*cb_func)(client_data *); // 任务回调函数
client_data *user_data; // 回调函数处理的客户数据,由定时器的执行者传递给回调函数
util_timer *prev;
util_timer *next;
};

// 定时器链表:升序双向链表
class sort_timer_lst {
public:
// 添加定时器
void add_timer(util_timer *timer) {

}

// 当某个定时任务发生变化时(expire 属性增加), 调整定时器在链表中位置
void adjust_timer(util_timer *timer) {

}

// 删除指定定时器
void del_timer(util_timer *timer) {

}

// SIGALRM 信号每次被触发时,就在其信号处理函数(如果使用统一时间源,则是主函数)中执行一次 tick
// 函数,以处理链表上的到期任务
void tick() {
if (!head) {
return;
}
printf("timer tick\n");
time_t cur = time(nullptr); // 获得系统当前时间
util_timer *tmp = head;

// 从头结点开始依次处理每个定时器,直到遇到一个尚未到期的定时器,这就是定时器的核心逻辑
while (tmp) {
// 因为每个定时器都使用绝对时间作为超时值,所以我们可以把定时器的超时值和系统当前时间,比较以判断定时器是否到期
if (cur < tmp->expire) {
break;
}
tmp->cb_func(tmp->user_data); // 调用回调函数
// 执行完定时器中的定时任务之后,就将它从链表中删除,并重置链表头结点
head = tmp->next;
if (head) {
head->prev = nullptr;
}
delete tmp;
tmp = head;
}
}

private:
util_timer *head;
util_timer *tail;
};

2.2.2 处理非活动连接

定时任务可用于处理非活动连接。服务器程序通常要定期处理非活动连接:给客户端发一个重连请求,或者关闭该连接,或者其他。Linux 在内核中提供了对连接是否处于活动状态的定期检查机制,我们可以通过 socket 选项 KEEPALIVE 来激活它。不过使用这种方式将使得应用程序对连接的管理变得复杂。因此,我们可以考虑在应用层实现类似于 KEEPALIVE 的机制,以管理所有长时间处于非活动状态的连接。具体来说,可以利用 alarm 函数周期性地触发 SIGALRM 信号,该信号的信号处理函数利用管道通知主循环执行定时器链表上的定时任务,处理非活动连接。部分关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
void timer_handler() {
timer_lst.tick();
// 一次 alarm 调用之和产生一次 SIGALRM 信号,因此这里要重新启动定时器
alarm(TIMESLOT); // 重新启动计时器
}

// 回调函数:删除 epoll 注册 并 关闭客户连接
void cb_func(client_data *user_data) {
epoll_ctl(epollfd, EPOLL_CTL_DEL, user_data->sockfd, 0);
assert(user_data);
close(user_data->sockfd);
printf("close fd %d\n", user_data->sockfd);
}


int main(int argc, char *argv[]) {
int listenfd = socket(PF_INET, SOCK_STREAM, 0);

ret = bind(listenfd, (struct sockaddr *)&address, sizeof(address));

ret = listen(listenfd, 5);

epoll_event events[MAX_EVENT_NUMBER];
int epollfd = epoll_create(5); // epoll
assert(epollfd != -1);
addfd(epollfd, listenfd); // 监听 listenfd

ret = socketpair(PF_UNIX, SOCK_STREAM, 0, pipefd); // 创建管道
assert(ret != -1);
setnonblocking(pipefd[1]);
addfd(epollfd, pipefd[0]); // 监听管道读取端

// add all the interesting signals here
addsig(SIGALRM);
addsig(SIGTERM); // 信号
bool stop_server = false;

client_data *users = new client_data[FD_LIMIT];
bool timeout = false;
alarm(TIMESLOT); // TIMESLOT 秒后,触发 SIGALRM 信号

while (!stop_server) {
int number = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
for (int i = 0; i < number; i++) {
int sockfd = events[i].data.fd;
if (sockfd == listenfd) { // 处理新连接
struct sockaddr_in client_address;
socklen_t client_addrlength = sizeof(client_address);
int connfd = accept(listenfd, (struct sockaddr *)&client_address, &client_addrlength);
addfd(epollfd, connfd);
users[connfd].address = client_address;
users[connfd].sockfd = connfd;
util_timer *timer = new util_timer;
timer->user_data = &users[connfd];
timer->cb_func = cb_func;
time_t cur = time(nullptr);
timer->expire = cur + 3 * TIMESLOT;
users[connfd].timer = timer;
timer_lst.add_timer(timer); // 生成定时任务并加入 sorted_list
} else if ((sockfd == pipefd[0]) && (events[i].events & EPOLLIN)) { // 处理信号
int sig;
char signals[1024];
ret = recv(pipefd[0], signals, sizeof(signals), 0);
if (ret == -1) {
// handle the error
continue;
} else if (ret == 0) {
continue;
} else {
for (int i = 0; i < ret; ++i) {
switch (signals[i]) {
case SIGALRM: { // SIGALRM
timeout = true;
break;
}
case SIGTERM: {
stop_server = true;
}
}
}
}
} else if (events[i].events & EPOLLIN) {
memset(users[sockfd].buf, '\0', BUFFER_SIZE);
ret = recv(sockfd, users[sockfd].buf, BUFFER_SIZE - 1, 0);
printf("get %d bytes of client data %s from %d\n", ret, users[sockfd].buf, sockfd);
util_timer *timer = users[sockfd].timer;
if (ret < 0) {
if (errno != EAGAIN) {
cb_func(&users[sockfd]);
if (timer) {
timer_lst.del_timer(timer);
}
}
} else if (ret == 0) {
cb_func(&users[sockfd]);
if (timer) {
timer_lst.del_timer(timer);
}
} else {
// send( sockfd, users[sockfd].buf, BUFFER_SIZE-1, 0 );
// 如果某个客户连接上有数据可读,则我们要调整该连接对应的定时器,以延迟该连接被关闭的时间
if (timer) {
time_t cur = time(nullptr);
timer->expire = cur + 3 * TIMESLOT;
printf("adjust timer once\n");
timer_lst.adjust_timer(timer);
}
}
} else {
// others
}
}

if (timeout) { // SIGALRM 信号
timer_handler();
timeout = false;
}
}
return 0;
}

2.3 I/O 复用系统调用的超时参数

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#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, events, 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)*1000
// ms,我们需要将定时时间 timeout 减去这段时间,以获得下次 epoll_wait 调用的超时参数
timeout -= (end - start) * 1000;
// 重新计算之后的 timeout 值有可能等于0,说明本次 epoll_wait
// 调用返回时,不仅有文件描述符就绪,而且其超时时间也刚好到达,此时我们也要处理定时任务,并重置定时时间
if (timeout < = 0) {
timeout = TIMEOUT;
}
// handle connections
}

2.4 高性能定时器

2.4.1 时间轮

基于排序链表的定时器存在一个问题:添加定时器的效率偏低。一种改进方案是时间轮,如下图所示:

其核心思想是:指针每隔 si (slot interval, 槽间隔)时间转动一步,顺时针移动到下一个 slot。共有 N 个 slot,则旋转一周时间为 \(N * si\) 。每个 slot 指向一个定时器链表,每条链表上的定时器的定时时间相差 \(N*si\)​ 的整数倍,时间轮利用这个关系将定时器散列到不同的链表中。

加上指针目前指向下标 \(cs\) 处,需要添加的定时器定时时间为 \(ti\),则该定时间将被插入下标 \(ts\) 位置的链表中: \[ ts = (cs + \frac{ti}{si} ) \% N \] 对时间轮而言,要提高定时精度,就要使 \(si\) 值足够小;要提高执行效率,则要求 \(N\)​ 值足够大。

上图描述的是一种简单的时间轮,因为它只有一个轮子。而复杂的时间轮可能有多个轮子,不同的轮子拥有不同的粒度。相邻的两个轮子,精度高的转一圈,精度低的仅往前移动一槽,

2.4.2 时间堆

前面两种方法以固定的频率调用心搏函数 tick,并在其中依次检测到期的定时器,然后执行到期定时器上的回调函数。设计定时器的另外一种思路是:将所有定时器中超时时间最小的一个定时器的超时值作为心搏间隔。这样,一旦心搏函数 tick 被调用,超时时间最小的定时器必然到期,我们就可以在 tick 函数中处理该定时器。然后,再次从剩余的定时器中找出超时时间最小的一个,并将这段最小时间设置为下一次心搏间隔。如此反复,就实现了较为精确的定时。可以使用最小堆实现这一方案。


信号与定时器
https://arcsin2.cloud/2024/05/08/信号与定时器/
作者
arcsin2
发布于
2024年5月8日
许可协议