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

相关推荐
磊〜1 小时前
Linux 服务器安装 dstat 监控插件
linux·运维·服务器
无聊的小坏坏1 小时前
从 OneThreadOneLoop 线程池到进程池:高性能 Reactor 服务器的演进
服务器·网络·一个进程一个事件循环
Csxyzj1 小时前
nginx
服务器·nginx
二进制coder1 小时前
服务器BMC开发视角:解析CPU管理的两大核心接口PECI与APML
运维·服务器·网络
大连滚呢王1 小时前
Linux(麒麟)服务器离线安装单机Milvus向量库
linux·python·milvus·银河麒麟·milvus_cli
南方的狮子先生1 小时前
【C++】C++文件读写
java·开发语言·数据结构·c++·算法·1024程序员节
嗝屁小孩纸1 小时前
免费测评RPC分布式博客平台(仅用云服务器支持高性能)
服务器·分布式·rpc
宋辰月1 小时前
学习react第三天
前端·学习·react.js
玖剹2 小时前
二叉树递归题目(一)
c语言·c++·算法·leetcode