主从Reactor模式:epoll与多线程
在前面的第 27 讲和第 28 讲中,我们介绍了基于 poll 事件分发的 Reactor 反应堆模式以及主从 Reactor(Master-Slave Reactor)模式。与 poll 相比,Linux 提供的 epoll 是一种更为高效的事件分发机制。本文将切换到基于 epoll 实现的主从 Reactor 模式,并深入分析 epoll 相较于 poll 等传统事件分发机制的性能优势。
主从 Reactor 与 epoll 架构概览
在进入实现细节之前,先从架构层面理解主从 Reactor 模式与 epoll 的结合方式。
如图所示,主反应堆线程(Main Reactor)专注于监听套接字上的连接建立事件,而已连接套接字的 I/O 读写事件则由多个从反应堆线程(Sub-Reactor)负责处理。这种分工模式使得连接建立与数据处理互不阻塞,充分发挥多核 CPU 的并行处理能力。
如何切换到 epoll
所有代码已放置于 GitHub,可自行查看或下载。
该网络编程框架同时支持 poll 和 epoll 两种事件分发机制。切换至 epoll 的关键位于 lib/event_loop.c 文件中的 event_loop_init_with_name 函数,通过宏 EPOLL_ENABLE 控制选择哪种分发机制。
struct event_loop *event_loop_init_with_name(char *thread_name) {
...
#ifdef EPOLL_ENABLE
yolanda_msgx("set epoll as dispatcher, %s", eventLoop->thread_name);
eventLoop->eventDispatcher = &epoll_dispatcher;
#else
yolanda_msgx("set poll as dispatcher, %s", eventLoop->thread_name);
eventLoop->eventDispatcher = &poll_dispatcher;
#endif
eventLoop->event_dispatcher_data = eventLoop->eventDispatcher->init(eventLoop);
...
}在根目录的 CMakeLists.txt 中,通过 CheckSymbolExists 模块检测系统中是否存在 epoll_create 函数和 sys/epoll.h 头文件,自动决定是否开启 EPOLL_ENABLE 宏。
# check epoll and add config.h for the macro compilation
include(CheckSymbolExists)
check_symbol_exists(epoll_create "sys/epoll.h" EPOLL_EXISTS)
if (EPOLL_EXISTS)
# Linux 下设置为 epoll
set(EPOLL_ENABLE 1 CACHE INTERNAL "enable epoll")
# Linux 下也设置为 poll
# set(EPOLL_ENABLE "" CACHE INTERNAL "not enable epoll")
else ()
set(EPOLL_ENABLE "" CACHE INTERNAL "not enable epoll")
endif ()为使编译器识别该宏,需通过 configure_file 命令将宏值写入 config.h 文件(基于模板 config.h.cmake),并通过 include_directories 确保编译器能找到该头文件。
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/config.h.cmake
${CMAKE_CURRENT_BINARY_DIR}/include/config.h)
include_directories(${CMAKE_CURRENT_BINARY_DIR}/include)如此配置后,在 Linux 环境下默认使用 epoll 作为事件分发机制。
若需强制使用 poll(例如用于对比测试),可修改 CMakeLists.txt,将 Linux 分支下的 EPOLL_ENABLE 设为空:
# check epoll and add config.h for the macro compilation
include(CheckSymbolExists)
check_symbol_exists(epoll_create "sys/epoll.h" EPOLL_EXISTS)
if (EPOLL_EXISTS)
# Linux 下也设置为 poll
set(EPOLL_ENABLE "" CACHE INTERNAL "not enable epoll")
else ()
set(EPOLL_ENABLE "" CACHE INTERNAL "not enable epoll")
endif ()样例程序
样例程序与第 28 讲逻辑一致,仅底层事件分发机制从 poll 切换为 epoll。
#include <lib/acceptor.h>
#include "lib/common.h"
#include "lib/event_loop.h"
#include "lib/tcp_server.h"
char rot13_char(char c) {
if ((c >= 'a' && c <= 'm') || (c >= 'A' && c <= 'M'))
return c + 13;
else if ((c >= 'n' && c <= 'z') || (c >= 'N' && c <= 'Z'))
return c - 13;
else
return c;
}
// 连接建立之后的 callback
int onConnectionCompleted(struct tcp_connection *tcpConnection) {
printf("connection completed\n");
return 0;
}
// 数据读到 buffer 之后的 callback
int onMessage(struct buffer *input, struct tcp_connection *tcpConnection) {
printf("get message from tcp connection %s\n", tcpConnection->name);
printf("%s", input->data);
struct buffer *output = buffer_new();
int size = buffer_readable_size(input);
for (int i = 0; i < size; i++) {
buffer_append_char(output, rot13_char(buffer_read_char(input)));
}
tcp_connection_send_buffer(tcpConnection, output);
return 0;
}
// 数据通过 buffer 写完之后的 callback
int onWriteCompleted(struct tcp_connection *tcpConnection) {
printf("write completed\n");
return 0;
}
// 连接关闭之后的 callback
int onConnectionClosed(struct tcp_connection *tcpConnection) {
printf("connection closed\n");
return 0;
}
int main(int c, char **v) {
// 主线程 event_loop
struct event_loop *eventLoop = event_loop_init();
// 初始化 acceptor
struct acceptor *acceptor = acceptor_init(SERV_PORT);
// 初始化 tcp_server,线程数为 4,表示 1 个 acceptor 线程 + 4 个 I/O 线程
// 每个 I/O 线程自带一个 event_loop
struct TCPserver *tcpServer = tcp_server_init(eventLoop, acceptor, onConnectionCompleted, onMessage,
onWriteCompleted, onConnectionClosed, 4);
tcp_server_start(tcpServer);
// main thread for acceptor
event_loop_run(eventLoop);
}缓冲区对象(Buffer)的设计意义
此前未详细展开的部分是缓冲区对象 buffer,这也是网络编程框架应考虑的核心组件。
框架的目标是对应用程序封装套接字的读写细节,转而提供基于 buffer 对象的读写操作。从套接字接收数据、处理异常、发送数据等操作均由 buffer 对象封装和屏蔽,应用程序仅需从 buffer 中获取已接收的字节流进行应用层处理,例如通过 buffer_read_char 逐字节读取。
同理,框架也必须提供基于 buffer 的套接字发送接口:应用程序先生成 buffer 对象,将编码后的数据填入,再调用 tcp_connection_send_buffer 将 buffer 中的数据通过套接字发送。
回调函数与线程模型
onMessage、onConnectionClosed 等回调函数运行在 Sub-Reactor 线程中,即生成 buffer 对象和执行 encode 的代码均在 Sub-Reactor 线程中执行。回调函数本身仅提供 Handler 处理逻辑,具体执行由事件分发线程(event loop 线程)发起。
框架通过一层抽象,使应用程序开发者只需关注回调函数,回调函数中的对象也是 buffer 和 tcp_connection 等封装后的对象,套接字、字节流等底层实现细节完全由框架处理。
样例程序运行结果
启动服务器后,屏幕输出显示已使用 epoll 作为事件分发器:
$./epoll-server-multithreads
[msg] set epoll as dispatcher, main thread
[msg] add channel fd == 5, main thread
[msg] set epoll as dispatcher, Thread-1
[msg] add channel fd == 9, Thread-1
[msg] event loop thread init and signal, Thread-1
[msg] event loop run, Thread-1
[msg] event loop thread started, Thread-1
[msg] set epoll as dispatcher, Thread-2
[msg] add channel fd == 12, Thread-2
[msg] event loop thread init and signal, Thread-2
[msg] event loop run, Thread-2
[msg] event loop thread started, Thread-2
[msg] set epoll as dispatcher, Thread-3
[msg] add channel fd == 15, Thread-3
[msg] event loop thread init and signal, Thread-3
[msg] event loop run, Thread-3
[msg] event loop thread started, Thread-3
[msg] set epoll as dispatcher, Thread-4
[msg] add channel fd == 18, Thread-4
[msg] event loop thread init and signal, Thread-4
[msg] event loop run, Thread-4
[msg] event loop thread started, Thread-4
[msg] add channel fd == 6, main thread
[msg] event loop run, main thread开启多个 telnet 客户端连接服务器并进行交互:
$telnet 127.0.0.1 43211
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
fafaf
snsns
^]
telnet> quit
Connection closed.服务端输出显示 epoll_wait 不断返回处理 I/O 事件。其中主线程的 epoll_wait 仅处理 acceptor 套接字的事件(连接建立),Sub-Reactor 线程的 epoll_wait 处理已连接套接字的读写事件。

epoll 性能分析
epoll 的性能优势可从以下两个维度深入分析。
维度一:事件集合管理
每次使用 poll 或 select 之前,都需要准备一个感兴趣的事件集合,系统内核拿到该集合后在内核空间构建相应的数据结构完成注册。而 epoll 维护了一个全局的事件集合(通过红黑树实现),通过 epoll 句柄可对该集合进行增加、删除或修改操作。在绝大多数场景下,事件集合的变化幅度有限,epoll 无需每次重新扫描和构建内核数据结构,显著减少了内核与用户空间之间的数据拷贝和内存分配开销。
维度二:就绪列表(Ready List)
每次调用 poll 或 select 后,应用程序需要扫描整个感兴趣的事件集合,从中找出真正活跃的事件。当该列表增长到 10K 以上时,每次扫描的时间损耗极为可观,而实际活跃事件可能仅有寥寥数个。epoll 则直接返回活跃事件列表,应用程序免除了大量无效扫描的时间开销。
边缘触发(Edge-Triggered)与条件触发(Level-Triggered)
epoll 还提供了更高级的能力——边缘触发(Edge-Triggered,ET)。第 23 讲通过直观的示例讲解了边缘触发和条件触发的区别。
以下通过一个具体场景进一步说明:
- 若某个套接字有 100 字节可读,边缘触发和条件触发都会产生 read ready notification 事件。
- 若应用程序只读取了 50 字节:
- 边缘触发(ET):不再产生新的事件通知,直到有新的数据到达。
- 条件触发(LT):因为还有 50 字节未读取,会持续产生 read ready notification 事件。
在条件触发模式下,若某个套接字缓冲区可写,会无限次返回 write ready notification 事件。若应用程序未准备好发送数据,必须解除该套接字上的 ready notification 事件注册,否则将导致 CPU 空转。
内核版本注记:epoll 的 ET 和 LT 模式自 Linux 2.5.44 引入,在 Linux 7.0 中行为保持不变。
EPOLLEXCLUSIVE标志自 Linux 4.5 引入,用于避免惊群效应(thundering herd)。
性能对比总结
| 特性 | poll / select | epoll |
|---|---|---|
| 事件集合管理 | 每次调用传入完整集合 | 红黑树持久维护 |
| 内核数据结构构建 | 每次调用重新构建 | 增量更新 |
| 就绪事件返回 | 返回全部集合,应用自行过滤 | 仅返回活跃事件 |
| 时间复杂度 | O(n) | O(1)(活跃事件数) |
| 最大 fd 数限制 | 受 FD_SETSIZE 限制(select) | 无硬性限制 |
| 触发模式 | 仅条件触发(LT) | 支持 LT 和 ET |
| 适用场景 | 少量连接 | 大量并发连接(C10K+) |
总结
本文将程序框架从 poll 切换至 epoll 版本。与 poll 版本相比,仅底层框架进行了更改,上层应用程序无需任何修改——这正是框架抽象的价值所在。与 poll 相比,epoll 从事件集合管理和就绪列表两个维度提升了程序性能,是 Linux 下高性能网络程序的首选事件分发机制。
结合多线程的主从 Reactor 模式,epoll 实现了连接建立与 I/O 处理的分离,充分发挥了多核 CPU 的并行处理能力,为构建高性能网络服务器奠定了基础。
思考题
- 阐述对边缘触发(Edge-Triggered)和条件触发(Level-Triggered)工作原理的理解,以及在何种场景下应选择哪种模式。
- 对于边缘触发和条件触发两种模式,
onMessage回调函数的处理逻辑需要注意哪些差异?
版本信息
- 更新日期:2026-06-09
- 目标内核:Linux 7.0
- 关键 API 版本:epoll 自 Linux 2.5.44 引入;
EPOLLEXCLUSIVE自 Linux 4.5 引入;epoll_create1自 Linux 2.6.27 引入