Skip to content

第 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 等价LTET
是否支持 ONESHOT
跨平台✅ POSIX,所有 Unix-like + Windows✅ Unix-like,无 Windows❌ 只 Linux
典型监听规模< 1000< 10000100 万级
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 限制
poll50000+ 次操作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)
poll100000 × 8B = 800 KB内核会复制一份 = 800 KB~1.6 MB,每次系统调用都来回拷贝
epollevents 数组(按需 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 实际项目都用啥?

项目主选备注
Nginxepoll (Linux) / kqueue (BSD) / select (兜底)ET 模式 + worker 进程
Redisepoll (Linux) / kqueue (BSD) / select / evportLT 模式 + 单线程事件循环
Nettyepoll (Linux) / kqueue (BSD) / NIO SelectorJava 通过 JNI 直接调 epoll
Node.js (libuv)epoll (Linux) / kqueue (mac) / IOCP (Win)自动选最优
Tomcat NIOJava NIO Selector → epollLT 模式

5. select/poll/epoll API 对比表

任务selectpollepoll
创建监听集fd_set rfds; FD_ZERO(&rfds);struct pollfd pfds[N];int epfd = epoll_create1(0);
添加 fdFD_SET(fd, &rfds);pfds[i] = {fd, POLLIN, 0};epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
删除 fdFD_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 & POLLINevents[i].events & EPOLLIN

6. 性能实测数据(行业经验值)

6.1 Latency(10K 个 fd,1 个就绪)

方案单次 wait latency备注
select❌ 超限
poll~80 μsn 越大越慢
epoll~3 μs与 n 几乎无关

6.2 QPS(C10K 测试,echo 服务器)

方案最大 QPSCPU 占用
一连接一线程 + 阻塞 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 怎么解决惊群?

🎬 可视化演示

演示加载缓慢或样式异常?点此在新标签页打开 ↗