- "传统 Reactor:用
select(2)
/poll(2)
的等待时间实现定时"; - "现代 Linux:用 timerfd 把定时当作可读 fd纳入同一事件循环"。
方案 A:用 select(2)
的超时实现定时
要点:把"下一次到期时间"换算成 struct timeval timeout
传入 select
,select
返回后先处理 I/O,再检查"当前时间 ≥ 到期时间"来触发定时回调,并据此计算下一次的超时。
c
// 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/O
if (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 完全一致。
c
// 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_interval
its.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) 创建 epoll
int 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 加入 epoll
struct 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
(避免系统时间调整导致跳变)。