Linux内核分析之进程间通信-06

32.1 eventfd —— 事件通知机制

eventfd 把"一个事件发生了"压缩成 8 个字节的计数器加一个等待队列——它是 epoll 生态里最小的积木:线程池唤醒、io_uring 完成通知(37 章)、QEMU 主循环、DMA 完成回调(virtio,51 章)都以它为终结点。本节拆解计数语义、读写路径与内核侧 API。


32.1.1 eventfd_ctx:一个计数器就是全部

// fs/eventfd.c:30-45
struct eventfd_ctx {
    struct kref kref;       /* 引用计数 (fd 与内核侧共享) */
    wait_queue_head_t wqh;
    /*
     * Every time that a write(2) is performed on an eventfd, the
     * value of the __u64 being written is added to "count" and a
     * wakeup is performed on "wqh". If EFD_SEMAPHORE flag was not
     * specified, a read(2) will return the "count" value to userspace,
     * and will reset "count" to zero. The kernel side eventfd_signal()
     * also, adds to the "count" counter and issue a wakeup.
     */
    __u64 count;
    unsigned int flags;
    int id;
};

计数语义的两个模式(注释原文即规范):

 默认模式 (事件源模式):
   write(efd, &n, 8):  count += n; 唤醒 wqh
   read(efd):          返回 count 并清零
   → "多少次 write 都只醒一次 read" —
     通知合并! 风暴期间事件只累积不放大
     (epoll 边缘触发配合的标准姿势: 读到 EAGAIN 为止)

 EFD_SEMAPHORE 模式 (信号量模式):
   read: count>0 时返回 1 并 count--
   → 每次 write 唤醒恰好一个读者 —
     "每个事件必须被单独消费"的语义

 就绪判定 (eventfd_poll, :118-180):
   可读: count != 0        (:180 的三态表达式)
   可写: count < MAX - 1   (计数器未满)
   注: count 达 ULLONG_MAX 后 write 返回 EINVAL
   — 通知无消费时会饱和, 需应用兜底

32.1.2 内核侧 API:eventfd_signal_mask

// fs/eventfd.c:56-80(节选)
void eventfd_signal_mask(struct eventfd_ctx *ctx, __poll_t mask)
{
    ...
    /*
     * check eventfd_signal_allowed() before calling this function. If
     * it returns false, the eventfd_signal() call should be deferred to
     * a workqueue...
     */
    ...
    spin_lock_irqsave(&ctx->wqh.lock, flags);
    if (ctx->count < ULLONG_MAX)
        ctx->count++;
    wake_up_locked_poll(&ctx->wqh, EPOLLIN | mask);
    spin_unlock_irqrestore(&ctx->wqh.lock, flags);
}
EXPORT_SYMBOL_GPL(eventfd_signal_mask);

内核子系统(io_uring、virtio、dma-buf 完成路径)向用户态通知时调 eventfd_signal_mask()——计数 +1 并按掩码唤醒(epoll/poll/同 waitqueue 各自的位)。注释里的约束是实时性契约:在"不允许唤醒"的上下文(原子区、调度禁用区)不得直接调用,必须延迟到 workqueue——否则一次 DMA 完成就会撕裂调度器状态。kref 使内核侧可持引用独立于 fd 存在(fd 关闭而内核还在用是合法态)。

32.1.3 与 epoll 的协作细节

eventfd 在事件循环中的两种角色:

 [1] 通用唤醒通道 (最常见):
     epoll 循环线程睡在 epoll_wait 上
     工作线程/内核路径完成工作 → eventfd_signal
     → 循环醒来收货 — 替代跨线程 pthread_kill /
       自管道技巧 (self-pipe trick, 老式写法)

 [2] 跨上下文完成通知:
     io_uring: sqpoll 内核线程完成 IO → eventfd (37 章)
     virtio: vhost 线程 → eventfd → QEMU 主循环 (51 章)
     vfio 设备中断 → eventfd (43 章) — 硬件中断 fd 化!

 EFD_NONBLOCK 几乎必开:
     读空 eventfd 阻塞会挂死事件循环 —
     "读到 EAGAIN"是消费循环的终止条件

 标志一致性断言 (eventfd.c:383-385):
     BUILD_BUG_ON(EFD_CLOEXEC != O_CLOEXEC) —
     eventfd2 的标志值就是通用文件标志值,
     编译期保证两套常量不分叉

与 26 章管道的对照最能说明定位:管道 64KB 环、有数据语义、fd 成对;eventfd 一个计数、无数据语义(数值只是事件数)、单 fd——管道传内容,eventfd 传"叮"。需要带数据的异步通道时用 pipe/socket,纯"该干活了"用 eventfd。


小结

eventfd 以"64 位计数 + 等待队列"承载事件通知:默认模式合并通知(写 N 次读 1 次)、EFD_SEMAPHORE 模式逐事件消费;内核侧 eventfd_signal_mask 是全内核向用户态事件循环敲门的标准门铃(io_uring/virtio/vfio 都是它的客户),实时性约束以"不许在原子区调用"显式声明。它是 epoll 生态的粘合剂——一切非 fd 的异步源最终都经它接入统一的等待。下一节看同一家族的定时器成员。

32.2 timerfd —— 定时器文件描述符

timerfd 把 POSIX 定时器到期变成 fd 上的可读事件:timerfd_create 建_fd、timerfd_settime 编排、到期后 fd 可读(读出到期次数)。它让事件循环不用为定时器单独挂信号处理或线程——超时与其他 fd 事件在同一次 epoll_wait 里统一等待。本节拆解其 hrtimer/alarm 双形态与到期计数语义。


32.2.1 timerfd_ctx:hrtimer 与 alarm 的联合

// fs/timerfd.c:31-50
struct timerfd_ctx {
    union {
        struct hrtimer tmr;     /* 普通时钟 */
        struct alarm alarm;     /* 可唤醒系统的时钟 */
    } t;
    ktime_t tintv;              /* 周期值 */
    ktime_t moffs;              /* CLOCK_REALTIME 偏移快照 */
    wait_queue_head_t wqh;
    u64 ticks;              /* 到期次数累计 */
    int clockid;
    short unsigned expired;         /* 就绪标志 */
    short unsigned settime_flags;
    struct rcu_head rcu;
    struct list_head clist;         /* cancel_list 节点 */
    spinlock_t cancel_lock;
    bool might_cancel;          /* settime 竞态防护标志 */
};

static enum hrtimer_restart timerfd_tmrproc(struct hrtimer *htmr)   /* :74 */

static void timerfd_triggered(struct timerfd_ctx *ctx)          /* :62 */
{
    spin_lock_irqsave(&ctx->wqh.lock, flags);
    ctx->expired = 1;
    ctx->ticks++;                   /* 到期次数 +1 */
    wake_up_locked_poll(&ctx->wqh, EPOLLIN);
    ...
}

hrtimer/alarm 联合体对应两组 clockid:CLOCK_MONOTONIC/CLOCK_REALTIME/CLOCK_BOOTTIME 走 hrtimer;CLOCK_REALTIME_ALARM/CLOCK_BOOTTIME_ALARM 走 alarm(内核 RTC 唤醒源,可把整系统从挂起中唤醒——手机闹钟的机制,需 CAP_WAKE_ALARM)。ticks 的语义与 eventfd 不同:多次到期合并为计数——周期 1ms 的定时器在循环卡了 100ms 后 read,读到 100 而不是 1,精度损失可见但事件不丢。

32.2.2 编排与读取

// fs/timerfd.c:187-230(要点)
static int timerfd_setup(struct timerfd_ctx *ctx, int flags,
             struct itimerspec64 *ktmr)
{
    ...
    hrtimer_setup(&ctx->t.tmr, timerfd_tmrproc, clockid, htmode);   /* :208 */
    ...
}
timerfd_settime(fd, flags, &new, &old) 的编排语义:

 new.it_value:     首次到期 (0 = 解除定时器!)
 new.it_interval:  周期 (0 = 单次)
 TFD_TIMER_ABSTIME: 绝对时间 (相对系统时钟起点而非调用时刻)
 TFD_TIMER_CANCEL_ON_SET:
     CLOCK_REALTIME 上"时钟被拨动"(NTP/手工) 时取消
     — 绝对时间语义的时钟跳变防御
     (clist/cancel_list + might_cancel 即为此机制:
      挂在全局取消链上, 时钟设置路径统一清算)

 timerfd_read (read_iter, :262):
   expired==0: 阻塞(或 EAGAIN)等到期
   到期: 返回 8 字节 u64 = 本轮累计 ticks, 清 expired
   (ticks 不清零! 继续累计 — 与 eventfd 清零语义的差异)

 timerfd_poll (:246): expired ? EPOLLIN : 0
   — 就绪判定只看标志, 与 read 的消费配对

ticks 不清零的设计值得注意:eventfd 的读是"取走全部",timerfd 的读是"取走快照"——若到期风暴后一次读 100,下一轮 poll 仍立即就绪(ticks 仍非零? 不——expired 清了但 ticks 留作累计供 TFD_TIMER 报告),实际语义是"每次 read 消费一轮过期事件"。绝对时间 + CANCEL_ON_SET 的组合是 NTP 环境下唯一安全的定时方式。

32.2.3 与 POSIX 定时器的对照

维度 setitimer/SIGALRM POSIX timer_t timerfd
通知方式 信号(27 章全链路) 信号或线程回调 fd 可读
epoll 集成 否(信号打断循环) 否 是
多实例 每进程 3 个槽 可多个 可多个(每 fd 一个)
精度 hrtimer 级 hrtimer 级 hrtimer 级
到期合并 信号挂起(丢失计数) 计数可查 ticks 显式计数

事件驱动服务进程的定时器几乎清一色 timerfd:超时、限速、心跳、租约全部在 epoll 的同一张兴趣表上。setitimer 的信号路径在多线程进程里还要过 27.1 节的投递边界——timerfd 把这整层不确定性换成了确定性的一次 poll 唤醒。


小结

timerfd 以"hrtimer/alarm 联合 + expired 标志 + ticks 计数"实现定时器 fd 化:三组 clockid 覆盖单调/实时/可唤醒系统三类时钟,TFD_TIMER_CANCEL_ON_SET 经全局取消链防御时钟跳变,read 返回累计到期次数而 eventfd 清零——同为"fd 上的计数",消费语义按场景分化。它与 signal 化的 setitimer 对照凸显 fd 化的价值:多实例、可 epoll、无投递边界延迟。下一节看家族最后一位成员——把信号本体变成可读数据的 signalfd。

32.3 signalfd —— 信号文件描述符

signalfd 把"信号到达"变成"fd 上有数据可读":signalfd4 以信号掩码建 fd,目标信号到达即 poll 就绪,read 读出 signalfd_siginfo。它解决了事件循环最头疼的问题——信号会打断 epoll_wait 且只能在主线程处理——把异步信号纳入统一的 fd 等待。本节拆解其掩码语义、读取路径与经典集成模式。


32.3.1 掩码与就绪判定

signalfd4(fd, &mask, sizeof(mask), flags) 的语义:

 [1] mask: 关心的信号集 (如 SIGINT|SIGTERM|SIGCHLD)
     fd 只"看见"掩码内的信号
 [2] 关键前提: 必须先 pthread_sigmask(SIG_BLOCK, mask)
     把这些信号**阻塞**!
     否则信号走正常投递 (27.1 节 handler/默认动作),
     永远到不了 fd

 就绪判定 (signalfd_poll, fs/signalfd.c:51):
     挂起集 ∩ fd 掩码 非空 → EPOLLIN
     — 复用 27.1.1 节的 pending 挂起集, 只是
     "消费方式"从 handle_signal 换成了 read

 实时信号红利 (14.5 节): SIGRTMIN..SIGRTMAX 是
 队列化的 — N 次发送 N 次 read;
 标准信号 (1..31) 在挂起集里是位图 — 同号合并,
 signalfd 只能读到一次 (与 kill 语义一致, 非缺陷)

32.3.2 读取路径:复用 dequeue_signal

// fs/signalfd.c:162 与 177(read 循环内的核心调用)
    ret = dequeue_signal(&ctx->sigmask, info, &type);
    ...
        ret = dequeue_signal(&ctx->sigmask, info, &type);
// fs/signalfd.c:201-247(读入口与操作表)
static ssize_t signalfd_read_iter(struct kiocb *iocb, struct iov_iter *to)
{ ... }
    ...
    .poll       = signalfd_poll,        /* :246 */
    .read_iter  = signalfd_read_iter,       /* :247 */

read 路径直接调用 27.1.2 节同一个 dequeue_signal()(kernel/signal.c:618)——signalfd 与 handler 是同一挂起集的两个消费者,只是 signalfd 消费时把 siginfo 序列化成 signalfd_siginfo(128 字节定长结构:si_signo/si_code/si_pid/si_uid/si_ptr……)写进用户缓冲。多次 read 直到缓冲满或无信号可取(返回 EAGAIN)。

消费路径的对照:

 信号到达 → pending 挂起集 + TIF_SIGPENDING
   ├─ 有 handler  → 返回用户态边界 → handle_signal (27.2 搭帧)
   ├─ 默认动作    → 终止/停止/忽略
   └─ 被 sigmask 阻塞 + 有 signalfd 掩盖:
        → fd 变就绪 → epoll_wait 返回 → read 拿 siginfo
          (全程无信号投递边界、无栈切换!)

 细节: read 消费的是"调用线程的挂起集" —
 signalfd 必须在接收线程内阻塞信号并读取;
 多线程各建自己的 signalfd 各读各的

32.3.3 经典集成模式与家族总结

事件循环的标准信号接入:

 int main() {
     sigset_t mask;
     sigemptyset(&mask);
     sigaddset(&mask, SIGTERM); sigaddset(&mask, SIGINT);
     sigaddset(&mask, SIGCHLD);
     pthread_sigmask(SIG_BLOCK, &mask, NULL);     /* [1] 先阻塞 */

     int sfd = signalfd4(-1, &mask, 8, SFD_NONBLOCK|SFD_CLOEXEC);
     epoll_ctl(ep, EPOLL_CTL_ADD, sfd, ...);      /* [2] 织入循环 */

     for (;;) {
         epoll_wait(ep, events, N, -1);           /* 信号不再是中断 */
         if (events[i].data.fd == sfd) {
             read(sfd, &si, sizeof(si));          /* [3] 结构化消费 */
             switch (si.ssi_signo) { ... }
         }
     }
 }

 对照传统写法: 信号 handler 里 write(self_pipe[1], "x", 1)
 (self-pipe trick) — signalfd 出现前的事件循环敲门砖,
 eventfd (32.1 节) + signalfd 出现后成为历史

三件套的共同模板收束本章:

状态源 就绪 read 返回
eventfd 64 位计数 count>0 计数(或信号量态的 1)
timerfd hrtimer/alarm expired 到期次数 ticks
signalfd 挂起信号集 集合非空 signalfd_siginfo 数组

三者都以"等待队列 + poll + read"的 file_operations 三件套接入 fd 世界(26.2.3 节 pipe_poll 的同一模式),把信号、定时器、跨线程通知统一进 epoll 的事件表——这就是现代服务进程"单线程循环管一切"的内核基础。


小结

signalfd 以掩码建 fd、复用 dequeue_signal 消费挂起集、把 siginfo 序列化进 read 缓冲——信号从此有了"先阻塞、再 poll、后结构化读取"的同步消费形态,handler 模式的栈切换与投递边界延迟全部消失。它与 eventfd(通知)、timerfd(定时器)共用"等待队列+poll+read"模板,共同把事件驱动循环所需的全部异步源收编为 fd。第五部分(进程间通信)的七种机制——管道/FIFO、信号全链路、SysV 三件套、POSIX mqueue、共享内存、mmap I/O、futex、fd 三件套——至此全部完成。