跳到正文
Elaine Blog
返回

select、poll 与 epoll:从就绪通知理解高并发 I/O

网络与 I/O

一个线程如何管理成千上万条连接?关键不是“同时执行所有连接”,而是让操作系统通知哪些连接当前可以继续读写。这就是 I/O 多路复用

Table of contents

Open Table of contents

运行 Selector 回声服务器

下载NIO 回声服务器下载,运行真实 Socket 测试:

mvn -Dtest=NetworkServerLabTest#echoesThroughARealNioSocket test

服务器把 ServerSocketChannel 注册到 Selector,关注接受连接事件;新连接再注册读取事件。select() 返回后必须遍历并移除已选择的 key。

三种机制解决同一个问题

select 每次传入文件描述符集合,集合大小受实现限制,内核和用户态需要反复复制并扫描。poll 使用可变数组,摆脱固定集合位图限制, 但仍随关注数量线性扫描。

Linux 的 epoll 在内核维护关注集合,应用等待就绪事件列表,不必每轮重新提交全部连接。它适合“连接很多、同一时刻活跃连接较少”的服务器, 但业务处理耗时仍决定最终吞吐。

Java Selector 是跨平台抽象:Linux 通常映射到 epoll,macOS 可能使用 kqueue。应用不应依赖某个平台的内部实现细节来保证正确性。

就绪不等于操作一定完成

读就绪表示读取大概率不会阻塞,但一次 read 可能只得到半条消息,也可能返回 0;写就绪表示发送缓冲区有空间,不代表整条响应一次写完。 因此每个连接都要保存尚未解析的输入和尚未发送的输出状态。

水平触发会在条件仍满足时重复通知,较容易写对;边沿触发只在状态变化时通知,处理方通常要读到返回“暂时无数据”为止,否则可能错过后续机会。

事件循环的正确边界

事件循环应该快速完成:接受连接、读取有限字节、推进协议状态、提交耗时任务、写出已完成结果。不要在循环中执行文件大读、远程调用、长时间锁等待或无界计算。

需要观测:

常见故障

空轮询会让 select 立即返回却没有有效事件,表现为单核 CPU 飙高。连接忘记取消注册或关闭会泄漏文件描述符。只关注读、不处理写积压会让内存持续增长。 先保存线程栈、系统调用和连接指标证据,再判断是 JDK、框架还是应用状态机问题。

下一步

阅读Netty 快速入门,用框架提供的 Channel、EventLoop 和 Pipeline 管理这些状态。


分享这篇文章:

上一篇
BIO、NIO、直接内存与零拷贝
下一篇
Netty 快速入门:Channel、EventLoop 与 Pipeline