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

相关推荐
肆忆_7 小时前
# 用 5 个问题学懂 C++ 虚函数(入门级)
c++
chlk12310 小时前
Linux文件权限完全图解:读懂 ls -l 和 chmod 755 背后的秘密
linux·操作系统
舒一笑10 小时前
Ubuntu系统安装CodeX出现问题
linux·后端
改一下配置文件11 小时前
Ubuntu24.04安装NVIDIA驱动完整指南(含Secure Boot解决方案)
linux
不想写代码的星星11 小时前
虚函数表:C++ 多态背后的那个男人
c++
深紫色的三北六号21 小时前
Linux 服务器磁盘扩容与目录迁移:rsync + bind mount 实现服务无感迁移(无需修改配置)
linux·扩容·服务迁移
SudosuBash1 天前
[CS:APP 3e] 关于对 第 12 章 读/写者的一点思考和题解 (作业 12.19,12.20,12.21)
linux·并发·操作系统(os)
哈基咪怎么可能是AI1 天前
为什么我就想要「线性历史 + Signed Commits」GitHub 却把我当猴耍 🤬🎙️
linux·github
十日十行2 天前
Linux和window共享文件夹
linux
端平入洛2 天前
delete又未完全delete
c++