为什么需要 IO 多路复用?
想象你是一个餐厅的厨师(CPU),同时负责做 100 桌客人的菜。
- 阻塞 IO:你给第 1 桌客人下完单后,就站在传菜口死死等这桌菜做好。菜没做好,你什么都不干,也不去管其他 99 桌。这显然极其浪费你的时间(CPU 资源)。
- 多线程/多进程:你雇了 100 个厨师,每人盯一桌。虽然解决了等待问题,但 100 个厨师的工资(内存开销)和互相沟通的成本(上下文切换开销)太高了。
IO 多路复用(I/O Multiplexing)就是完美的折中方案:
你只需要一个人(单线程),站在一个“中央调度台”前。你不用死死盯着某一桌,而是让调度台帮你盯着这 100 桌。哪一桌的菜做好了(IO 就绪了),调度台就拍一下你的肩膀告诉你,你再去把那桌菜端走。这样,你一个人就能高效地处理成百上千个 IO 请求。
核心原理:从“轮询”到“事件通知”
IO 多路复用的本质,是操作系统提供的一种机制,允许一个进程/线程同时监控多个文件描述符(FD,如 Socket),并在某些 FD 可读或可写时,主动通知应用程序。
它的演进经历了三个重要阶段,完美诠释了原理的升级:
1. select:最早的“全员点名”
- 原理:应用程序把所有需要监控的 FD 拷贝给内核。内核通过遍历所有 FD,检查它们是否就绪。
- 痛点:
- 性能极差:每次都要把所有 FD 拷来拷去,且内核要线性扫描。
- 数量限制:默认最多只能监控 1024 个 FD。
2. poll:取消了数量限制,但没解决效率问题
- 原理:用链表代替了 select 的位图,打破了 1024 的限制。
- 痛点:依然是线性扫描,FD 越多,内核检查越慢。
3. epoll(Linux 终极杀器):从“点名”到“订阅回调”
epoll 彻底改变了游戏规则,它是目前高性能网络服务器(如 Nginx、Redis)的基石。
- 原理:
- 注册机制:应用程序通过
epoll_ctl把需要监控的 FD 注册到内核的一个红黑树中(增删改的时间复杂度仅为 O(logN) )。 - 事件回调:当某个 FD 真正就绪时,内核通过回调函数将其加入一个“就绪链表(ready list)”。
- 精准获取:应用程序调用
epoll_wait时,内核直接把就绪链表返回,不需要任何遍历。
- 注册机制:应用程序通过
- 优势:不管监控 100 个还是 10 万个连接,只要活跃的只有 10 个,
epoll_wait就只返回这 10 个,效率极高。
📊 三种模型对比总结
表格
| 特性 | select | poll | epoll |
|---|---|---|---|
| 底层数据结构 | 数组/位图 | 链表 | 红黑树 + 就绪链表 |
| 最大连接数 | 1024 | 无上限 | 无上限(受系统内存限制) |
| FD 增加/删除开销 | O(N) | O(N) | O(logN) |
| 查询就绪状态 | 线性遍历 O(N) | 线性遍历 O(N) | 回调机制 O(1) |
| 适用场景 | 连接数少且活跃 | 逐渐被淘汰 | 海量并发、高吞吐场景 |
- Nginx:为什么单机能抗住几万并发?因为它底层使用了 epoll,一个 Worker 进程就能处理成千上万个连接。
- Python asyncio / Node.js:它们底层的事件循环(Event Loop),在 Linux 上正是基于 epoll 实现的




