Linux内核分析之进程间通信-06
This language version is unavailable; showing the other language.
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 三件套——至此全部完成。