主题
第 6 章 · select / poll / epoll 横向对比
学完本章你将知道:把前 3 章的内容全部汇总——三者在数据结构、时间复杂度、跨平台性、典型用法上的全方位差异;面对一个新项目时该选哪一个。
1. 一张终极对比表
| 维度 | select (1983) | poll (1986) | epoll (2002, Linux) |
|---|---|---|---|
| fd 数量上限 | 1024(编译期硬限) | 仅受 ulimit -n | 仅受 ulimit -n |
| 数据结构 | 3 个 fd_set 位图 | pollfd 数组 | 红黑树 + 就绪链表 |
| 关心事件 | 拆 3 张表 | events 字段位掩码 | events 字段位掩码 |
| input/output 分离 | ❌(fd_set 被改写) | ✅(events / revents) | ✅(events / events) |
| 每次调用开销 | O(n) 拷贝 + O(n) 遍历 | O(n) 拷贝 + O(n) 遍历 | O(就绪 fd 数) |
| fd 注册成本 | 每次都要传 | 每次都要传 | 一次 epoll_ctl,长期复用 |
| 触发模式 | LT 等价 | LT 等价 | LT 或 ET |
| 是否支持 ONESHOT | ❌ | ❌ | ✅ |
| 跨平台 | ✅ POSIX,所有 Unix-like + Windows | ✅ Unix-like,无 Windows | ❌ 只 Linux |
| 典型监听规模 | < 1000 | < 10000 | 100 万级 |
| API 复杂度 | 简单 | 简单 | 较复杂(3 个调用) |
| 典型用户 | 跨平台脚本 / 历史代码 | 跨平台 Unix 工具 | Nginx, Redis, Netty (Linux) |
2. 时间复杂度推导
2.1 推导假设
设:
n= 监听的 fd 总数k= 每次调用时实际就绪的 fd 数(通常 k << n)
2.2 select 的总开销
text
1. 用户→内核拷贝 fd_set: O(n / 64) ← bit 操作很快但仍是 n 比例
2. 内核遍历 fd_set 加等待队列: O(n)
3. 进程睡眠(不算 CPU)
4. 唤醒后内核再扫一遍 fd_set: O(n)
5. 内核→用户拷贝 fd_set: O(n / 64)
6. 用户态 FD_ISSET 遍历找就绪: O(n)
──────────
总计: O(n) (4 次 O(n) 操作)2.3 poll 的总开销
text
1. 用户→内核拷贝 pollfd 数组: O(n) ← 每个 8B,比位图大
2. 内核遍历加等待队列: O(n)
3. 唤醒后再扫: O(n)
4. 内核→用户拷贝: O(n)
5. 用户态遍历 revents: O(n)
──────────
总计: O(n) (和 select 同阶,但常数更大)2.4 epoll 的总开销
text
注册阶段(仅启动 + 新连接时):
epoll_ctl ADD: O(log n) ← 红黑树插入
↓
事件循环阶段(每次 epoll_wait):
1. 用户态调用 epoll_wait: O(1)
2. 内核检查就绪链表: O(1)
3. 拷贝就绪事件给用户: O(k)
4. 用户态遍历 events: O(k)
──────────
总计: O(k)2.5 对比示例:监听 10000 个 fd,1 个就绪
| 方案 | 操作量 | 相对值 |
|---|---|---|
| select | ❌ 直接超 1024 限制 | — |
| poll | 50000+ 次操作 | 5000x |
| epoll | ~10 次操作 | 1x(基准) |
3. 数据结构差异详解
3.1 三种"监听集合"的对比
text
select (fd_set 位图)
┌────────────────────────────────┐
│ bit 0: ┃ bit 1: ┃ ... bit 1023│
│ 0 ┃ 1 ┃ │
└────────────────────────────────┘
128 字节定长,bit 1 表示"关心"
poll (pollfd 数组)
┌──────┬──────┬─────────────────┐
│ fd=3 │ fd=8 │ ... fd=12345 │
│POLLIN│POLLIN│ POLLIN|POLLOUT│
└──────┴──────┴─────────────────┘
8B × N,连续内存,遍历 O(n)
epoll (红黑树 + 就绪链表)
┌─────────────────┐ ┌─────────────────┐
│ 红黑树 (RB) │ │ 就绪链表 (RDL) │
│ / \ │ │ epi₃ → epi₈ │
│ ... ... │ │ │
└─────────────────┘ └─────────────────┘
O(log n) 增删查找 O(1) 取就绪3.2 内存占用对比(监听 10 万个 fd 时)
| 方案 | 用户态结构 | 内核态结构 | 总占用 |
|---|---|---|---|
| select | ❌ 不可能(超 1024) | — | — |
| poll | 100000 × 8B = 800 KB | 内核会复制一份 = 800 KB | ~1.6 MB,每次系统调用都来回拷贝 |
| epoll | events 数组(按需 64-1024 个)= 几 KB | 红黑树节点 ~80B × 100000 = ~8 MB(只在内核,不来回拷贝) | ~8 MB(仅内核常驻) |
关键:epoll 占的内存看起来更大(红黑树节点),但这是常驻内核的、不需要每次 syscall 来回拷贝——而 poll 那 1.6 MB 每次 poll 都要拷贝两次。
4. 何时选谁?决策矩阵
4.1 简易决策树
4.2 各类场景推荐
| 场景 | 推荐 | 理由 |
|---|---|---|
| 写 Linux 高并发服务器 | epoll | 性能不二之选 |
| 写跨 Unix-like 工具(mac+linux) | poll | 两边都能编译,没有 1024 限制 |
| 写跨平台库(含 Windows) | select(兜底)+ 平台特定路径 | libuv / libevent 的方式 |
| 监听少量 fd(<10)、求简单 | poll | 一行 poll(&pfd, 1, ms) 解决 |
| 老代码维护、不想动 | 保留原方案 | select 慢但能跑 |
| 极致性能、Linux 5.1+ | io_uring | 真异步 + 批量 + 减 syscall |
4.3 实际项目都用啥?
| 项目 | 主选 | 备注 |
|---|---|---|
| Nginx | epoll (Linux) / kqueue (BSD) / select (兜底) | ET 模式 + worker 进程 |
| Redis | epoll (Linux) / kqueue (BSD) / select / evport | LT 模式 + 单线程事件循环 |
| Netty | epoll (Linux) / kqueue (BSD) / NIO Selector | Java 通过 JNI 直接调 epoll |
| Node.js (libuv) | epoll (Linux) / kqueue (mac) / IOCP (Win) | 自动选最优 |
| Tomcat NIO | Java NIO Selector → epoll | LT 模式 |
5. select/poll/epoll API 对比表
| 任务 | select | poll | epoll |
|---|---|---|---|
| 创建监听集 | fd_set rfds; FD_ZERO(&rfds); | struct pollfd pfds[N]; | int epfd = epoll_create1(0); |
| 添加 fd | FD_SET(fd, &rfds); | pfds[i] = {fd, POLLIN, 0}; | epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev); |
| 删除 fd | FD_CLR(fd, &rfds); | 数组里挪走 | epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL); |
| 等事件 | select(maxfd+1, &rfds, ...) | poll(pfds, n, timeout) | epoll_wait(epfd, events, max, timeout) |
| 检查 fd 是否就绪 | FD_ISSET(fd, &rfds) | pfds[i].revents & POLLIN | events[i].events & EPOLLIN |
6. 性能实测数据(行业经验值)
6.1 Latency(10K 个 fd,1 个就绪)
| 方案 | 单次 wait latency | 备注 |
|---|---|---|
| select | ❌ 超限 | — |
| poll | ~80 μs | n 越大越慢 |
| epoll | ~3 μs | 与 n 几乎无关 |
6.2 QPS(C10K 测试,echo 服务器)
| 方案 | 最大 QPS | CPU 占用 |
|---|---|---|
| 一连接一线程 + 阻塞 I/O | < 5000 | 上下文切换爆炸 |
| select 单线程 | 不可能(超 1024) | — |
| poll 单线程 | ~50K | 高(O(n)) |
| epoll 单线程 | ~200K | 中等 |
| epoll + ET + 多 worker | ~500K+ | 较低 |
数据来源:来自经典论文《Comparing the Performance of Web Server Architectures》及 Nginx / Netty 官方 benchmark。具体数字取决于硬件、内核版本、协议。
7. 三者的"亲属关系"——多路复用进化树
text
┌─────────────────┐
│ 阻塞 I/O (远古) │
│ 一连接一线程 │
└────────┬────────┘
↓
┌─────────────────┐
│ select (1983) │
│ 位图,1024 限 │
└────────┬────────┘
↓
┌─────────────────┐
│ poll (1986) │
│ 数组,无上限 │
└────────┬────────┘
┌──────────────┴──────────────┐
↓ ↓
┌─────────────────┐ ┌─────────────────┐
│ kqueue (BSD) │ │ epoll (Linux) │
│ 2000 │ │ 2002 │
└────────┬────────┘ └────────┬────────┘
└──────────────┬───────────────────┘
↓
┌─────────────────┐
│ libuv / libevent │
│ 跨平台抽象 │
└────────┬────────┘
↓
┌─────────────────┐
│ io_uring (2019) │
│ Linux 真异步 │
└─────────────────┘演化逻辑:每一代都是为了解决前一代的痛点——select 解决了"轮询浪费 CPU";poll 解决了"1024 限制";epoll 解决了"O(n) 性能";io_uring 解决了"还是同步 + syscall 太多"。
8. 本章小结
text
三剑客对比一句话总结
─────────────────────────────────
select → 元老,跨平台,1024 上限,O(n)
poll → 易用版 select,无上限,O(n)
epoll → Linux 神器,O(就绪数),ET 性能怪兽9. 真实面试题(常考)
Q1:select 改用 poll 能解决哪些问题?没解决的是什么?
A:解决了:① 1024 fd 上限;② events / revents 分离,不需要 master/working 双份;③ API 更直观。没解决:每次都要全量传/拷/扫,O(n) 性能瓶颈仍在——这才是 epoll 要解决的。
Q2:epoll 比 poll 快几个数量级?请讲数字。
A:监听 1 万个 fd、就绪 1 个时:poll 单次 ~80 μs,epoll ~3 μs,约 20-30 倍;监听 10 万个 fd 时差距能到 几百倍。但如果几乎所有 fd 都同时就绪(边界场景),两者性能接近。
Q3:为什么 select / poll 要把 fd 集合每次都从用户态拷到内核?
A:因为它们是无状态的——每次调用内核都不知道你要监听啥,必须从头告诉一遍。epoll 通过
epoll_ctl一次注册到内核红黑树,长期复用,避免了每次拷贝。
Q4:什么情况下 epoll 性能反而不如 poll?
A:fd 数少且就绪率极高时——比如只监听 5 个 fd,每次都全部就绪。此时:① poll 数组小、缓存友好;② epoll 还要走红黑树/链表的间接寻址。但这种场景几乎不存在于真实生产环境。
Q5:跨平台库(如 libuv)是怎么处理 epoll/kqueue/select 差异的?
A:编译期或运行时分发——libuv 在不同平台编译时链接不同实现:Linux 用 epoll、macOS/BSD 用 kqueue、Windows 用 IOCP、其他 Unix 用 poll。统一对外 API(
uv_loop_t),用户感知不到底层。
Q6:epoll 一定比 poll 节省内存吗?
A:不一定。epoll 在内核里保存红黑树(每节点 ~80B),监听 100 万 fd 占 80 MB;poll 用户态数组 8 MB。但 poll 这 8 MB 每次 syscall 都要拷贝两次,epoll 是常驻内核的——总系统开销 epoll 仍胜出。
下一站 → 第 7 章:实战应用,去看 Nginx、Redis、Netty 这些工业级项目,究竟是怎么用 epoll 的——LT 还是 ET?多线程还是单线程?SO_REUSEPORT 怎么解决惊群?
🎬 可视化演示
演示加载缓慢或样式异常?点此在新标签页打开 ↗