epoll 是 Linux 网络服务器的核心 I/O 多路复用机制。它为什么比 select/poll 快?内部怎么实现的?
一、先看图
flowchart TD
subgraph eventpoll
RBTREE[红黑树<br/>管理所有 fd]
RDLLIST[就绪链表<br/>有事件的 fd]
WAIT[等待队列<br/>阻塞的 epoll_wait]
end
ADD[epoll_ctl ADD] --> RBTREE
MOD[epoll_ctl MOD] --> RBTREE
DEL[epoll_ctl DEL] --> RBTREE
RBTREE -->|回调触发| RDLLIST
RDLLIST --> EWAIT[epoll_wait<br/>返回就绪 fd]
EWAIT --> WAIT
classDef ep fill:#388bfd22,stroke:#388bfd,color:#adbac7;
classDef op fill:#3fb95022,stroke:#3fb950,color:#adbac7;
class RBTREE,RDLLIST,WAIT ep
class ADD,MOD,DEL,EWAIT op
二、核心结构
// fs/eventpoll.c
struct eventpoll {
struct rb_root_cached rbr; // 红黑树:所有注册的 fd
struct list_head rdllist; // 就绪链表
wait_queue_head_t wq; // epoll_wait 等待队列
// ...
};
struct epitem {
struct rb_node rbn; // 红黑树节点
struct list_head rdllink; // 就绪链表节点
struct epoll_filefd ffd; // fd + file*
struct epoll_event event; // 用户注册的事件
// ...
};三、epoll_ctl:注册 fd
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);- 分配
epitem - 插入红黑树(O(log n))
- 在目标 fd 的等待队列注册回调
ep_poll_callback
当 fd 有事件 → 回调把 epitem 加入就绪链表 → 唤醒 epoll_wait。
四、epoll_wait:收割事件
int n = epoll_wait(epfd, events, maxevents, timeout);- 检查就绪链表 → 非空则直接返回
- 空 → 睡在
ep->wq→ 等待回调唤醒 - 唤醒后遍历就绪链表 →
copy_to_user→ 返回
O(就绪 fd 数),不是 O(全部 fd 数)——这是 epoll 打败 select/poll 的关键。
五、LT vs ET
5.1 LT(Level-Triggered,默认)
fd 有数据可读 → 每次 epoll_wait 都返回。没读完 → 下次还返回。
5.2 ET(Edge-Triggered)
event.events = EPOLLIN | EPOLLET;只在状态变化时通知一次。必须一次读到 EAGAIN。
ET 优势:减少 epoll_wait 返回次数。 ET 陷阱:不读完 → 永远不再通知。
5.3 内部实现
LT:就绪事件返回后,如果 fd 仍有事件 → 重新加入就绪链表。 ET:返回后 不 重新加入。
六、EPOLLEXCLUSIVE(4.5+)
event.events = EPOLLIN | EPOLLEXCLUSIVE;多线程/多进程 epoll_wait 同一个 listen socket → 惊群(thundering herd)。
EPOLLEXCLUSIVE:只唤醒一个等待者。
Nginx 1.11.3+ 使用。
七、EPOLLONESHOT
event.events = EPOLLIN | EPOLLONESHOT;触发一次后自动禁用 → 需要 epoll_ctl(MOD)
重新启用。
保证每个 fd 同一时刻只被一个线程处理。
八、级联 epoll
epoll fd 本身也是 fd → 可以被另一个 epoll 监控 → 级联 epoll。
用途:分层事件处理(不常用,有复杂性)。
九、epoll vs io_uring
| 特性 | epoll | io_uring |
|---|---|---|
| 通知模型 | 就绪通知 | 完成通知 |
| syscall | epoll_wait | 可零 syscall |
| 适用 | 网络 | 文件 + 网络 |
| 复杂度 | 低 | 高 |
| 生态 | 成熟 | 快速成长 |
大多数网络场景 → epoll 足够。文件 I/O + 极致性能 → io_uring。
十、小结
- epoll = 红黑树(管理 fd)+ 就绪链表(O(1) 事件返回)+ 回调机制
- LT 安全但可能多次返回;ET 高效但必须读完
- EPOLLEXCLUSIVE 解决惊群
- epoll 仍是网络服务器的主力,io_uring 是补充不是替代
参考文献
fs/eventpoll.cman 7 epoll- Davide Libenzi, “epoll — scalable I/O event notification mechanism.”
- 网络百科-67-epoll-deep
工具
strace -e epoll_ctl,epoll_waitss -tlnp(查看 listen socket)bpftrace -e 'kprobe:ep_poll { @[comm] = count(); }'
上一篇:io_uring 内核内部 下一篇:select/poll
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【操作系统百科】异步 I/O 模型 benchmark
epoll、io_uring、libaio、阻塞线程池——四种异步模型的真实性能对比。本文用统一 workload 量化 echo server、静态文件服务、数据库 I/O 场景下的吞吐、延迟与 CPU 开销。
【操作系统百科】内存回收
Linux 内存回收是 VM 最复杂的子系统之一。本文讲 active/inactive LRU、kswapd 与 direct reclaim、watermark 三线、swappiness 的真实含义、MGLRU 改造、memcg 回收与 PSI。
【操作系统百科】交换
swap 还值得开吗?本文讲 swap area 基础、swap cache、zram 压缩内存、zswap 前端压缩池、swappiness 的真实含义、容器里的 swap 策略,以及为什么现代 Android 全靠 zram 不靠磁盘。
【操作系统百科】Slab/SLUB 分配器
buddy 只管页粒度(4K+),内核大多数对象只有几十到几百字节。slab/SLUB 在 buddy 之上做对象级缓存。本文讲 slab 历史、SLUB 接手、SLOB 退场、kmem_cache、per-CPU cache、KASAN 集成。