当前位置:首页>Linux>linux系统编程(十):eventfd-timerfd-signalfd

linux系统编程(十):eventfd-timerfd-signalfd

  • 2026-09-18 12:33:57
linux系统编程(十):eventfd-timerfd-signalfd

eventfd / timerfd / signalfd —— 一切皆文件的现代演绎

Linux 把"任意事件"都做成 fd —— 计数器变 fd(eventfd),定时器变 fd(timerfd),信号变 fd(signalfd)。这些 fd 都能塞进 epoll,跟普通 socket / pipe 一起处理。这就是现代事件循环(libuv / libev / Boost.Asio)的标准玩法。


0. 引言:epoll 时代的"统一事件源"

老 Unix 时代每种事件用不同 API:

事件类型
老的处理方式
网络数据
select
 等 socket fd
定时器
alarm
 + SIGALRM
信号
signal
 handler
进程内通知
管道 / 条件变量

问题:异步信号安全限制、handler 不能复杂、跟 epoll 不能融合。

Linux 内核给出新答案:把所有事件都做成 fd,一个 epoll 全包。

事件源
老办法
新办法
进程内通知
pipe(占两个 fd)
eventfd
(一个 fd)
定时器
alarm + SIGALRM
timerfd
信号
signal handler
signalfd
进程退出
SIGCHLD + waitpid
pidfd(5.3+)
文件变化
inotify_init
inotify_init1

下面三个最常用的 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 经典场景

场景
怎么用 eventfd
线程池
boss 线程把任务放队列后写 eventfd,worker 主循环 epoll_wait 醒来处理
优雅退出
信号 handler 写 eventfd,主循环 epoll_wait 醒来知道该停了
跨进程通知
fork 前创建 eventfd,父子都能用

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 的优势:

维度
timerfd 的改进
信号路径
不走信号,async-signal-safe 限制无关
多 timer 共存
多个 timer 互不干扰(alarm 全进程共一个)
时钟选择
可用 CLOCK_MONOTONIC,不受系统时间跳变影响
跟 epoll 集成
天然作为 fd,跟其他 I/O 平等处理

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 的痛点

痛点
signalfd 的改进
handler 受 async-signal-safe 限制
read 之后走主循环
,能 printf / malloc / 调用任何函数
多信号到达可能丢失
内核排队,read 一次拿一个 siginfo 不丢
跟 epoll 集成困难
信号变 fd,跟其他 fd 平等处理

4. 三者对比

特性
eventfd
timerfd
signalfd
触发方式
显式 write
时间到期
信号到达
read 拿到
64 位计数器
过期次数
siginfo_t
典型场景
线程间唤醒
定时任务
优雅退出
不用时怎么办
self-pipe
alarm/setitimer
signal handler

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)
进程内/跨进程通知
self-pipe trick
timerfd_create + settime
单次/周期定时器
alarm / setitimer
signalfd
信号 fd 化
signal handler

三者放一起的精髓:

任意事件源都能变成 fd → 全部塞进 epoll → 单线程统一处理。

这就是 Linux 现代事件循环的根基,理解之后再看 libuv / libevent / Boost.Asio / nginx / Redis 的网络层,会发现底层都是这套架构。

最新文章

随机文章