eventfd / timerfd / signalfd¶
是什么 / 解决什么问题¶
Linux 提供了将事件(通知、定时器、信号)封装为文件描述符的机制,使其可以统一用 epoll 监听。避免了回调、信号处理函数等异步复杂性,将所有事件源收敛到一个事件循环中。
eventfd — 事件通知¶
基本用法¶
#include <sys/eventfd.h>
// 创建 eventfd(初始计数 = 0)
int efd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
// 通知(写入:计数器加 n)
uint64_t val = 1;
write(efd, &val, sizeof(val));
// 等待(读取:获取计数器值并清零)
uint64_t count;
read(efd, &count, sizeof(count));
// count = 自上次 read 后的累计写入值
// 如果计数器为 0 且非阻塞:read 返回 EAGAIN
EFD_SEMAPHORE 模式¶
int efd = eventfd(0, EFD_SEMAPHORE | EFD_NONBLOCK);
// 写入: 计数器 += val
uint64_t val = 5;
write(efd, &val, sizeof(val));
// 读取: 计数器 -= 1, 返回 1
uint64_t one;
read(efd, &one, sizeof(one)); // one = 1, 计数器剩 4
// 类似信号量行为
典型用途¶
// 线程间唤醒(替代 pipe 的 self-pipe trick)
// 线程 A:等待事件
struct epoll_event ev = { .events = EPOLLIN, .data.fd = efd };
epoll_ctl(epfd, EPOLL_CTL_ADD, efd, &ev);
// epoll_wait 中收到 efd 可读事件
// 线程 B:发送通知
uint64_t val = 1;
write(efd, &val, sizeof(val)); // 唤醒线程 A
优势 vs pipe: - 只需一个 fd(pipe 需要两个) - 8 字节计数器,语义更明确 - 内核开销更小
timerfd — 定时器¶
基本用法¶
#include <sys/timerfd.h>
// 创建定时器 fd
int tfd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC);
// 设置定时器
struct itimerspec ts = {
.it_value = { .tv_sec = 5, .tv_nsec = 0 }, // 首次触发: 5秒后
.it_interval = { .tv_sec = 1, .tv_nsec = 0 }, // 周期: 每1秒
};
timerfd_settime(tfd, 0, &ts, NULL);
// flag = 0: 相对时间
// flag = TFD_TIMER_ABSTIME: 绝对时间
// 等待触发(read 返回触发次数)
uint64_t expirations;
read(tfd, &expirations, sizeof(expirations));
// expirations: 自上次 read 后触发了几次
// 如果定时器未触发:阻塞或 EAGAIN
// 取消定时器
struct itimerspec zero = {};
timerfd_settime(tfd, 0, &zero, NULL);
与 epoll 配合¶
int tfd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC);
struct itimerspec ts = { .it_value = { .tv_sec = 0, .tv_nsec = 100000000 } }; // 100ms
timerfd_settime(tfd, 0, &ts, NULL);
struct epoll_event ev = { .events = EPOLLIN, .data.fd = tfd };
epoll_ctl(epfd, EPOLL_CTL_ADD, tfd, &ev);
// 事件循环中:
if (events[i].data.fd == tfd) {
uint64_t exp;
read(tfd, &exp, sizeof(exp));
handle_timeout();
// 重新设置定时器(单次触发模式)
timerfd_settime(tfd, 0, &ts, NULL);
}
高精度定时¶
// 纳秒精度
struct itimerspec ts = {
.it_value = { .tv_sec = 0, .tv_nsec = 1000000 }, // 1ms
.it_interval = { .tv_sec = 0, .tv_nsec = 1000000 },
};
timerfd_settime(tfd, 0, &ts, NULL);
// CLOCK_MONOTONIC: 不受系统时间调整影响(推荐)
// CLOCK_REALTIME: 受 NTP/手动调时影响
signalfd — 信号¶
基本用法¶
#include <sys/signalfd.h>
// 先屏蔽信号(防止默认处理)
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGINT);
sigaddset(&mask, SIGTERM);
sigaddset(&mask, SIGCHLD);
sigprocmask(SIG_BLOCK, &mask, NULL);
// 创建 signalfd
int sfd = signalfd(-1, &mask, SFD_NONBLOCK | SFD_CLOEXEC);
// 读取信号信息
struct signalfd_siginfo info;
ssize_t n = read(sfd, &info, sizeof(info));
if (n == sizeof(info)) {
printf("signal %d from pid %d\n", info.ssi_signo, info.ssi_pid);
}
与 epoll 配合¶
struct epoll_event ev = { .events = EPOLLIN, .data.fd = sfd };
epoll_ctl(epfd, EPOLL_CTL_ADD, sfd, &ev);
// 事件循环中:
if (events[i].data.fd == sfd) {
struct signalfd_siginfo info;
read(sfd, &info, sizeof(info));
switch (info.ssi_signo) {
case SIGTERM:
graceful_shutdown();
break;
case SIGCHLD:
reap_children();
break;
}
}
统一事件循环¶
// 所有事件源统一到 epoll
int epfd = epoll_create1(EPOLL_CLOEXEC);
// 网络事件
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev_listen);
// 定时器事件
int tfd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK);
epoll_ctl(epfd, EPOLL_CTL_ADD, tfd, &ev_timer);
// 信号事件
int sfd = signalfd(-1, &mask, SFD_NONBLOCK);
epoll_ctl(epfd, EPOLL_CTL_ADD, sfd, &ev_signal);
// 线程间通知
int efd = eventfd(0, EFD_NONBLOCK);
epoll_ctl(epfd, EPOLL_CTL_ADD, efd, &ev_notify);
// 一个循环处理所有事件
for (;;) {
int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
for (int i = 0; i < n; i++) {
int fd = events[i].data.fd;
if (fd == listen_fd) handle_accept();
else if (fd == tfd) handle_timer();
else if (fd == sfd) handle_signal();
else if (fd == efd) handle_notification();
else handle_client_io(fd);
}
}
行为与语义¶
eventfd 计数器溢出¶
// 计数器最大值: UINT64_MAX - 1 (0xFFFFFFFFFFFFFFFE)
// 如果 write 会导致溢出:
// 阻塞模式: 阻塞等待 read 消费
// 非阻塞: 返回 EAGAIN
timerfd 的 expirations 累计¶
// 如果定时器触发了多次但没有 read:
// read 返回累计触发次数(不丢失)
uint64_t exp;
read(tfd, &exp, sizeof(exp));
// exp 可能 > 1(说明处理跟不上触发频率)
signalfd 和信号处理函数¶
signalfd 和 sigaction 可以共存:
- 如果信号同时被 signalfd 监听和有 handler:
→ handler 优先执行
- 实践中:使用 signalfd 时用 sigprocmask 屏蔽信号
→ 信号只通过 signalfd 递送
性能考量¶
| 方案 | 延迟 | 复杂度 |
|---|---|---|
| eventfd 通知 | ~1-2μs | 极低 |
| pipe 通知 | ~2-3μs | 低 |
| 信号 (kill) | ~3-5μs | 中(需要 handler) |
| futex | ~1μs | 中 |
eventfd 比 pipe 更轻量:一个 fd,64位计数器,内核路径更短。
常见陷阱与 FAQ¶
1. timerfd 精度问题¶
2. signalfd 在多线程中的行为¶
// signalfd 读取的是发给该进程的信号
// 多个线程可以有各自的 signalfd
// 但信号只被一个 signalfd 读取(先到先得)
// 推荐:只在一个线程中使用 signalfd
3. eventfd 的 read 大小¶
// read 必须提供 8 字节缓冲区
uint64_t val;
read(efd, &val, sizeof(val)); // 必须 sizeof(uint64_t)
// 提供 < 8 字节的缓冲区: EINVAL
观测与调试¶
# 查看 eventfd/timerfd/signalfd
ls -la /proc/<pid>/fd/
readlink /proc/<pid>/fd/5
# anon_inode:[eventfd]
# anon_inode:[timerfd]
# anon_inode:[signalfd]
# strace 看 timerfd 操作
strace -e trace=timerfd_create,timerfd_settime,read -p <pid>
# 查看 timerfd 的设置
cat /proc/<pid>/fdinfo/<tfd>
# clockid: 1 (CLOCK_MONOTONIC)
# ticks: 0
# settime flags: 01
# it_value: (5, 0)
# it_interval: (1, 0)
延伸阅读¶
man 2 eventfd— 事件通知man 2 timerfd_create— 定时器man 2 signalfd— 信号转 fd- libuv / libev 源码 — 跨平台事件循环如何使用这些机制