侧边栏壁纸
博主头像
如此肤浅博主等级

但行好事,莫问前程!

  • 累计撰写 24 篇文章
  • 累计创建 12 个标签
  • 累计收到 6 条评论

目 录CONTENT

文章目录

定时器

如此肤浅
2022-10-17 / 0 评论 / 0 点赞 / 580 阅读 / 2,947 字 / 正在检测是否收录...
温馨提示:
本文最后更新于 2023-03-08,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

网络程序需要处理定时事件,比如定期检测一个客户连接的活动状态。服务器程序通常管理着众多定时事件,因此有效地组织这些定时事件,使之能在预期的时间点被触发且不影响服务器的主要逻辑,对于服务器的性能有着至关重要的影响。为此,我们要将每个定时事件分别封装成定时器,并使用某种容器类数据结构,比如链表、排序链表和时间轮,将所有定时器串联起来,以实现对定时事件的统一管理。

定时是指在一段时间之后触发某段代码的机制,我们可以在这段代码中依次处理所有到期的定时器。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
}
0

评论区