跳转至

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 精度问题

timerfd 精度取决于系统 tick 和 hrtimer 支持
大部分现代内核: 纳秒级 hrtimer
但实际精度受调度延迟影响(如果线程忙,可能延迟处理)

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 源码 — 跨平台事件循环如何使用这些机制