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.pswp.cn/bicheng/71831.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/71831.shtml
英文地址,請注明出處:http://en.pswp.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;機器學習的…