muduo源码阅读:linux timefd定时器

news/2025/2/26 8:12:03
⭐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 的定时器堆),但需权衡精度和性能。


http://www.niftyadmin.cn/n/5868361.html

相关文章

对鸿蒙 中 对象的理解

鸿蒙中的对象概述 1. 对象的基本概念 在鸿蒙开发里&#xff0c;对象是类的实例。类是一种用户自定义的数据类型&#xff0c;它定义了对象的属性&#xff08;数据&#xff09;和方法&#xff08;行为&#xff09;。当创建一个类的实例时&#xff0c;就得到了一个对象。例如&…

中国旅游行业年度报告2024

过去的一年对中国旅游业是意义非凡的一年、是中国旅游行业复苏的关键一年&#xff0c;中国旅游市场多项关键指标同比大幅增长&#xff0c;接近或超越2019年同期水平&#xff0c;中国旅游行业在复苏与繁荣的征程中又向前迈进了一大步。2024年中国国内旅游人次56.15亿&#xff0c…

IDEA集成DeepSeek,通过离线安装解决无法安装Proxy AI插件问题

文章目录 引言一、安装Proxy AI1.1 在线安装Proxy AI1.2 离线安装Proxy AI 二、Proxy AI中配置DeepSeek2.1 配置本地部署的DeepSeek&#xff08;Ollama方式&#xff09;2.2 通过第三方服务商提供的API进行配置 三、效果测试 引言 许多开发者尝试通过安装Proxy AI等插件将AI能力…

在vscode中编译运行c语言文件,配置并运行OpenMP多线程并行程序设计

1.下载安装vscode Visual Studio Code - Code Editing. Redefined 2.安装vscode扩展 打开vscode,按ctrl+shift+x,打开扩展,搜索c/c++,下载相应的扩展 3.下载MinGW-w64 MinGW-w64 提供了 GNU 编译器集合,可以编译c/c++文件 这里下载见我的资源,可直接下载 把压缩包解压…

python学智能算法(五)|差分进化算法:原理认识和极小值分析

【1】引言 前序已经学习了模拟退火算法和遗传算法&#xff0c;相关文章链接为&#xff1a; python学智能算法&#xff08;一&#xff09;|模拟退火算法&#xff1a;原理解释和最小值求解_模拟退火算法python-CSDN博客 python学智能算法&#xff08;二&#xff09;|模拟退火算…

Go语言中的信号量:原理与实践指南

Go语言中的信号量&#xff1a;原理与实践指南 引言 在并发编程中&#xff0c;控制对共享资源的访问是一个经典问题。Go语言提供了丰富的并发原语&#xff08;如sync.Mutex&#xff09;&#xff0c;但当我们需要灵活限制并发数量时&#xff0c;信号量&#xff08;Semaphore&am…

android aosp系统定制如何监控系统性能

监控 Android 系统性能是优化系统、排查问题和提升用户体验的关键步骤。以下是一个详细的方案&#xff0c;涵盖从工具使用到自定义监控的实现方法。 1. 使用 Android 自带工具 1.1 adb 工具 adb 是 Android Debug Bridge 的缩写&#xff0c;是监控系统性能的基础工具。 1.1…

20.面试算法-树的深度优先遍历(二)

1. 对称和反转专题 在上一篇文章中的三个问题都需要先知道左右子树的情况才能处理自己当前的结果&#xff0c;这本质都是后序遍历&#xff0c;那前序什么时候会用呢&#xff1f;本小节就好几个。 LeetCode100&#xff1a;给你两棵二叉树的根节点 p 和 q&#xff0c;编写一个函…