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

相关推荐
Full Stack Developme38 分钟前
Linux 多种压缩格式,优缺点和适用场景
linux·运维·服务器
旖旎夜光41 分钟前
Linux(4)(下)
linux·学习
敲敲了个代码4 小时前
从硬编码到 Schema 推断:前端表单开发的工程化转型
前端·javascript·vue.js·学习·面试·职场和发展·前端框架
TG:@yunlaoda360 云老大5 小时前
华为云国际站代理商GeminiDB的企业级高可用具体是如何实现的?
服务器·网络·数据库·华为云
Shanxun Liao5 小时前
Cenots 7.9 配置多台 SSH 互信登陆免密码
linux·运维·ssh
j_xxx404_5 小时前
Linux:第一个程序--进度条|区分回车与换行|行缓冲区|进度条代码两个版本|代码测试与优化
linux·运维·服务器
looking_for__6 小时前
【Linux】Ext系列文件系统
linux
Morwit6 小时前
【力扣hot100】64. 最小路径和
c++·算法·leetcode
我命由我123456 小时前
SVG - SVG 引入(SVG 概述、SVG 基本使用、SVG 使用 CSS、SVG 使用 JavaScript、SVG 实例实操)
开发语言·前端·javascript·css·学习·ecmascript·学习方法
OliverH-yishuihan6 小时前
开发linux项目-在 Windows 上 基于“适用于 Linux 的 Windows 子系统(WSL)”
linux·c++·windows