IO 多路复用
高并发网络服务的基石:一个线程同时监听大量连接。面试必考:select/poll/epoll 对比、触发模式。
解决什么
阻塞 IO: 一个线程等一个连接, 千连接千线程(线程爆炸)
IO 多路复用: 一个线程等所有连接, 有事件才处理三兄弟对比
| 维度 | select | poll | epoll |
|---|---|---|---|
| 数据结构 | 位图(1024 上限) | 链表(无上限) | 事件表(内核维护) |
| 每次调用 | 全量拷贝 fd 集合 | 全量拷贝 | 只传变化的 |
| 就绪通知 | 轮询扫描 | 轮询扫描 | 事件回调 |
| 复杂度 | O(n) | O(n) | O(就绪数) |
| 平台 | 通用 | 通用 | Linux |
epoll 的杀手锏:
- 内核事件表:fd 注册一次,不用每次全量拷贝(select 每次都要)
- 事件驱动:就绪才通知(select/poll 要全扫)
- 百万连接只有少量活跃时,epoll 效率碾压
触发模式
| 模式 | 语义 | 注意 |
|---|---|---|
| 水平触发(LT) | 有数据就持续通知 | 简单,不读完也会再通知(默认) |
| 边缘触发(ET) | 状态变化才通知一次 | 必须一次读完,否则丢数据 |
LT: 缓冲有 100 字节, 读 10 字节 → 还会通知(还有 90)
ET: 缓冲有 100 字节, 读 10 字节 → 不再通知(下次有新数据才通知)ET 的坑:要循环读到 EAGAIN(读空),否则数据滞留。高并发框架用 ET + 非阻塞 IO 组合(如 Redis、Netty)。
应用
| 系统 | 用法 |
|---|---|
| Redis | 单线程事件循环(见 Redis 线程模型篇) |
| Nginx | 事件驱动高并发 |
| Node.js | libuv 封装 |
| Netty | Java NIO 封装(见事件驱动篇) |
与多线程的配合
- 单线程 + 多路复用:IO 密集场景足够(Redis 十万 QPS)
- 多线程 + 多路复用:每个线程有自己的 epoll(多核利用)
- Reactor 模式是标准封装(见事件驱动篇)
面试追问
- epoll 为什么比 select 快? 内核事件表(不重复拷贝)+ 事件回调(不轮询)。就绪才通知
- 水平触发和边缘触发? LT 持续通知(默认),ET 只通知一次(要读完)。ET + 非阻塞是高性能组合
- select 的 1024 限制? fd 位图上限。poll 链表无上限但仍是轮询
- IO 多路复用解决什么? 单线程管海量连接:事件驱动,有事件才处理
- Redis 为什么用 epoll? 单线程 + 事件驱动:十万连接只有命令到达才处理