TimerFd Epoll
- “传统 Reactor:用
select(2)
/poll(2)
的等待时间实现定时”; - “现代 Linux:用 timerfd 把定时当作可读 fd纳入同一事件循环”。
方案 A:用 select(2)
的超时实现定时
要点:把“下一次到期时间”换算成 struct timeval timeout
传入 select
,select
返回后先处理 I/O,再检查“当前时间 ≥ 到期时间”来触发定时回调,并据此计算下一次的超时。
// build: gcc -Wall -O2 select_timer.c -o select_timer
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <unistd.h>
#include <sys/time.h>
#include <errno.h>static long long now_ms(void) {struct timeval tv; gettimeofday(&tv, NULL);return (long long)tv.tv_sec * 1000 + tv.tv_usec / 1000;
}int main(void) {// 模拟:每 1000ms 触发一次“定时任务”long long interval = 1000;long long next_expire = now_ms() + interval;// 示意:监听 0 号 fd(stdin)作为 I/O 事件源int maxfd = 0;for (;;) {long long n = now_ms();long long remain = next_expire - n;if (remain < 0) remain = 0;struct timeval tv;tv.tv_sec = remain / 1000;tv.tv_usec = (remain % 1000) * 1000;fd_set rfds;FD_ZERO(&rfds);FD_SET(0, &rfds); // 监听 stdin 可读int rc = select(maxfd + 1, &rfds, NULL, NULL, &tv);if (rc < 0) {if (errno == EINTR) continue; // 被信号中断,重来perror("select");break;}// 1) 先处理 I/Oif (rc > 0 && FD_ISSET(0, &rfds)) {char buf[256];ssize_t nr = read(0, buf, sizeof(buf));if (nr > 0) {write(1, "read stdin\n", 11);}}// 2) 再处理定时(可能一次到期多个,视设计而定)long long now = now_ms();if (now >= next_expire) {// 触发“定时回调”write(1, "timer fired\n", 12);// 重新安排下一次next_expire = now + interval;}}return 0;
}
若用
poll(2)
,思路一致:把remain
毫秒放进poll(timeout_ms)
,返回后同样先处理 I/O,再检查到期。区别仅在 API 形式,这里就不重复放代码了。
方案 B:用 timerfd
+ epoll
把定时纳入 I/O 事件流
要点:创建 timerfd
(推荐 CLOCK_MONOTONIC
),把它加入 epoll
;设置一次性到期(one-shot);到期时 timerfd
变为可读,在读回 8 字节计数后执行回调,然后重新设置下一次到期。这样,代码路径与普通套接字 I/O 完全一致。
// build: gcc -Wall -O2 epoll_timerfd.c -o epoll_timerfd
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <sys/timerfd.h>
#include <string.h>
#include <errno.h>
#include <time.h>
#include <fcntl.h>static int set_nonblock(int fd) {int fl = fcntl(fd, F_GETFL, 0);if (fl < 0) return -1;return fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}static void arm_timerfd(int tfd, int ms) {struct itimerspec its;memset(&its, 0, sizeof(its));// one-shot:只设置 it_value,不设置 it_intervalits.it_value.tv_sec = ms / 1000;its.it_value.tv_nsec = (ms % 1000) * 1000000;if (timerfd_settime(tfd, 0, &its, NULL) < 0) {perror("timerfd_settime");exit(1);}
}int main(void) {// 1) 创建 epollint ep = epoll_create1(EPOLL_CLOEXEC);if (ep < 0) { perror("epoll_create1"); return 1; }// 2) 创建 timerfd(MONOTONIC 避免系统时间跳变影响)int tfd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK);if (tfd < 0) { perror("timerfd_create"); return 1; }// 3) 把 timerfd 加入 epollstruct epoll_event ev; memset(&ev, 0, sizeof(ev));ev.events = EPOLLIN; // 也可用 EPOLLET,看你整体风格ev.data.u32 = 1; // 简单标识if (epoll_ctl(ep, EPOLL_CTL_ADD, tfd, &ev) < 0) {perror("epoll_ctl add timerfd"); return 1;}// 4) 也把 stdin(0) 放进来,演示 I/O & 定时“同路由”int stdin_fd = 0;set_nonblock(stdin_fd);memset(&ev, 0, sizeof(ev));ev.events = EPOLLIN;ev.data.u32 = 2;if (epoll_ctl(ep, EPOLL_CTL_ADD, stdin_fd, &ev) < 0) {perror("epoll_ctl add stdin"); return 1;}// 5) 设定首次 1000ms 后触发int interval_ms = 1000;arm_timerfd(tfd, interval_ms);// 6) 事件循环struct epoll_event events[16];for (;;) {int n = epoll_wait(ep, events, 16, -1);if (n < 0) {if (errno == EINTR) continue;perror("epoll_wait"); break;}for (int i = 0; i < n; ++i) {if (events[i].data.u32 == 1 && (events[i].events & EPOLLIN)) {// timerfd 就绪:必须 read 8 字节计数uint64_t cnt;ssize_t r = read(tfd, &cnt, sizeof(cnt));if (r == sizeof(cnt)) {// 触发“定时回调”write(1, "timer fired\n", 12);// 重新安排下一次(one-shot 设计)arm_timerfd(tfd, interval_ms);} else if (r < 0 && errno == EAGAIN) {// 非阻塞读完} else {perror("read timerfd");}} else if (events[i].data.u32 == 2 && (events[i].events & EPOLLIN)) {// stdin 可读:与 socket/pipe 一样处理char buf[256];ssize_t nr = read(0, buf, sizeof(buf));if (nr > 0) {write(1, "read stdin\n", 11);}}}}close(tfd);close(ep);return 0;
}
对照要点(便于你在工程中选型)
-
一致性:
select/poll
超时:定时靠“返回值 + 当前时间比较”,I/O 走 fd 事件 → 两条路径。timerfd
:定时也是一个 fd,与 I/O 完全“同路径”。
-
易错点:
select/poll
:计算剩余时间与“更新最早到期”的竞态需要谨慎处理。timerfd
:记得read 出 8 字节计数清空可读状态;建议用 one-shot,回调后再settime
。
-
推荐时钟:
CLOCK_MONOTONIC
(避免系统时间调整导致跳变)。