← 返回题目列表

什么是 I/O 多路复用?select、poll 和 epoll 有什么区别?

高频 困难 第 11 / 22 题 更新于 2026/07/25
IO 多路复用selectpollepollSelector

简化版

I/O 多路复用是让一个线程等待多个文件描述符的就绪事件,哪个就绪就处理哪个。select 有描述符数量和每次扫描集合的问题;poll 改用可变长数组,但仍需线性扫描;Linux epoll 在内核中维护关注集合和就绪队列,通常更适合大量连接。

详细版

多路复用的“多路”是多个 Socket,“复用”是复用一个等待机制和事件线程。它不是同一时刻并行执行多个业务,而是避免为每个连接单独安排一个阻塞线程。

机制关注集合获取就绪事件主要特点
select固定大小的 fd 位集合每次调用后扫描集合兼容性好,但有 FD_SETSIZE 等限制
pollpollfd 数组扫描数组中的 revents没有 select 的固定位集合上限,仍是线性遍历
epoll内核中持久维护epoll_wait 返回就绪项避免每轮传入和扫描全部关注集合,支持 LT/ET

Java NIO 提供的是 Selector 统一抽象。它由当前 JDK 和操作系统的 SelectorProvider 选择底层实现;不应把所有平台的 Java Selector 都等同于 Linux epoll。

完整版教学

一、为什么需要多路复用

一个网络连接的大部分时间可能都在等待数据。传统的“一连接一平台线程”模式中,大量线程会占用内存,并增加调度和上下文切换开销。

多路复用先由内核等待一批描述符,只把“已就绪”的结果交给用户程序。事件线程不再按连接个数阻塞,而是按实际活跃事件工作。

二、select 为什么在大并发下吃力

select 通过位集合传入需要关心的读、写和异常描述符。调用返回时,集合已被修改为就绪项,程序必须扫描集合才能找到它们,下次调用前还要重建参数。

它常见的局限有:

  • 可表示的描述符受 FD_SETSIZE 限制;
  • 每轮都要在用户态和内核态之间传递集合;
  • 就绪连接很少时,仍要按关注集合的范围检查。

三、poll 改了什么

pollpollfd 数组替代固定长度位集合,因此不再受 select 那个固定的 FD_SETSIZE 限制,可监听数量主要受系统资源限制。

poll 没有改变根本的全量处理方式:数组每轮仍要传给内核,返回后程序仍要检查各项的 revents

四、epoll 的两个核心改变

Linux epoll 将“管理关注集合”与“等待就绪事件”分开:

  1. 通过 epoll_ctl 增删改需要关心的 fd,这个集合持久保存在内核中;
  2. 通过 epoll_wait 等待并取回已就绪项,无须每轮传入和扫描整个关注集合。

这使它在“连接很多、同时活跃的连接占比较低”的场景中通常更有优势。但不应简单背成“epoll 所有操作都是 O(1)”:性能仍受就绪事件数、回调管理、锁竞争和业务处理时间影响。

五、LT 和 ET

  • LT(水平触发):只要 fd 仍处于就绪状态,就可能继续通知,容错性较好。
  • ET(边缘触发):就绪状态发生变化时通知,通常要配合非阻塞 fd,一直读或写到返回 EAGAIN,否则可能遗留数据却等不到新通知。

六、用数字理解 O(n) 与就绪集合

假设监听 100,000 个连接,本轮只有 100 个连接就绪。select/poll 风格需要维护并检查大规模关注集合,工作量与关注项规模相关;epoll_wait 直接返回就绪项时,用户态消费的主要是这 100 个事件,但内核维护注册、产生事件和唤醒仍有成本。

关注连接数 N = 100,000
本轮就绪数 K = 100
select/poll 查找就绪项:通常随 N 增长
epoll_wait 返回结果:主要随 K 增长,但不是“系统所有成本恒为 O(1)”
场景select/poll 的压力epoll 的压力
N 很小、K 接近 N差异可能不明显仍要处理 K 个事件
N 很大、K 很小全量集合成本突出就绪队列更有优势
单个事件处理 50 ms都会被业务拖慢同样会被业务拖慢
高频增删 fd重建/传递集合epoll_ctl 也有成本

七、Java Selector 与线程模型的边界

Java Selector 是跨平台抽象,注册的是 SelectableChannel,每个注册由 SelectionKey 表示。选择操作只更新就绪集合或执行就绪 key 的 action,并不承诺底层一定是 epoll;Linux、macOS、Windows 以及不同 JDK 实现可以采用不同机制。

一个常见 Reactor 会让 I/O 线程负责 accept、read、decode 和排队,把耗时业务交给工作线程,再把响应安全地送回事件循环。跨线程修改 interest set 后常需配合 selector.wakeup(),否则负责 select 的线程可能要等到其他事件或超时才看到变化。

记忆钩子:多路复用优化的是“从 N 个连接中找出 K 个就绪者”,后续仍然要逐个读写和处理 K 个事件。

惊群、热点连接和事件批量大小也会影响延迟。一次 epoll_wait 返回 10,000 个事件时,应用仍要遍历并安排 10,000 次处理;若每个事件花 0.2 ms,单线程理论工作量就是 2 秒,因此常需多 Reactor、批量上限和业务线程池协作。

描述符就绪只说明某类操作当前不会按通常方式阻塞,不说明业务请求完整,也不说明随后永远可读写。多个线程竞争同一 fd、缓冲区被其他代码耗尽或连接发生错误时,实际调用结果仍必须逐次检查。

八、常见误区与追问

  • 误区:I/O 多路复用能让一个线程并行执行多个业务。 它复用的是等待机制,事件处理仍按线程调度执行,CPU 密集工作不会自动并行。
  • 误区:poll 完全解决了 select 的性能问题。 poll 去掉固定位集合上限,但每轮传递和线性检查数组的基本方式仍在。
  • 误区:epoll 的所有操作都是 O(1)。 注册维护、就绪事件生成以及返回 K 个事件都需要成本,至少要处理实际就绪项。
  • 误区:ET 模式收到一次通知只读一次即可。 ET 通常要求非阻塞 fd 持续处理到 EAGAIN,否则可能留下数据却没有新的边缘通知。
  • 追问:LT 为什么更容易写对? 只要就绪条件持续存在就可能再次通知,应用一次没有排空数据通常仍有补救机会。
  • 追问:Java Selector 在 Linux 上一定使用 epoll 吗? 应依赖 Selector 语义而非实现假设,具体 provider 和底层机制取决于 JDK 与平台。
  • 追问:为什么不能长期注册 OP_WRITE? Socket 大多数时间都可写,持续关注会产生大量无意义就绪事件,通常只在存在未发送数据时开启。

九、加强记忆

I/O 多路复用的目标是用少量线程等待大量连接:select 用固定位集合并全量扫描,poll 改用可变长数组但仍需线性检查,epoll 将关注集合持久保存在内核并返回就绪项。它优化的是等待 I/O,不会让 CPU 密集业务自动并行。