在写基于 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 架构)中最关键的底层组件之一。