eventfd 初认识Reactor/多线程服务器的关键唤醒机制

在写基于 epoll 的 Reactor 框架时,一个非常常见的需求是:

  • 主线程在 epoll_wait() 阻塞

  • 其它线程(如线程池 worker)想通知 Reactor 做一些事情

  • 比如:

    • 修改 fd 事件

    • 添加新的 fd

    • 关闭连接

    • 让 loop 安全退出

但问题是:

epoll_wait() 是阻塞的,不会自动醒来!

如果你不唤醒它,Reactor 主线程就会被永久卡住。

这时 eventfd 就是最优雅、最标准、最"Linux-style" 的解决方案。

一、什么是 eventfd?

eventfd 是 Linux 内核提供的 轻量级事件通知机制。

它创建的是一个 特殊的 fd(文件描述符),这个 fd:

  • 内核里维护一个 64 位无符号计数器

  • 程序可以通过 read() / write() 读写这个计数器

  • 对 epoll 来说,它表现得和普通 fd 一样,也能触发 EPOLLIN

总结:

eventfd = 一个"可以放数字进去并能让 epoll 感知变化"的内核计数器。

二、eventfd 的创建方式

cpp 复制代码
#include <sys/eventfd.h>

int evfd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);

拆解一下:

1) 初始值:0

eventfd 内核计数器的初始值。

2) EFD_NONBLOCK

让 eventfd 的 read/write 都是非阻塞的,避免:

  • 写满阻塞

  • 读空阻塞

3) EFD_CLOEXEC

和 epoll 一样,防止 exec() 时 fd 泄露。

一般写法:

cpp 复制代码
eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC)

三、eventfd 怎样配合 epoll 使用?

关键步骤只有两步:

① 把 eventfd 加入 epoll

cpp 复制代码
epoll_event ev{};
ev.events = EPOLLIN;    // 不需要 ET
ev.data.fd = evfd;      // 我们只传 fd
epoll_ctl(epfd, EPOLL_CTL_ADD, evfd, &ev);

现在 epoll_wait 会监控它。

② 其它线程每次想唤醒 epoll → write() 一下

cpp 复制代码
uint64_t one = 1;
write(evfd, &one, sizeof(one));

这会导致:

  • eventfd 的内核计数器 +1

  • eventfd 变为"可读"

  • epoll_wait 立刻返回

Reactor 主线程就被成功唤醒。

四、主线程醒来后怎么处理 eventfd?

在 Reactor.loop() 里,看到的是这一段逻辑:

cpp 复制代码
if (fd == evfd_) {
    DrainEventfd(evfd_);
    continue;
}

为什么要 Drain(读空)?

因为 eventfd 是计数器:

  • 写一次 = 计数器 +1

  • 写多次 = 计数器 +N

如果你不读掉内核里的数字,它会保持可读状态,导致:

epoll_wait 会疯狂立即返回 → loop 空转占满 100% CPU

因此正确做法是:

cpp 复制代码
uint64_t cnt;
for (;;) {
    ssize_t n = read(evfd, &cnt, sizeof(cnt));
    if (n < 0 && errno == EAGAIN) break;  // 读空了
}

五、eventfd 解决了什么具体问题?

1. epoll_wait 的阻塞问题

Reactor 主线程在这里阻塞:

cpp 复制代码
epoll_wait(epfd, evlist, max, -1);

如果其它线程想让 Reactor:

  • 增加/删除/修改 fd

  • 发送数据

  • 关闭连接

  • 退出 loop

主线程需要立刻醒过来处理。

eventfd + epoll 就能做到。

2. 线程安全地修改 epoll

epoll_ctl 不是 thread-safe 的:

多个线程同时对 epoll_ctl 操作可能引发 race condition

解决方案:

所有 epoll_ctl 统一放到 Reactor 主线程执行。

其它线程要发请求 → 写 eventfd → 唤醒 Reactor → Reactor 统一执行。

3. 实现退出 Reactor

你的 Reactor.stop():

cpp 复制代码
if (running_.compare_exchange_strong(expected, false)) {
    wakeup(); // 写 eventfd,唤醒 epoll_wait
}

流程:

cpp 复制代码
stop()  → wakeup() → write(eventfd) → epoll_wait 返回 → loop 检查 running_ → 退出

这是专业网络框架的统一做法。

六、为什么不用 pipe()?

pipe(2) 也能实现跨线程唤醒,但 eventfd 的优势是:

  • 更轻量(只有一个 8 字节计数器)

  • 不需要一次写一个字节(pipe 会写满阻塞)

  • 不需要"读多少写多少"

  • 性能更高

  • 不会在 poll/epoll 中出现不必要的边缘触发混乱

  • 内核专门为 event 机制优化过

因此:

现代 Linux 网络服务器首选 eventfd,不用 pipe 去唤醒 epoll。

七、eventfd 在 Reactor 中的最终作用(总结)

把所有事串起来:

cpp 复制代码
Worker Thread(其它线程)
    ↓ write(eventfd)
eventfd 计数器+1
    ↓
epoll_wait 立刻醒来
    ↓
Reactor.loop 检查事件
    ↓
发现 eventfd 可读
    ↓
DrainEventfd 读空
    ↓
接着处理你唤醒它的任务

简单说:

eventfd = Reactor 的"唤醒器"

任何想让 Reactor 主线程立刻处理任务的线程 → write() 即可。

八、总结

eventfd 的作用就是:

提供一种能通过 write() 唤醒 epoll_wait 的轻量级跨线程事件通知机制。

它是现在网络服务器(Reactor + ThreadPool 架构)中最关键的底层组件之一。

相关推荐
西安同步电子12 分钟前
时统卡驱动踩坑:Windows 和 Linux 下时间同步的底层差异
linux·stm32·单片机
虚无的纽扣12 分钟前
【Linux】线程互斥:多线程改同一个变量,到底会出什么问题
linux·服务器·1024程序员节
qetfw23 分钟前
Bash 函数与流程控制:read、if、for、case 验证
linux·前端·javascript·bash
m0_5474866624 分钟前
《Linux操作系统基本原理与应用》全套PPT课件2026
linux·linux系统
Shadow(⊙o⊙)1 小时前
LangChain 自定义工具全解析:从 @tool 到 bind_tools 的完整调用链路
java·服务器·前端·1024程序员节
Pointer Pursuit1 小时前
Linux环境变量
linux·服务器·c语言·链表·1024程序员节
破芽1 小时前
learn-claude-code-s08-s09-学习笔记
笔记·学习
星恒随风1 小时前
Linux网络编程基础(二):彻底理解局域网通信、MAC与IP地址、数据封装、端口号、网络字节序及Socket接口
linux·网络·笔记·学习·tcp/ip·1024程序员节
Marst Code1 小时前
(Unix)pipe 把程序当成可组合的函数
服务器·unix
CHHH_HHH2 小时前
【Linux网络篇】初识网络与Socket编程基础
linux·服务器·c语言·开发语言·网络·后端·1024程序员节