epoll¶
是什么 / 解决什么问题¶
epoll 是 Linux 特有的 I/O 多路复用机制,用一个线程高效监听数千到数百万个文件描述符的就绪事件。解决了 select/poll 在大量 fd 场景下 O(n) 扫描的性能问题。
核心 API¶
#include <sys/epoll.h>
// 创建 epoll 实例,返回 epoll fd
int epfd = epoll_create1(EPOLL_CLOEXEC);
// 注册/修改/删除 fd 的监听
struct epoll_event ev = {
.events = EPOLLIN | EPOLLET, // 关注的事件 + 触发模式
.data.fd = sock_fd, // 用户数据,事件就绪时原样返回
};
epoll_ctl(epfd, EPOLL_CTL_ADD, sock_fd, &ev);
epoll_ctl(epfd, EPOLL_CTL_MOD, sock_fd, &ev);
epoll_ctl(epfd, EPOLL_CTL_DEL, sock_fd, NULL);
// 等待事件就绪
struct epoll_event events[MAX_EVENTS];
int nfds = epoll_wait(epfd, events, MAX_EVENTS, timeout_ms);
// timeout_ms: -1 永久阻塞, 0 立即返回, >0 超时毫秒
使用模式¶
基本事件循环¶
#define MAX_EVENTS 1024
int epfd = epoll_create1(EPOLL_CLOEXEC);
struct epoll_event events[MAX_EVENTS];
// 注册 listen_fd
struct epoll_event ev = { .events = EPOLLIN, .data.fd = listen_fd };
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev);
for (;;) {
int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
for (int i = 0; i < n; i++) {
if (events[i].data.fd == listen_fd) {
// 新连接到来
int conn_fd = accept4(listen_fd, NULL, NULL, SOCK_NONBLOCK | SOCK_CLOEXEC);
struct epoll_event cev = { .events = EPOLLIN | EPOLLET, .data.fd = conn_fd };
epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, &cev);
} else {
handle_client(events[i].data.fd, events[i].events);
}
}
}
存储自定义上下文¶
epoll_event.data 是一个 union,可以存指针而非 fd:
struct connection {
int fd;
char *write_buf;
size_t write_len;
// ...
};
struct connection *conn = malloc(sizeof(*conn));
conn->fd = client_fd;
struct epoll_event ev = {
.events = EPOLLIN | EPOLLET,
.data.ptr = conn, // 存指针
};
epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &ev);
// 事件就绪时取出
struct connection *c = (struct connection *)events[i].data.ptr;
行为与语义¶
LT (Level-Triggered) — 水平触发(默认)¶
- 只要 fd 处于就绪状态,每次 epoll_wait 都会返回它
- 行为类似 poll:有数据可读就一直通知
- 你可以不一次读完,下次 epoll_wait 还会再通知
ET (Edge-Triggered) — 边缘触发¶
- 只在状态变化时通知一次(从不可读变为可读)
- 后续不再通知,直到新数据到达引起新的状态变化
- 必须一次读完所有数据(读到 EAGAIN)
ET 模式的读取模板¶
// ET 模式下必须循环读到 EAGAIN
void handle_read_et(int fd) {
char buf[4096];
for (;;) {
ssize_t n = read(fd, buf, sizeof(buf));
if (n > 0) {
process_data(buf, n);
} else if (n == 0) {
// 对端关闭
close(fd);
return;
} else {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
break; // 读完了,正常退出
}
// 真正的错误
perror("read");
close(fd);
return;
}
}
}
LT vs ET 对比¶
| 特性 | LT | ET |
|---|---|---|
| 通知频率 | 重复通知直到处理完 | 只通知一次 |
| 编程复杂度 | 低(不读完也行) | 高(必须读完/写满) |
| epoll_wait 调用次数 | 多(会被同一 fd 反复唤醒) | 少 |
| 适用 | 逻辑简单的场景 | 高性能服务器(Nginx 用 ET) |
| 是否需要非阻塞 fd | 建议用,但不强制 | 必须用非阻塞 |
EPOLLONESHOT¶
注册时加 EPOLLONESHOT,事件触发一次后自动禁用,需要手动 EPOLL_CTL_MOD 重新激活:
性能考量¶
为什么 epoll 比 select/poll 快¶
| 机制 | 每次调用开销 |
|---|---|
| select | O(n) — 拷贝整个 fd_set 到内核,内核遍历所有 fd |
| poll | O(n) — 同上,只是数据结构不同 |
| epoll_wait | O(活跃 fd 数) — 内核维护就绪链表,只返回就绪的 |
epoll_ctl 注册时内核用红黑树管理 fd,事件就绪时通过回调加入就绪链表。epoll_wait 只需拷贝就绪链表。
最佳实践¶
- MAX_EVENTS 设合理值:太小导致频繁调用 epoll_wait,太大浪费栈空间。通常 256~1024。
- 用 EPOLL_CLOEXEC:避免 fd 泄漏到子进程。
- ET 模式减少 epoll_wait 唤醒:高吞吐场景首选。
- 避免 thundering herd:多线程共享同一 epfd 时用 EPOLLEXCLUSIVE(4.5+)。
epoll 的开销¶
epoll_create1:分配内核数据结构(一次性)epoll_ctl:红黑树操作 O(log n)(仅注册/修改时)epoll_wait:O(就绪数),但涉及用户态/内核态切换
如果连接数少(< 几十个),epoll 的优势不明显,poll 甚至更快(少了注册开销)。
常见陷阱与 FAQ¶
1. ET 模式忘记读完¶
// 错误:ET 模式只读一次
n = read(fd, buf, sizeof(buf));
// 如果内核缓冲区有 8KB 数据,你只读了 4KB
// 剩下的 4KB 不会再触发通知 → 数据"丢失"(实际还在缓冲区但不被处理)
2. 关闭 fd 后忘记 epoll_ctl DEL¶
实际上 close(fd) 会自动从 epoll 移除(内核处理)。但如果 fd 被 dup 过,close 一个副本不会移除:
最佳实践:显式调用 epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL) 再 close。
3. EPOLLHUP 和 EPOLLERR 不需要注册¶
这两个事件始终会被报告,无论你是否在 events 中设置它们:
// 即使你只注册了 EPOLLIN,也会收到 EPOLLHUP
if (events[i].events & (EPOLLHUP | EPOLLERR)) {
close(events[i].data.fd); // 必须处理
}
4. epoll_wait 被信号中断¶
int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
if (n == -1) {
if (errno == EINTR) continue; // 被信号打断,重试
perror("epoll_wait");
break;
}
5. 存指针时的生命周期问题¶
// 危险:conn 被释放后 epoll 仍持有野指针
free(conn);
// 下次 epoll_wait 返回时 data.ptr 指向已释放内存
// 正确顺序:先 DEL 再 free
epoll_ctl(epfd, EPOLL_CTL_DEL, conn->fd, NULL);
close(conn->fd);
free(conn);
观测与调试¶
# 查看进程持有的 epoll fd
ls -la /proc/<pid>/fd/ | grep eventpoll
# strace 观察 epoll 调用
strace -e trace=epoll_create1,epoll_ctl,epoll_wait -p <pid>
# 查看 epoll 监听了哪些 fd(需要 root)
cat /proc/<pid>/fdinfo/3
# 输出示例:
# pos: 0
# flags: 02
# mnt_id: 1
# tfd: 5 events: 19 data: 7f8a3c000b60
# bpftrace 统计 epoll_wait 返回的事件数分布
bpftrace -e 'tracepoint:syscalls:sys_exit_epoll_wait { @ret = hist(args->ret); }'
延伸阅读¶
man 7 epoll— 最权威的语义说明man 2 epoll_ctl/man 2 epoll_wait— 参数细节- Nginx 的
src/event/modules/ngx_epoll_module.c— 工业级 epoll 使用范例 - Redis 的
src/ae_epoll.c— 极简 epoll 封装