eventfd / timerfd / signalfd —— 一切皆文件的现代演绎
Linux 把"任意事件"都做成 fd —— 计数器变 fd(eventfd),定时器变 fd(timerfd),信号变 fd(signalfd)。这些 fd 都能塞进 epoll,跟普通 socket / pipe 一起处理。这就是现代事件循环(libuv / libev / Boost.Asio)的标准玩法。
0. 引言:epoll 时代的"统一事件源"
老 Unix 时代每种事件用不同 API:
问题:异步信号安全限制、handler 不能复杂、跟 epoll 不能融合。
Linux 内核给出新答案:把所有事件都做成 fd,一个 epoll 全包。
下面三个最常用的 fd 化事件源。
1. eventfd —— 进程内"通知一下"
#include<sys/eventfd.h>inteventfd(unsignedint initval, int flags);/* flags: EFD_NONBLOCK | EFD_CLOEXEC | EFD_SEMAPHORE */
返回一个 fd,内部维护一个 64 位无符号计数器。
1.1 默认模式(计数器累加)
int efd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);/* 写:累加到计数器 */uint64_t v = 1;write(efd, &v, sizeof v); /* 计数 +1 */write(efd, &v, sizeof v); /* 计数 +2 *//* 读:把累计计数取走,重置为 0 */uint64_t got;read(efd, &got, sizeof got); /* got = 2,计数 = 0 */
write 和 read 必须传 8 字节。
1.2 信号量模式(EFD_SEMAPHORE)
int efd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC | EFD_SEMAPHORE);/* write 跟之前一样累加 */uint64_t v = 3;write(efd, &v, sizeof v); /* 计数 = 3 *//* read 每次只减 1 */uint64_t got;read(efd, &got, sizeof got); /* got = 1,计数 = 2 */read(efd, &got, sizeof got); /* got = 1,计数 = 1 */read(efd, &got, sizeof got); /* got = 1,计数 = 0 */read(efd, &got, sizeof got); /* 阻塞 / EAGAIN */
像信号量。
1.3 用途:替代 self-pipe trick
老技巧"写 pipe 通知主线程退出循环":
/* 老 pipe 做法:占两个 fd */int p[2]; pipe(p);fcntl(p[0], F_SETFL, O_NONBLOCK);/* 信号 handler 里 */write(p[1], "x", 1);/* 主循环 */epoll_ctl(ep, EPOLL_CTL_ADD, p[0], &ev);
eventfd 一个 fd 搞定:
int efd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);/* 通知 */uint64_t v = 1;write(efd, &v, sizeof v);/* 主循环 */epoll_ctl(ep, EPOLL_CTL_ADD, efd, &ev);
跨线程通知、跨进程通知(fork 后共享)都常用。
1.4 经典场景
| |
|---|
| 线程池 | boss 线程把任务放队列后写 eventfd,worker 主循环 epoll_wait 醒来处理 |
| 优雅退出 | 信号 handler 写 eventfd,主循环 epoll_wait 醒来知道该停了 |
| 跨进程通知 | |
2. timerfd —— 定时器变成 fd
#include<sys/timerfd.h>inttimerfd_create(int clockid, int flags);/* clockid: CLOCK_REALTIME / CLOCK_MONOTONIC / CLOCK_BOOTTIME ... * flags: TFD_NONBLOCK | TFD_CLOEXEC */inttimerfd_settime(int fd, int flags, const struct itimerspec *new_value, struct itimerspec *old_value);struct itimerspec { struct timespec it_interval; /* 周期,0 = 单次定时器 */ struct timespec it_value; /* 第一次触发时刻 */};
2.1 单次定时器:3 秒后触发一次
int tfd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC);struct itimerspec spec = { .it_value = {.tv_sec = 3, .tv_nsec = 0}, .it_interval = {0} /* 0 = 不重复 */};timerfd_settime(tfd, 0, &spec, NULL);/* 加进 epoll */struct epoll_event ev = { .events = EPOLLIN, .data.fd = tfd };epoll_ctl(ep, EPOLL_CTL_ADD, tfd, &ev);/* 触发后 read 拿到 1 (过期次数) */uint64_t expirations;read(tfd, &expirations, sizeof expirations);
2.2 周期定时器:每秒触发
struct itimerspec spec = { .it_value = {.tv_sec = 1}, /* 1 秒后第一次 */ .it_interval = {.tv_sec = 1} /* 之后每 1 秒一次 */};timerfd_settime(tfd, 0, &spec, NULL);
2.3 取消定时器
struct itimerspec stop = {0}; /* 全 0 表示停止 */timerfd_settime(tfd, 0, &stop, NULL);
2.4 跟 epoll 配合的经典场景
while (1) { int n = epoll_wait(ep, evs, MAX, -1); for (int i = 0; i < n; i++) { if (evs[i].data.fd == tfd) { uint64_t exp; read(tfd, &exp, sizeof exp); handle_timer(); /* 定时任务 */ } else { /* 其他 fd */ } }}
跟 alarm + SIGALRM 比,timerfd 的优势:
| |
|---|
| 不走信号,async-signal-safe 限制无关 |
| 多个 timer 互不干扰(alarm 全进程共一个) |
| 可用 CLOCK_MONOTONIC,不受系统时间跳变影响 |
| |
3. signalfd —— 信号变 fd
#include<sys/signalfd.h>intsignalfd(int fd, constsigset_t *mask, int flags);/* flags: SFD_NONBLOCK | SFD_CLOEXEC */
3.1 用法
sigset_t mask;sigemptyset(&mask);sigaddset(&mask, SIGINT);sigaddset(&mask, SIGTERM);/* 必须先屏蔽这些信号,否则还会走默认/handler 路径 */pthread_sigmask(SIG_BLOCK, &mask, NULL);int sfd = signalfd(-1, &mask, SFD_NONBLOCK | SFD_CLOEXEC);/* 加进 epoll */struct epoll_event ev = { .events = EPOLLIN, .data.fd = sfd };epoll_ctl(ep, EPOLL_CTL_ADD, sfd, &ev);/* 主循环 */while (1) { epoll_wait(...); if (evs[i].data.fd == sfd) { struct signalfd_siginfo si; while (read(sfd, &si, sizeof si) == sizeof si) { printf("signal %d arrived\n", si.ssi_signo); if (si.ssi_signo == SIGTERM) goto out; } }}out:
3.2 signalfd 解决了 signal handler 的痛点
| |
|---|
| handler 受 async-signal-safe 限制 | read 之后走主循环,能 printf / malloc / 调用任何函数 |
| 内核排队,read 一次拿一个 siginfo 不丢 |
| |
4. 三者对比

5. 三个一起用:完整事件循环骨架
#include<sys/epoll.h>#include<sys/eventfd.h>#include<sys/timerfd.h>#include<sys/signalfd.h>#include<signal.h>#include<unistd.h>#include<stdio.h>intmain(void){ /* 1. 屏蔽信号 → 走 signalfd */ sigset_t mask; sigemptyset(&mask); sigaddset(&mask, SIGINT); sigaddset(&mask, SIGTERM); pthread_sigmask(SIG_BLOCK, &mask, NULL); int sfd = signalfd(-1, &mask, SFD_NONBLOCK | SFD_CLOEXEC); /* 2. 1秒周期 timer */ int tfd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC); struct itimerspec spec = {{1, 0}, {1, 0}}; timerfd_settime(tfd, 0, &spec, NULL); /* 3. 任务通知 */ int efd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC); /* 4. epoll 把它们都加进来 */ int ep = epoll_create1(EPOLL_CLOEXEC); struct epoll_event ev; ev.events = EPOLLIN; ev.data.fd = sfd; epoll_ctl(ep, EPOLL_CTL_ADD, sfd, &ev); ev.data.fd = tfd; epoll_ctl(ep, EPOLL_CTL_ADD, tfd, &ev); ev.data.fd = efd; epoll_ctl(ep, EPOLL_CTL_ADD, efd, &ev); /* 5. 主循环 */ struct epoll_event evs[16]; int running = 1; while (running) { int n = epoll_wait(ep, evs, 16, -1); for (int i = 0; i < n; i++) { int fd = evs[i].data.fd; if (fd == sfd) { struct signalfd_siginfo si; read(sfd, &si, sizeof si); printf("signal %d → exit\n", si.ssi_signo); running = 0; } else if (fd == tfd) { uint64_t exp; read(tfd, &exp, sizeof exp); printf("tick\n"); } else if (fd == efd) { uint64_t v; read(efd, &v, sizeof v); printf("got notify x%lu\n", v); } } } close(sfd); close(tfd); close(efd); close(ep); return 0;}
50 行做出一个能"周期任务 + 接信号优雅退出 + 接受外部通知"的事件循环。 现代框架(libuv / libevent / Asio)的内核就是这个套路。
6. 一些细节坑
6.1 eventfd 必须读写 8 字节
char c = 1;write(efd, &c, 1); /* 错误,必须 8 字节 */
6.2 signalfd 之前必须屏蔽信号
不屏蔽,信号会走默认路径或注册的 handler,signalfd 拿不到。
6.3 timerfd 的 CLOCK_REALTIME 会被时间跳变影响
服务器调时间或 NTP 跳变,CLOCK_REALTIME 的 timerfd 可能立刻过期或永远不到期。做超时控制用 CLOCK_MONOTONIC。
6.4 epoll 拿到事件后必须 read,否则 LT 模式下死循环
read 出来才会清除"就绪"状态。否则 epoll_wait 一直返回这个 fd。
6.5 read signalfd 拿不全多个挂起信号
每次只拿 sizeof(signalfd_siginfo)。要循环 read 直到 EAGAIN:
while (read(sfd, &si, sizeof si) == sizeof si) { handle(si);}
7. 总结
| | |
|---|
eventfd(0, flags) | | |
timerfd_create + settime | | |
signalfd | | |
三者放一起的精髓:
任意事件源都能变成 fd → 全部塞进 epoll → 单线程统一处理。
这就是 Linux 现代事件循环的根基,理解之后再看 libuv / libevent / Boost.Asio / nginx / Redis 的网络层,会发现底层都是这套架构。