?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;
}
阻塞模式下,當對定時器文件描述符執行read
或write
等操作時,如果操作不能立即完成,進程會進入睡眠狀態,等待操作條件滿足。這就導致應用程序在這個操作上被阻塞,無法繼續執行后續代碼,包括處理其他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
的定時器堆),但需權衡精度和性能。