网络程序需要处理定时事件,比如定期检测一个客户连接的活动状态。服务器程序通常管理着众多定时事件,因此有效地组织这些定时事件,使之能在预期的时间点被触发且不影响服务器的主要逻辑,对于服务器的性能有着至关重要的影响。为此,我们要将每个定时事件分别封装成定时器,并使用某种容器类数据结构,比如链表、排序链表和时间轮,将所有定时器串联起来,以实现对定时事件的统一管理。
定时是指在一段时间之后触发某段代码的机制,我们可以在这段代码中依次处理所有到期的定时器。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 | 同上 |
recv | SO_RCVTIMEO | 同上 |
recvmsg | SO_RCVTIMEO | 同上 |
accept | SO_RCVTIMEO | 同上 |
connect | SO_SNDTIMEO | 返回-1,设置 errno 为 EINPROGRESS |
在程序中,我们可以根据系统调用(send、sendmsg、recv、recvmsg、accept 和 connect)的返回值以及 errno 来判断超时时间是否已到,进而决定是否开始处理定时任务。
sigalrm 信号
由 alarm 和 setitimer 函数设置的实时闹钟一旦超时,将触发 SIGALRM 信号。因此,我们可以利用该信号的信号处理函数来处理定时任务。但是,如果要处理多个定时任务,我们就需要不断地触发 SIGALRM 信号,并在其信号处理函数中执行到期的任务。一般而言,SIGALRM 信号按照固定的频率生成,即由 alarm 或 setitimer 函数设置的定。
alarm
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
功能
- 设置定时器(闹钟)。在指定 seconds 后,内核会给当前进程发送 SIGALRM 信号。进程收到该信号,默认动作终止。
- 每个进程都有且只有唯一的一个定时器。
- 定时与进程状态无关!就绪、运行、阻塞、暂停、终止、僵尸……无论进程处于何种状态,alarm 都计时。
参数
- seconds:指定的时间,以秒为单位。如果为 0,定时器失效。
返回值
- 之前没有定时器:返回 0
- 之前有定时器:返回定时器剩余的秒数。
setitimer
#include <sys/time.h>
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
功能
- 设置定时器(闹钟)。可代替 alarm 函数,精度微秒。
参数
- which:指定计时方式
- ITIMER_REAL:计算自然时间(用户时间 + 内核时间 + 切换时间等);发送 SIGALRM
- ITIMER_VIRTUAL:虚拟空间计时(用户时间),只计算进程占用 cpu 的时间;发送SIGVTALRM
- ITIMER_PROF:运行时计时(用户时间 + 内核时间), 计算占用 cpu 及执行系统调用的时间;发送SIGPROF
- new_value:负责设定超时时间
- old_value:存放旧的超时时间,不使用这个参数时为 NULL
返回值
- 成功:0
- 失败:-1,设置 errno
// 定时器的结构体
// 例如:过10秒后,每隔2秒定时一次
struct itimerval {
struct timerval it_interval; // 2,闹钟触发周期
struct timerval it_value; // 10,延迟多长时间后开启定时器执行
};
// 时间的结构体
struct timeval {
long tv_sec; // 秒
long tv_usec; // 微妙
};
例:
// 过3秒后,每隔2秒定时一次
int main()
{
struct itimerval new_val;
// 设置间隔的时间
new_val.it_interval.tv_sec = 2;
new_val.it_interval.tv_usec = 0;
// 设置延迟的时间,3秒后开始第一次定时
new_val.it_value.tv_sec = 3;
new_val.it_value.tv_usec = 0;
// 非阻塞
int ret = setitimer(ITIMER_REAL, &new_val, NULL);
printf("定时器开始....\n");
if(ret == -1) {
perror("setitimer");
exit(0);
}
getchar(); // 将代码阻塞在这里,方便观察输出
return 0;
}
I/O 复用系统调用的超时参数
Linux 下的 3 组 I/O 复用系统调用都带有超时参数,因此它们不仅能统一处理信号和 I/O 事件,也能统一处理定时事件。但是由于 I/O 复用系统调用可能在超时时间到期之前就返回(有 I/O 事件发生),所以如果我们要利用它们来定时,就需要不断更新定时参数以反映剩余的时间。
#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-satrt)*1000 ms,
我们需要将定时时间timeout减去这段时间,以获得下次epoll_wait调用的超时参数
*/
timeout -= (end - start) * 1000;
/*
重新计算之后的timeout值有可能等于0,说明本次epoll_wait调用返回时,
不仅有文件描述符就绪,而且其超时时间也刚好达到,
此时我们也要处理定时任务,并重置定时时间
*/
if(timeout <= 0) {
timeout = TIMEOUT;
}
// handle connections
}
评论区