muduo源码阅读:linux timefd定时器

devtools/2025/2/27 4:14:41/
⭐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.ppmy.cn/devtools/162956.html

相关文章

直角三角堰计算公式

直角三角堰的计算公式通常用于确定流经直角三角形形状的堰的流量。河北瑾航科技遥测终端机 通过采集液位数据(模拟量、串口485/232)&#xff0c;计算得到瞬时流量&#xff0c;然后通过积分进行累计算出累积量&#xff1b;直角三角堰的流量计算公式为&#xff1a; 直角三角堰 计…

Java面试准备篇:全面了解面试流程与常见问题

文章目录 1.1 Java面试概述1.2 面试流程和注意事项1.3 自我介绍及项目介绍1.4 常见面试问题 在现代职场中&#xff0c;面试是求职过程中至关重要的一环&#xff0c;特别是对于Java开发者而言。为了帮助广大Java开发者更好地应对面试&#xff0c;本文将提供一份全面的Java面试…

编写第一个 C++ 程序 – Hello World 示例

“Hello World”程序是学习任何编程语言的第一步&#xff0c;也是您将学习的最直接的程序之一。它是用于演示编码过程如何工作的基本程序。您所要做的就是在输出屏幕上显示 “Hello World”。 C Hello World 程序 下面是在控制台屏幕上打印 “Hello World” 的 C 程序。 // …

开源模型应用落地-glm模型小试-glm-4-9b-chat-压力测试(六)

一、前言 GLM-4是智谱AI团队于2024年1月16日发布的基座大模型&#xff0c;旨在自动理解和规划用户的复杂指令&#xff0c;并能调用网页浏览器。其功能包括数据分析、图表创建、PPT生成等&#xff0c;支持128K的上下文窗口&#xff0c;使其在长文本处理和精度召回方面表现优异&a…

Unity FBXExport导出的FBX无法在Blender打开

将FBX转换为obj&#xff1a; Convert 3D models online - free and secure

kafka+spring cloud stream 发送接收消息

方案 1&#xff1a;使用旧版 StreamListener&#xff08;适用于 Spring Cloud Stream < 2.x&#xff09; 1. 添加依赖&#xff08;pom.xml&#xff09; <!-- Spring Cloud Stream Kafka Binder --> <dependency> <groupId>org.springframework.clo…

数据库(MySQL)二

MySQL 六、MySQL索引视图6.1 索引底层原理6.1.1 索引hash算法6.1.2 索引二叉树算法6.1.3 索引平衡二叉树算法6.1.4 索引BTREE树算法6.1.5 普通SQL全表扫描过程 6.2 索引分类6.2.1 按数据结构层次分类6.2.2 按字段数量层次分类6.2.3 按功能逻辑层次分类&#xff08;面试题&#…

【Elasticsearch】同一台服务器部署集群

【Elasticsearch】同一台服务器部署集群 1. 同一台服务器搭建ES集群2. 配置不同的node节点3. ES集群中安装IK分词器4. 启动es集群5. Kibana访问集群6. es-head7. 集群中创建索引7.1 什么是分片以及分片的好处7.2 副本&#xff08;Replication&#xff09;7.3 通过es-head创建索…