Linux epoll 深度实战:从 select/poll 到百万级并发的事件驱动模型
一、为什么需要 epoll?—— select 与 poll 的历史局限
在网络编程的演进历程中,I/O 多路复用技术始终是构建高性能服务器的基石。在 epoll 出现之前,select 和 poll 是主流的事件通知机制,但它们的设计局限性严重制约了系统的并发能力。
1.1 select 的三大瓶颈
select 函数诞生于 1983 年(BSD 4.2),其接口定义如下:
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
select 存在三个核心问题:
第一,fd_set 的大小受限。fd_set 本质是一个由 FD_SETSIZE(通常 1024)位组成的位图,这意味着 select 能监控的文件描述符数量被硬编码限制在 1024。在 C10K 问题面前,这是致命缺陷。
第二,每次调用必须重置 fd_set。select 会修改传入的 fd_set 指针内容,因此每次调用前都需要重新设置关心的事件集合,这对于高频循环调用是额外的开销。
第三,O(n) 的扫描复杂度。select 返回后,调用方必须遍历整个 fd_set 来确定哪些 fd 就绪,当 fd 数量很大但有事件的比例很低时,这种轮询扫描极其低效。
1.2 poll 的改进与不足
poll 用动态数组代替位图,解决了 fd 数量限制:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
struct pollfd {
int fd; /* 文件描述符 */
short events; /* 请求事件 */
short revents; /* 返回事件 */
};
但 poll 仍然继承了 select 的 O(n) 扫描问题——内核和用户态都需要遍历整个 fd 数组来查找就绪的 fd。当并发连接达到数万级别时,这个线性扫描的开销不可接受。
1.3 C10K 问题与 epoll 的诞生
1999年,Dan Kegel 提出了著名的"C10K 问题":如何让单台服务器同时处理 10,000 个并发连接?在 epoll 之前,解决方案要么是使用多进程/多线程模型(Apache prefork),要么是用户态异步框架(这些都存在各自的局限)。
Linux 2.5.44(2002年)引入 epoll,在内核层面用红黑树管理 fd 集合,用就绪链表直接返回就绪事件,实现了 O(1) 的事件通知效率(相对于监控的 fd 总数而言),一举解决了选择问题。
二、epoll 核心 API 与数据结构
2.1 三个系统调用
epoll 的 API 极其精简,只有三个系统调用:
// 1. 创建 epoll 实例,返回 epfd
int epoll_create(int size); // 旧版(Linux 2.6.8后忽略size)
int epoll_create1(int flags); // 新版(支持 EPOLL_CLOEXEC)
// 2. 注册/修改/删除监控事件
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// 3. 等待就绪事件
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
2.2 epoll_event 结构体与事件类型
struct epoll_event 是 epoll 的核心数据结构:
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; /* epoll 事件位掩码 */
epoll_data_t data; /* 用户数据传递 */
};
常用事件类型说明:
| 事件宏 | 触发条件 | 典型用途 |
|---|---|---|
| EPOLLIN | 对端有数据可读 | 读取 TCP 数据、UDP 数据报 |
| EPOLLOUT | 对端可写(发送缓冲区未满) | 发送数据、写回调 |
| EPOLLRDHUP | 对端关闭连接(半关闭) | 优雅关闭连接 |
| EPOLLET | 边缘触发模式(Edge Triggered) | 高吞吐场景,减少事件触发次数 |
| EPOLLONESHOT | 事件触发后自动禁用 fd | 多线程下避免多个线程同时处理同一 fd |
| EPOLLERR | 错误发生(总是监控) | 连接异常处理 |
| EPOLLHUP | 挂起(总是监控) | 对端 RST 或异常断开 |
epoll_ctl 操作码详解
// 注册新 fd 到 epoll 实例
epoll_ctl(epfd, EPOLL_CTL_ADD, new_fd, &event);
// 修改已注册 fd 的关注事件
epoll_ctl(epfd, EPOLL_CTL_MOD, modify_fd, &event);
// 从 epoll 实例移除 fd(关闭 fd 自动移除)
epoll_ctl(epfd, EPOLL_CTL_DEL, remove_fd, NULL);
三、底层实现原理:红黑树 + 就绪链表
3.1 内核数据结构总览
epoll 的内核实现基于三个核心数据结构:
// 每个监控的 fd 对应一个 epitem(红黑树节点)
struct epitem {
struct rb_node rbn; // 红黑树节点(以 fd 为键)
struct list_head rdllink; // 就绪链表节点
struct epoll_filefd ffd; // fd 和 file 指针
struct eventpoll *ep; // 所属 epoll 实例
struct epoll_event event; // 注册的事件掩码
};
// epoll 实例(每个 epoll_create 创建一个)
struct eventpoll {
struct mutex mtx; // 操作锁
wait_queue_head_t wq; // 进程等待队列(epoll_wait 阻塞用)
struct list_head rdllist; // 就绪链表
struct rb_root rbr; // 红黑树根节点
struct epoll_filefd *ovflist; // 溢出链表(优化路径)
};
3.2 事件注册路径
当调用 epoll_ctl(EPOLL_CTL_ADD) 时内核执行以下步骤:
步骤一:分配 epitem 对象,初始化红黑树节点和就绪链表节点。
步骤二:调用 ep_install(),将 epitem 插入到 eventpoll 的红黑树中(O(log n) 插入)。
步骤三:通过 poll_table 调用底层 file_operations 的 poll 方法,注册回调函数 ep_poll_callback()注册到目标 fd 设备的等待队列中。
关键的是,此时不会阻塞,只是建立了 epoll 实例与 fd 之间的回调关系。
3.3 事件触发与就绪链表
当 fd 就绪(例如网卡收到数据触发中断)时,中断处理程序会:
步骤一:唤醒该 fd 等待队列上的所有回调函数,执行 ep_poll_callback()。
步骤二:ep_poll_callback() 将对应的 epitem 加入 eventpoll 的 rdllist 就绪链表。
步骤三:检查 eventpoll 的 wq 等待队列是否有进程在阻塞等待(即有进程在调用 epoll_wait),如果有则唤醒它。
这一过程完全由内核驱动,不需要主动扫描所有 fd。这意味着不论监控 100 个 fd 还是 10 万个 fd,事件触发路径的开销都是 O(1)。
3.4 就绪事件返回(epoll_wait)
当 epoll_wait 被调用时:
步骤一:检查 rdllist 是否为空,若为空则将当前进程加入 wq 等待队列并设置状态为 TASK_INTERRUPTIBLE,然后调度出去(阻塞等待)。
步骤二:当被唤醒(有事件到达或超时),从 rdllist 取出就绪的 epitem,将其 event 数组拷贝回用户空间。
步骤三:关键优化——对于水平触发(LT)模式但就绪 fd 不支持 WQ_FLAG_EXCLUSIVE 的,epitem 会重新挂回 rdllist 以支持下次通知。ET 模式的则直接从链表中删除。
另外,内核还有一个 ovflist 溢出链表优化:如果在 epoll_wait 拷贝就绪事件到用户空间期间有新事件到来,为避免丢失,新事件暂存到 ovflist,处理完后再转移回 rdllist。
四、水平触发 vs 边缘触发
4.1 水平触发 Level Triggered(LT,默认模式)
LT 是 epoll 的默认工作模式。当 fd 处于就绪状态时,epoll_wait 每次调用都会通知用户。这类似于 select/poll 的行为。
LT 的特点:
编程模型简单,不用担心事件遗漏。只要缓冲区还有数据没读完,下次 epoll_wait 还会通知你。但每次通知都需要读写操作,在高并发场景下意味着更多的系统调用。
LT 模式的典型服务端代码模式:
// LT 模式:epoll_wait 返回后读取直到 EAGAIN
while (1) {
int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
for (int i = 0; i < n; i++) {
if (events[i].events & EPOLLIN) {
// 只需一次 read 调用,没读完 LT 会再次通知
ssize_t count = read(events[i].data.fd, buf, BUF_SIZE);
if (count == -1 && errno == EAGAIN) {
// 缓冲区已空,退出
break;
}
// 处理 buf 中的数据...
}
}
}
在 LT 模式下,如果只读了一部分数据就停止,下次 epoll_wait 会继续通知事件就绪,所以不需要一次性读完。
4.2 边缘触发 Edge Triggered(ET)
ET 模式通过 EPOLLET 标志位设置,它只在 fd 状态变化时通知一次(边沿触发)。这种模式要求用户必须一次性处理完所有可用数据,否则会丢失事件。
ET 的特点:
事件触发次数大幅减少,吞吐能力显著提升。但编程复杂度更高,必须使用非阻塞 IO,且必须循环读写直到返回 EAGAIN 错误。
ET 模式要求 fd 必须设置为非阻塞模式:
// 设置非阻塞模式
int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
// 注册时添加 EPOLLET 标志
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
ET 模式的典型服务端读处理:
// ET 模式:必须循环 read 直到 EAGAIN
if (events[i].events & EPOLLIN) {
while (1) {
ssize_t count = read(fd, buf, BUF_SIZE);
if (count == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// 数据全部读完,退出循环
break;
}
// 真正的错误
close(fd);
break;
} else if (count == 0) {
// 对端关闭连接
close(fd);
break;
}
// 处理 buf 中的 count 字节数据
process_data(buf, count);
}
}
4.3 LT vs ET 性能对比
在典型的 HTTP 服务器场景下:
LT 模式:每次 epoll_wait 返回后只读一次,有大量数据时可能触发多次 epoll_wait 调用,系统调用开销增加。
ET 模式:每次触达时一次性读完,减少了 epoll_wait 调用次数,在 100K+ 并发场景下吞吐量可提升 20%-30%。
但 ET 模式需要注意不能阻塞——如果 ET 模式下使用阻塞 IO 且数据量大于一次 read 能读的量,会导致后续数据永远丢失(因为 ET 只通知一次)。
五、完整 epoll HTTP 服务器实战
5.1 架构设计
下面我们将构建一个基于 epoll + 线程池的 HTTP 服务器,架构如下:
主线程:创建 epoll 实例,accept 新连接,注册到 epoll;epoll_wait 等待事件。
工作线程:从主线程获取就绪的连接句柄,解析 HTTP 请求,生成响应,写回客户端。
5.2 事件驱动核心循环
#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#define MAX_EVENTS 65536
#define BUF_SIZE 8192
#define PORT 8080
static void set_nonblocking(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}
static int create_server(uint16_t port) {
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd == -1) {
perror("socket");
exit(EXIT_FAILURE);
}
int opt = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons(port),
.sin_addr.s_addr = INADDR_ANY
};
if (bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
perror("bind");
exit(EXIT_FAILURE);
}
// 重要:listen 的 backlog 应足够大
if (listen(listen_fd, 4096) == -1) {
perror("listen");
exit(EXIT_FAILURE);
}
set_nonblocking(listen_fd);
return listen_fd;
}
// 客户端连接上下文
struct client {
int fd;
char read_buf[BUF_SIZE];
int read_offset;
char write_buf[BUF_SIZE];
int write_len;
int write_offset;
int keep_alive;
};
static struct client *new_client(int fd) {
struct client *c = calloc(1, sizeof(struct client));
c->fd = fd;
c->keep_alive = 1;
return c;
}
static void close_client(int epfd, struct client *c) {
epoll_ctl(epfd, EPOLL_CTL_DEL, c->fd, NULL);
close(c->fd);
free(c);
}
// HTTP 请求解析与响应生成
static int handle_request(struct client *c) {
// 简单解析 HTTP 请求行
char *method = c->read_buf;
char *path = strchr(method, ' ');
if (!path) return -1;
*path++ = 0;
char *version = strchr(path, ' ');
if (!version) return -1;
*version++ = 0;
// 检查 Connection: keep-alive
c->keep_alive = (strstr(version, "Connection: keep-alive") != NULL);
// 构造 HTTP/1.1 200 响应
const char *body = "Hello from epoll server!";
c->write_len = snprintf(c->write_buf, BUF_SIZE,
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/plain\r\n"
"Content-Length: %zu\r\n"
"Connection: %s\r\n"
"\r\n"
"%s",
strlen(body),
c->keep_alive ? "keep-alive" : "close",
body);
c->write_offset = 0;
return 0;
}
int main(void) {
int listen_fd = create_server(PORT);
int epfd = epoll_create1(EPOLL_CLOEXEC);
if (epfd == -1) {
perror("epoll_create1");
exit(EXIT_FAILURE);
}
// 注册 listen_fd 到 epoll
struct epoll_event ev = {
.events = EPOLLIN,
.data.ptr = new_client(listen_fd) // 用 ptr 区分客户端和监听 fd
};
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev);
struct epoll_event events[MAX_EVENTS];
printf("epoll HTTP server running on port %d\n", PORT);
while (1) {
int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);
if (nfds == -1) {
if (errno == EINTR) continue;
perror("epoll_wait");
break;
}
for (int i = 0; i < nfds; i++) {
struct client *c = events[i].data.ptr;
// 监听 fd:接受新连接
if (c->fd == listen_fd) {
while (1) {
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);
int conn_fd = accept4(listen_fd,
(struct sockaddr*)&client_addr, &addr_len,
SOCK_NONBLOCK | SOCK_CLOEXEC);
if (conn_fd == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
break; // 没有更多连接了
}
perror("accept4");
break;
}
struct client *new_c = new_client(conn_fd);
struct epoll_event cev = {
.events = EPOLLIN | EPOLLRDHUP | EPOLLERR,
.data.ptr = new_c
};
epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, &cev);
}
continue;
}
// 错误处理
if (events[i].events & (EPOLLERR | EPOLLHUP)) {
close_client(epfd, c);
continue;
}
// 可读事件
if (events[i].events & EPOLLIN) {
ssize_t n = read(c->fd,
c->read_buf + c->read_offset,
BUF_SIZE - c->read_offset - 1);
if (n == -1) {
if (errno != EAGAIN && errno != EWOULDBLOCK) {
close_client(epfd, c);
}
continue;
} else if (n == 0) {
close_client(epfd, c);
continue;
}
c->read_offset += n;
c->read_buf[c->read_offset] = 0;
// 检查请求是否完整(简单判断 \r\n\r\n)
if (strstr(c->read_buf, "\r\n\r\n")) {
if (handle_request(c) == 0) {
// 切换到写关注
struct epoll_event wev = {
.events = EPOLLOUT | EPOLLRDHUP | EPOLLERR | EPOLLET,
.data.ptr = c
};
epoll_ctl(epfd, EPOLL_CTL_MOD, c->fd, &wev);
} else {
close_client(epfd, c);
}
}
continue;
}
// 可写事件
if (events[i].events & EPOLLOUT) {
while (c->write_offset < c->write_len) {
ssize_t n = write(c->fd,
c->write_buf + c->write_offset,
c->write_len - c->write_offset);
if (n == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
break; // 缓冲区满,下次再写
}
close_client(epfd, c);
goto next_event;
}
c->write_offset += n;
}
if (c->write_offset == c->write_len) {
if (c->keep_alive) {
// 重置状态,等待下一个请求
c->read_offset = 0;
c->write_offset = 0;
c->write_len = 0;
struct epoll_event rev = {
.events = EPOLLIN | EPOLLRDHUP | EPOLLERR | EPOLLET,
.data.ptr = c
};
epoll_ctl(epfd, EPOLL_CTL_MOD, c->fd, &rev);
} else {
close_client(epfd, c);
}
}
}
next_event:;
}
}
close(epfd);
close(listen_fd);
return 0;
}
编译运行:
gcc -O2 -Wall -o epoll_server epoll_server.c
./epoll_server
# 测试
curl http://localhost:8080/
六、epoll 的陷阱与最佳实践
6.1 文件描述符耗尽问题
Linux 对进程可打开的 fd 数量有三层限制:
系统级:fs.file-max(全局 fd 总数上限,通过 /proc/sys/fs/file-max 查看和修改)。
用户级:ulimit -n(单进程最大 fd 数)。在 systemd 服务中通过 LimitNOFILE= 配置。
调度级:fs.nr_open(单个进程硬限制)。使用 epoll 做 C10K/C100K 服务器时,必须将这些限制调高:
# 临时修改
ulimit -n 100000
# 永久修改 /etc/security/limits.conf
* soft nofile 100000
* hard nofile 500000
# 系统级调整
sysctl -w fs.file-max=2000000
sysctl -w fs.nr_open=2000000
6.2 惊群效应(Thundering Herd)
多线程 epoll 场景下的经典问题:多个线程阻塞在同一个 epoll 实例的 epoll_wait 上,当一个事件到来时,所有线程被唤醒,但实际上只有一个线程能处理该事件。大量不必要的唤醒会导致 CPU 浪费和缓存抖动。
解决方案一:EPOLLEXCLUSIVE 标志(Linux 4.5+)。这个标志使内核在唤醒时只唤醒一个线程,相当于给 epoll 添加了单播唤醒语义:
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLEXCLUSIVE;
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
解决方案二:SO_REUSEPORT 多监听套接字。多个线程分别创建独立 socket,绑定同一端口(需要开启 SO_REUSEPORT),每个线程独立 accept,内核负责均衡分发新连接。这是 Nginx 等高性能服务器采用的方案。
6.3 连接丢失问题(ET 模式必读)
ET 模式下最容易犯的错误不是"忘记循环 read 到 EAGAIN",而是忽略了 accept 循环。在 ET 模式下,如果监听 fd 上有多个新连接到来,内核可能只通知一次。因此 accept 时必须在循环中调用,直到返回 EAGAIN:
// 错误:只 accept 一次
int conn_fd = accept(listen_fd, ...); // 第一个连接
// 第二个连接永远不会被处理!
// 正确:循环 accept 直到 EAGAIN
while (1) {
int conn_fd = accept4(listen_fd, ..., SOCK_NONBLOCK);
if (conn_fd == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
break; // 没有更多连接
}
perror("accept4");
break;
}
// 处理新连接...
}
6.4 EPOLLONESHOT 与多线程竞态
在多线程 epoll 服务器中,如果一个 fd 的就绪事件被添加到就绪链表后,多个工作线程可能同时被唤醒并处理同一个 fd,造成数据混淆或崩溃。EPOLLONESHOT(Linux 2.6.2+)解决这个问题:事件触发后自动禁用该 fd,处理完成后需要手动 EPOLL_CTL_MOD 重新启用。
// 注册时添加 EPOLLONESHOT
ev.events = EPOLLIN | EPOLLONESHOT | EPOLLET;
// 工作线程处理完毕后必须重新 arm
struct epoll_event re_ev;
re_ev.events = EPOLLIN | EPOLLONESHOT | EPOLLET;
re_evdata.ptr = c;
epoll_ctl(epfd, EPOLL_CTL_MOD, c->fd, &re_ev);
七、epoll 与 io_uring 的对比
Linux 5.1 引入的 io_uring 是新一代异步 I/O 接口,与 epoll 在设计哲学上有本质区别。
epoll 本质上是一个异步事件通知机制,它告诉你哪个 fd 可读了、可写了,但真正的 I/O 操作(read/write)仍然由用户线程执行。
io_uring 则是一个真正的异步 I/O 执行机制,read/write 等 I/O 操作被提交到共享队列后即返回,由内核异步完成,用户只需检查完成状态。
关键区别在于:
系统调用开销:epoll 每次 I/O 仍需用户态发起 read/write 系统调用;io_uring 在 SQPOLL 模式下可做到零系统调用(内核轮询提交队列自动执行)。
数据拷贝路径:epoll + read/write 需要内核态与用户态之间拷贝;io_uring 支持预注册缓冲区,支持真正零拷贝。
适用场景:epoll 更适合事件驱动的网络服务器(频繁的连接管理,数据量相对小);io_uring 更适合存储密集型工作负载(大文件 I/O、大量随机读写、NVMe 设备)。
实践建议:两者并非替代关系,生产中可以结合使用——用 epoll 管理连接生命周期,用 io_uring 处理大块数据读写。Cloudflare、Nginx(实验分支)都有此类实践。
八、性能调优与监控
8.1 epoll 相关的内核参数
| 参数路径 | 默认值 | 说明 |
|---|---|---|
| /proc/sys/fs/epoll/max_user_watches | 约内存的 80% / 每 fd 占用字节 | 单用户可注册的 epoll watch 总数 |
| /proc/sys/fs/file-max | 依赖内存 | 系统级 fd 最大数 |
| /proc/sys/net/core/somaxconn | 4096(新版 65535) | listen backlog 上限 |
| /proc/sys/net/ipv4/tcp_max_syn_backlog | 256 | TCP 半连接队列上限 |
max_user_watches 是关键参数。在监控大量 fd 的场景(如监控文件系统的 inotify + epoll):
# 查看当前值
cat /proc/sys/fs/epoll/max_user_watches
# 调整为 500 万
sysctl -w fs.epoll.max_user_watches=5000000
# 永久生效
echo "fs.epoll.max_user_watches = 5000000" >> /etc/sysctl.conf
8.2 epoll 调优清单
listen backlog 调大。默认 somaxconn 可能只有 4096,对于高并发服务器需要调到 65535。
使用 TCP_NODELAY 关闭 Nagle 算法。对延迟敏感的服务(如游戏、实时通信),Nagle 算法会将小包合并延迟发送,严重影响延迟。
开启 SO_REUSEPORT。对于多 worker 进程/线程,启用 REUSEPORT 让内核做连接分配,避免锁竞争。
考虑使用 accept4 而非 accept。accept4 支持直接设置 SOCK_NONBLOCK | SOCK_CLOEXEC,省去额外的 fcntl 调用。
连接预分配与连接池。在高并发场景下频繁 malloc/free client 结构体有开销,可使用内存池预分配。
九、生产案例:epoll 在知名项目中的应用
9.1 Nginx 的事件驱动核心
Nginx 是 epoll 最著名的生产案例。Nginx 的 event 模块将 epoll 封装为 epoll_module,核心设计思路是:主进程创建 epoll 实例 → worker 进程独立 accept → 连接分配到 worker 的 epoll → 边沿触发(ET)模式减少事件风暴 → 非阻塞读写实现单进程数万连接。
关键配置:
events {
use epoll;
worker_connections 65535;
multi_accept on;
}
其中 multi_accept on 表示一次 epoll_wait 返回后尽可能多地 accept 新连接,避免连接在 backlog 中等待被后续轮次处理。
9.2 Redis 的事件驱动模型
Redis 6.0 之前采用单线程 epoll 模型,将所有客户端连接和命令处理放在一个 aeEventLoop 中轮询。ae.c 抽象了 epoll/kqueue/select 三种底层实现。Redis 虽然 6.0 引入了 IO 多线程,但核心的连接管理和命令分发仍依赖 epoll 的单线程循环。
9.3 Netty 的 Linux epoll 传输
Netty 在 Linux 平台提供了 EpollEventLoopGroup 和 EpollSocketChannel,相比 NIO 的 Selector(基于 poll/select),Netty 的 epoll 实现使用了 EPOLLET 边缘触发 + EPOLLONESHOT,在基准测试中可提升约 30% 的吞吐量。
十、总结与学习路径
epoll 作为 Linux 高性能网络编程的基石,理解其原理对系统程序员至关重要。总结全文核心要点:
核心数据结构:红黑树管理事件注册(O(log n) 增删),就绪链表直接返回就绪项(O(1) 获取)。
两种触发模式:LT 简单安全(默认),ET 高性能需要循环读取。
常见陷阱:ET 必须非阻塞 IO + 循环处理;accept 必须循环到 EAGAIN;多线程需要 EPOLLEXCLUSIVE 或 EPOLLONESHOT。
学习路径建议:
理解 select/poll 的局限 → 掌握 epoll API 调用 → 对比 LT/ET 行为差异 → 熟悉内核红黑树 + 就绪链表实现 → 动手写 epoll HTTP Server → 分析 Nginx/Redis 事件驱动代码 → 了解 io_uring 等下一代异步 IO 接口。
推荐资源:《Unix 网络编程》《Linux 高性能服务器编程》(游双著)、《Understanding Linux Network Internals》、man 手册(man 7 epoll、man 2 epoll_ctl、man 2 epoll_wait)、Linux 内核源码(fs/eventpoll.c)。

发表评论 取消回复