当前位置: 首页 > news >正文

muduo源码阅读:linux timefd定时器

⭐timerfd

timerfd 是Linux一个定时器接口,它基于文件描述符工作,并通过该文件描述符的可读事件进行超时通知。可以方便地与select、poll和epoll等I/O多路复用机制集成,从而在没有处理事件时阻塞程序执行,实现高效的零轮询编程模型。

🟠timerfd_create

创建一个新的定时器对象,并返回一个与其关联的文件描述符。

#include <sys/timerfd.h>
int timerfd_create(int clockid,int flags);

clockid:定时器所依据的时间基准。

CLOCK_REALTIME/CLOCK_MONOTONIC(含义见下文)。

flags:控制定时器文件描述符的行为,可以是0或多个以下标志通过位或(|)组合而成:

TFD_NONBLOCK: 设置为非阻塞模式,使得读取操作立即返回而不是等待直到有数据可读。

TFD_CLOEXEC: 设置执行新程序时自动关闭文件描述符的标志,这可以防止子进程中继承不必要的文件描述符(子进程不继承父进程的定时器文件描述符)。

系统实时时间 (CLOCK_REALTIME)

系统实时时间指的是从一个固定的时间点(通常是1970年1月1日UTC,也称为Unix纪元)到现在的总时间。这个时间是可以通过系统设置或网络时间协议(NTP)进行调整。

使用 CLOCK_REALTIME 获取的时间可以被操作系统或其他软件手动更改,例如当系统管理员手动调整系统时钟或自动同步时间时。如果应用程序依赖于 CLOCK_REALTIME 来计算事件之间的时间差,那么这些计算可能会因为系统时间的突然跳跃变得不准确。

单调递增的时间 (CLOCK_MONOTONIC)

单调递增的时间通常是从系统启动时开始计数,并且会持续增加直到系统关闭。与CLOCK_REALTIME 不同的是,CLOCK_MONOTONIC 不受系统时间的手动调整或自动同步的影响。

使用 CLOCK_MONOTONIC 可以确保获得的时间值总是向前移动,不会出现向后跳跃的情况。因此,它非常适合用来测量时间段。

🟠timerfd_settime

启动或停止由timerfd_create创建的定时器,并可以设置其初始时间和间隔时间。

#include <sys/timerfd.h>
int timerfd_settime(int ufd, int flags, 
                    const struct itimerspec *new_value, 
                    struct itimerspec *old_value);

ufd: 由timerfd_create返回的文件描述符。

flags: 设置为0表示相对定时器,即从当前时间开始计时;设置为TFD_TIMER_ABSTIME则表示绝对定时器,即按照指定的时间点来触发。

new_value:指向包含初始到期时间和后续间隔时间的结构体指针。

old_value: 如果不为NULL,则指向一个用于接收旧的定时器值的结构体。

返回值:成功时返回0;失败时返回-1并设置相应的错误号。

struct timespec{
       time_t tv_sec;                /* Seconds */
       long   tv_nsec;               /* Nanoseconds */
};
struct itimerspec {
       struct timespec it_interval;  /* Interval for periodic timer */
       struct timespec it_value;     /* Initial expiration */
};

it_value是首次超时时间,需要填写从clock_gettime获取的时间,并加上要超时的时间。 it_interval是后续周期性超时时间,是多少时间就填写多少。注意一个容易犯错的地方:tv_nsec加上去后一定要判断是否超出1000000000(如果超过要秒加一),否则会设置失败。

🟠clock_gettime
#include <time.h>
int clock_gettime(clockid_t clk_id, struct timespec *tp);

clockid_t clk_id 是时钟 ID,常用的选项包括 CLOCK_REALTIME 和 CLOCK_MONOTONIC。

CLOCK_REALTIME 提供的是系统实时时间,可能会因为系统时间调整而发生跳跃。
CLOCK_MONOTONIC 提供单调递增的时间,适合用于测量时间间隔。
struct timespec *tp 是一个指向 timespec 结构体的指针,用于存储获取到的时间信息。

第三个参数设置超时时间,如果为0则表示停止定时器。定时器设置超时方法:

设置超时时间是需要调用clock_gettime获取当前时间,如果是绝对定时器,那么需要获取CLOCK_REALTIME,在加上要超时的时间。如果是相对定时器,要获取CLOCK_MONOTONIC时间。

定时器代码实例:

#define _GNU_SOURCE
#include<sys/timerfd.h>
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<time.h>
void print_itimerspec(struct itimerspec *new_value) {
    printf("Initial expiration: sec: %ld nsec: %ld\n", new_value->it_value.tv_sec, new_value->it_value.tv_nsec);
    printf("Interval: sec: %ld nsec: %ld\n", new_value->it_value.it_interval.tv_sec, new_value->it_value.it_interval.tv_nsec);
}
int main() {
    struct itimerspec new_value;
    int tfd;
    //创建一个新的定时器对象
    tfd = timerfd_create(CLOCK_MONOTONIC, 0);
    if (tfd == -1) {
        perror("timerfd_create");
        exit(EXIT_FAILURE);
    }
    //设置定时器参数
    //首次超时时间为3秒后
    new_value.it_value.tv_sec = 3;
    new_value.it_value.tv_nsec = 0;
    // 后续每隔2秒触发一次
    new_value.it_interval.tv_sec = 2;
    new_value.it_interval.tv_nsec = 0;
    print_itimerspec(&new_value);
    // 启动定时器
    if (timerfd_settime(tfd, 0, &new_value, NULL) == -1) {
        perror("timerfd_settime");
        close(tfd);
        exit(EXIT_FAILURE);
    }
    // 循环读取定时器事件
    uint64_t exp;
    ssize_t s;
    while((s = read(tfd, &exp, sizeof(uint64_t))) != sizeof(uint64_t)) {
        if (s != -1) {
            fprintf(stderr, "Error reading timerfd\n");
            break;
        }
        if (errno == EINTR)
            continue;
        perror("read");
        break;
    }
    printf("Timer expired %llu times\n", exp);
    close(tfd);
    return 0;
}

read函数可以读timerfd,读的内容为uint_64,表示超时次数。

❓补充:什么是零轮询编程模型?

零轮询编程模型是一种高效处理I/O操作的方法,旨在避免传统轮询(polling)带来的CPU资源浪费。

传统的轮询会周期性地检查I/O设备是否准备好进行数据传输,可能导致大量的CPU时间被消耗在无意义的检查上。

相比之下,零轮询编程模型利用了操作系统提供的机制(select/poll/epoll等),允许程序在等待I/O事件时进入阻塞状态,即不占用CPU资源,直到有实际的I/O事件发生才会唤醒程序进行处理。这种模型通过减少或消除不必要的检查循环。

❓补充:timerfd、eventfd、signalfd分别有什么用?

timerfd、eventfd、signalfd配合epoll使用的场景,共同工作以实现一个不需要主动轮询的环境。

timerfd 提供了一个基于文件描述符的定时器接口,可以通过文件描述符的可读事件来通知超时。

eventfd 是一种用于进程间或线程间事件通知的机制,它提供了一个文件描述符,可以用来执行简单的事件计数。

signalfd 允许信号的接收通过文件描述符进行,这样就可以将信号处理集成到文件描述符的多路复用中。

epoll 则是一个I/O多路复用的接口,能够监控大量文件描述符的集合,当某个文件描述符准备好进行I/O操作时,就返回通知给应用程序。

补充:把定时器文件描述符设置为非阻塞模式和阻塞模式有什么区别,举例说明?和select/poll/epoll集成时,应该设置为阻塞还是非阻塞?为什么?

(1)非阻塞模式与阻塞模式的区别

非阻塞模式(通过设置 TFD_NONBLOCK 标志):当尝试从一个非阻塞的定时器文件描述符读取数据时,如果当前没有定时器到期事件可供读取,read 调用会立即返回。程序可以在不等待I/O操作完成的情况下继续执行其他任务。

阻塞模式:在默认情况下(即未设置 TFD_NONBLOCK),对定时器文件描述符进行读操作时,如果当前没有定时器到期事件可供读取,调用线程会被挂起,直到有数据可读为止。这允许程序在等待I/O操作完成期间节省CPU资源,但同时也会导致线程暂时不可用于处理其他任务。

和 select/poll/epoll 集成时的选择

在使用 select、poll 或 epoll 等机制管理多个文件描述符时,推荐将定时器文件描述符设置为 非阻塞模式。

因为这些机制本身已经提供了等待I/O就绪的功能。当将文件描述符设置为非阻塞模式时,可以避免在轮询中出现不必要的阻塞。例如使用 epoll 监控定时器文件描述符,当定时器到期时,epoll_wait 返回,由于定时器文件描述符处于非阻塞模式,可以立即尝试读取而不担心阻塞问题,然后根据需要执行相应的处理逻辑。这样确保应用能够高效地响应各种I/O事件,不会因为某个特定的操作被阻塞而导致整体性能下降(具体解释看补充问题)

❓补充:如果定时器文件描述符设置为阻塞模式会发生什么情况?

当定时器文件描述符使用阻塞模式,并使用epoll监听时,可能会导致应用程序在处理定时器事件时被阻塞,进而影响整体性能,使其他I/O事件无法及时得到处理。

#include <stdio.h> 
#include <stdlib.h> 
#include <sys/epoll.h> 
#include <time.h> 
#include <unistd.h> 
#include <fcntl.h> 
#define MAX_EVENTS 10 
int main() { 
    int epoll_fd = epoll_create1(0); 
    if (epoll_fd == -1) { 
        perror("epoll_create1"); 
        return 1; 
    } 
    // 创建定时器文件描述符 
    int timer_fd = timerfd_create(CLOCK_MONOTONIC, 0); 
    if (timer_fd == -1) { 
        perror("timerfd_create"); 
        return 1; 
    } 
    // 设置定时器 
    struct itimerspec new_value; 
    new_value.it_interval.tv_sec  = 5; 
    new_value.it_interval.tv_nsec  = 0; 
    new_value.it_value.tv_sec  = 5; 
    new_value.it_value.tv_nsec  = 0; 
    if (timerfd_settime(timer_fd, 0, &new_value, NULL) == -1) { 
        perror("timerfd_settime"); 
        return 1; 
    } 
    // 将定时器文件描述符添加到epoll实例中 
    struct epoll_event ev, events[MAX_EVENTS]; 
    ev.events  = EPOLLIN; 
    ev.data.fd  = timer_fd; 
    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, timer_fd, &ev) == -1) { 
        perror("epoll_ctl: timer_fd"); 
        return 1; 
    } 
    while (1) { 
        int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1); 
        if (nfds == -1) { 
            perror("epoll_wait"); 
            return 1; 
        } 
        for (int i = 0; i < nfds; i++) { 
            if (events[i].data.fd  == timer_fd) { 
                // 由于定时器文件描述符是阻塞模式,这里可能会阻塞 
                uint64_t expirations; 
                ssize_t s = read(timer_fd, &expirations, sizeof(uint64_t)); 
                if (s!= sizeof(uint64_t)) { 
                    perror("read"); 
                    return 1; 
                } 
                printf("Timer expired %lu times\n", expirations); 
            } 
        } 
    } 
   close(timer_fd); 
   close(epoll_fd); 
   return 0; 
} 

阻塞模式下,当对定时器文件描述符执行readwrite等操作时,如果操作不能立即完成,进程会进入睡眠状态,等待操作条件满足。这就导致应用程序在这个操作上被阻塞,无法继续执行后续代码,包括处理其他I/O事件。

在Linux内核中,每个文件描述符都有一个对应的文件对象,文件对象中包含了与该文件描述符相关的操作函数集合。对于定时器文件描述符,当执行read操作时,内核会检查定时器的状态和相关的缓冲区。如果缓冲区没有数据,内核会将当前进程加入到等待队列中,并将进程状态设置为睡眠状态,直到定时器到期并产生数据,或者发生其他可以满足read操作的条件。这种机制是为了确保read操作能够正确完成,但在多I/O事件处理的场景下,会导致其他 I/O 事件延迟处理:主线程或事件循环被挂起,网络套接字、文件操作等事件无法及时响应

❓上一个问题的补充:为什么要使用read读取定时器的内核缓冲区?为什么数据会存在定时器的内核缓冲区?

定时器文件描述符为何需要 read 操作?

内核缓冲区的数据来源

定时器文件描述符(如 Linux 的 timerfd)通过 timerfd_create 创建时,内核会为其维护一个计数器缓冲区。当定时器到期时,内核会向该缓冲区写入一个 8 字节的无符号整数,表示自上次读取后定时器触发的次数。(这就是定时器可读事件的本质)。

uint64_t expirations;
read(timer_fd, &expirations, sizeof(expirations));

若不读取,缓冲区会持续累积到期次数,导致后续 epoll_wait误判为"持续就绪"。

为什么检测到定时器文件描述符就绪时,需要通过read来读取定时器文件描述符?

  • 清除就绪状态:读取后重置内核缓冲区,避免 epoll_wait 重复触发。
  • 获取触发次数:通过读取的整数值,可统计定时器到期次数 (适用于周期性定时器)。
  • 避免数据堆积:长期不读取可能导致缓冲区溢出或逻辑错误。
上一个问题的补充:什么时候read定时器文件描述符会阻塞?

定时器文件描述符的缓冲区设计为“有数据时触发读就绪”,因此在正常逻辑中,epoll_wait 返回定时器就绪时,缓冲区应已有数据,此时 read 操作应立刻成功。但以下情况可能导致阻塞:

假设定时器到期时,内核触发超时事件并准备向文件描述符的缓冲区写入超时次数(uint64_t 类型数据).

内核检测到定时器到期,将事件标记为就绪并唤醒 epoll_wait
在写入缓冲区的过程中(如正在更新计数器),发生线程/进程上下文切换。
用户线程从 epoll_wait 返回后,立即调用 read,但此时内核尚未完成缓冲区数据的写入。

read 操作因缓冲区无数据而阻塞(若文件描述符未设置为非阻塞模式),或返回EAGAIN(非阻塞模式)。

类比: 多线程环境下“先通知后执行”的竞态,例如生产者-消费者模型中,消费者收到通知但数据尚未生产完毕。

解决方案:设置为非阻塞模式,通过fcntl(fd, F_SETFL, O_NONBLOCK) 避免 read 阻塞。

最佳实践

  • 非阻塞读取:所有通过 epoll 监听的文件描述符均设置为非阻塞模式。
  • 事件处理原子化在单次 epoll_wait 返回后,批量处理所有就绪事件,避免穿插阻塞调用。

定时器文件描述符的阻塞模式会破坏事件驱动架构的异步性,内核缓冲区的数据读取机制是定时触发的核心逻辑。通过非阻塞模式 + 严格的数据读取,可确保系统的高效性和可靠性。理解这一机制对设计高并发服务(如 Web 服务器、实时交易系统)至关重要。

❓上一个问题的补充:如果不使用timerfd实现定时器,应该怎么实现定时器?

定时器的替代方案

若需避免 read 操作,可结合信号(如 SIGEV_THREAD用户态定时器队列(如 libevent 的定时器堆),但需权衡精度和性能。

相关文章:

  • 学习Flask:Day 1:基础搭建
  • AI大模型(四)基于Deepseek本地部署实现模型定制与调教
  • Python图像处理入门:如何打开图像文件及常见格式
  • MySQL知识
  • SpringBoot整合sharding-jdbc 实现分库分表操作
  • 实操系列:我用deepseek写sql
  • C++ | 面向对象 | 类
  • 六十天前端强化训练之第二天CSS选择器与盒模型深度解析
  • DeepSeek技术提升,Linux本地部署全攻略
  • 【面试手撕】多线程/并发编程
  • 在 compare-form.vue 中添加 compareDate 隐藏字段,并在提交时自动填入当前时间
  • [OS] 基于RR(Round Robin)算法的CPU调度
  • ubuntu20.04 突破文件数限制
  • 前言:什么是大模型微调
  • 企业级大模型应用的Java-Python异构融合架构实践
  • 本地部署大模型: LM Studio、Open WebUI 与 Chatbox 全面对比以及选型指南
  • 在windows下安装windows+Ubuntu16.04双系统(上)
  • 公共数据授权运营模式研究(总体框架、主要模式及发展趋势)
  • linux下软件安装、查找、卸载
  • QT入门--QMainWindow
  • 六连板成飞集成:航空零部件业务收入占比为1.74%,市场环境没有重大调整
  • 金正恩观摩朝鲜人民军各兵种战术综合训练
  • 演员黄晓明、金世佳进入上海戏剧学院2025年博士研究生复试名单
  • 沙县小吃中东首店在沙特首都利雅得开业,首天营业额5万元
  • 吉林:消纳绿电,“氢”装上阵
  • 哈佛新论文揭示 Transformer 模型与人脑“同步纠结”全过程!AI也会犹豫、反悔?