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 的定时器堆),但需权衡精度和性能。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/71831.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Pinia 3.0 正式发布:全面拥抱 Vue 3 生态,升级指南与实战教程

一、重大版本更新解析 2024年2月11日&#xff0c;Vue 官方推荐的状态管理库 Pinia 迎来 3.0 正式版发布&#xff0c;本次更新标志着其全面转向 Vue 3 技术生态。以下是开发者需要重点关注的升级要点&#xff1a; 1.1 核心变更说明 特性3.0 版本要求兼容性说明Vue 支持Vue 3.…

【图像处理 --- Sobel 边缘检测的详解】

Sobel 边缘检测的详解 目录 Sobel 边缘检测的详解1. 梯度计算2. 梯度大小3. 梯度方向4. 非极大值抑制5. 双阈值处理6. 在 MATLAB 中实现 Sobel 边缘检测7.运行结果展示8.关键参数解释9.实验与验证 Sobel 边缘检测是一种经典的图像处理算法&#xff0c;用于检测图像中的边缘。它…

LeetCode 热题100 15. 三数之和

LeetCode 热题100 | 15. 三数之和 大家好&#xff0c;今天我们来解决一道经典的算法题——三数之和。这道题在 LeetCode 上被标记为中等难度&#xff0c;要求我们从一个整数数组中找到所有不重复的三元组&#xff0c;使得三元组的和为 0。下面我将详细讲解解题思路&#xff0c…

基因组组装中的术语1——from HGP

Initial sequencing and analysis of the human genome | Nature 1&#xff0c;分层鸟枪法测序hierarchical shotgun sequencing

安全开发-环境选择

文章目录 个人心得虚拟机选择ubuntu 22.04python环境选择conda下载使用&#xff1a; 个人心得 在做开发时配置一个专门的环境可以使我们在开发中的效率显著提升&#xff0c;可以避免掉很多环境冲突的报错。尤其是python各种版本冲突&#xff0c;还有做渗透工具不要选择windows…

数字体验驱动用户参与增效路径

内容概要 在数字化转型深化的当下&#xff0c;数字内容体验已成为企业与用户建立深度连接的核心切入点。通过个性化推荐引擎与智能数据分析系统的协同运作&#xff0c;企业能够实时捕捉用户行为轨迹&#xff0c;构建精准的用户行为深度洞察模型。这一模型不仅支撑内容分发的动…

Python 字符串(str)全方位剖析:从基础入门、方法详解到跨语言对比与知识拓展

Python 字符串&#xff08;str&#xff09;全方位剖析&#xff1a;从基础入门、方法详解到跨语言对比与知识拓展 本文将深入探讨 Python 中字符串&#xff08;str&#xff09;的相关知识&#xff0c;涵盖字符串的定义、创建、基本操作、格式化等内容。同时&#xff0c;会将 Py…

使用C++实现简单的TCP服务器和客户端

使用C实现简单的TCP服务器和客户端 介绍准备工作1. TCP服务器实现代码结构解释 2. TCP客户端实现代码结构解释 3. 测试1.编译&#xff1a;2.运行 结语 介绍 本文将通过一个简单的例子&#xff0c;介绍如何使用C实现一个基本的TCP服务器和客户端。这个例子展示了如何创建服务器…

Java Web开发实战与项目——Spring Boot与Spring Cloud微服务项目实战

企业级应用中&#xff0c;微服务架构已经成为一种常见的开发模式。Spring Boot与Spring Cloud提供了丰富的工具和组件&#xff0c;帮助开发者快速构建、管理和扩展微服务应用。本文将通过一个实际的微服务项目&#xff0c;展示如何使用Spring Boot与Spring Cloud构建微服务架构…

VMware建立linux虚拟机

本文适用于初学者&#xff0c;帮助初学者学习如何创建虚拟机&#xff0c;了解在创建过程中各个选项的含义。 环境如下&#xff1a; CentOS版本&#xff1a; CentOS 7.9&#xff08;2009&#xff09; 软件&#xff1a; VMware Workstation 17 Pro 17.5.0 build-22583795 1.配…

Linux8-互斥锁、信号量

一、前情回顾 void perror(const char *s);功能&#xff1a;参数&#xff1a; 二、资源竞争 1.多线程访问临界资源时存在资源竞争&#xff08;存在资源竞争、造成数据错乱&#xff09; 临界资源&#xff1a;多个线程可以同时操作的资源空间&#xff08;全局变量、共享内存&a…

LD_PRELOAD 绕过 disable_function 学习

借助这位师傅的文章来学习通过LD_PRELOAD来绕过disable_function的原理 【PHP绕过】LD_PRELOAD bypass disable_functions_phpid绕过-CSDN博客 感谢这位师傅的贡献 介绍 静态链接&#xff1a; &#xff08;1&#xff09;举个情景来帮助理解&#xff1a; 假设你要搬家&#x…

【无人集群系列---无人机集群编队算法】

【无人集群系列---无人机集群编队算法】 一、核心目标二、主流编队控制方法1. 领航-跟随法&#xff08;Leader-Follower&#xff09;2. 虚拟结构法&#xff08;Virtual Structure&#xff09;3. 行为法&#xff08;Behavior-Based&#xff09;4. 人工势场法&#xff08;Artific…

Oracle Fusion Middleware更改weblogic密码

前言 当用户忘记weblogic密码时&#xff0c;且无法登录到web界面中&#xff0c;需要使用服务器命令更改密码 更改方式 1、备份 首先进入 weblogic 安装目录&#xff0c;备份三个文件&#xff1a;boot.properties&#xff0c;DefaultAuthenticatorInit.ldift&#xff0c;Def…

MongoDB 复制(副本集)

MongoDB 复制(副本集) 引言 MongoDB是一个高性能、可扩展、易于使用的文档存储系统。它以JSON-like的文档存储结构&#xff0c;支持灵活的数据模型。在分布式系统中&#xff0c;为了提高数据可用性和系统稳定性&#xff0c;常常需要实现数据的备份和冗余。MongoDB提供了副本集…

【Erdas实验教程】009:非监督分类及分类后评价

文章目录 一、分类过程二、分类评价ERDAS 的 ISODATA 算法是基于最小光谱距离来进行的非监督分类,聚类过程始于任意聚类平均值或一个已有分类模板的平均值;聚类每重复一次,聚类的平均值就更新一次,新聚类的均值再用于下次聚类循环。这个过程不断重复,直到最大的循环次数已…

一周学会Flask3 Python Web开发-Jinja2模板访问对象

锋哥原创的Flask3 Python Web开发 Flask3视频教程&#xff1a; 2025版 Flask3 Python web开发 视频教程(无废话版) 玩命更新中~_哔哩哔哩_bilibili 如果渲染模板传的是对象&#xff0c;如果如何来访问呢&#xff1f; 我们看下下面示例&#xff1a; 定义一个Student类 cla…

git 命令 设置别名

在Git中&#xff0c;您可以通过以下命令查看所有的alias&#xff08;别名&#xff09;&#xff1a; git config --get-regexp alias 这个命令会列出所有配置的alias&#xff0c;例如&#xff1a; alias.st.status alias.co.checkout alias.br.branch ... 如果您想查看某个特定a…

React Router v5 vs v6 路由配置对比

React Router v5 vs v6 路由配置对比 React Router 是 React 中最常用的路由库&#xff0c;从 v5 到 v6 版本&#xff0c;发生了较大变化。本文对比 React Router v5 和 React Router v6 的配置方式&#xff0c;帮助开发者顺利迁移。 1. 安装依赖 React Router v5 npm inst…

机器学习,我们主要学习什么?

机器学习的发展历程 机器学习的发展历程&#xff0c;大致分为以下几个阶段&#xff1a; 1. 起源与早期探索&#xff08;20世纪40年代-60年代&#xff09; 1949年&#xff1a;Hebb提出了基于神经心理学的学习机制&#xff0c;开启了机器学习的先河1950年代&#xff1a;机器学习的…