编程与 AI15 分钟阅读更新于 2026-08-04

Linux I/O 多路复用怎么理解:select、poll 与 epoll

解释 I/O 多路复用如何让一个线程同时等待多个文件描述符,并梳理 select、poll 与 epoll 的工作方式、限制和适用场景。

相关工具

为什么一个线程要等待多个 I/O

网络程序经常同时面对很多连接。每个连接都有自己的文件描述符,数据可能在任意时刻到达。如果一个线程每次只等待一个连接,其他连接就只能排队;如果为每个连接都创建线程,又会增加调度、栈空间和上下文切换的开销。

I/O 多路复用提供了另一种安排方式:一个线程把多个文件描述符交给内核,告诉内核自己关心哪些可读、可写或异常事件。只要其中某些描述符已经就绪,内核就返回结果,线程再集中处理真正有事情可做的对象。

一个线程通过内核等待多个文件描述符并处理就绪事件的结构图
I/O 多路复用的基本模型

一个线程等待多个文件描述符,内核在事件就绪后返回可处理的对象。

文件描述符为什么可以代表网络连接

在 Linux 中,文件描述符是进程访问文件、管道、套接字等对象时使用的整数句柄。它本身不是数据,也不是网络连接的全部内容,而是进程与内核对象之间的一个引用。内核通过这个引用找到对应的状态、缓冲区和操作方法。

正因为网络套接字也使用文件描述符,I/O 多路复用就能用一套等待机制同时观察普通文件之外的网络连接。程序不需要为每种对象设计完全不同的等待接口,只要关心描述符什么时候具备某种 I/O 条件即可。

什么叫文件描述符就绪

可读不一定意味着已经读到了完整的一条业务消息,它通常表示当前读取操作不会因为等待数据而立即阻塞。可写也不意味着所有数据都已经发送完,而是发送缓冲区暂时有空间接收更多内容。就绪描述的是一次操作的条件,不是业务协议的完成状态。

因此,程序收到就绪通知后仍然要处理半包、粘包、连接关闭和错误等情况。网络数据是字节流,应用需要自己维护消息边界;I/O 多路复用只负责告诉程序“现在值得尝试读或写”,不负责解释数据内容。

I/O 就绪通知经过读取缓冲和业务消息处理的流程图
就绪事件与业务处理的关系

内核只报告读写条件,应用还要完成读取、缓冲、拆包和业务处理。

select:用集合等待多个描述符

select 通过描述符集合表示要监视的对象,并等待可读、可写或异常状态。调用返回时,程序检查集合中哪些描述符已经就绪,再逐个处理。它的接口直观,历史悠久,适合描述符数量不大、场景比较简单的程序。

select 的一个明显限制是集合通常以位图表示,描述符数量受到固定范围约束。每次调用前后还需要重新准备和检查集合,描述符数量增加时,内核和程序都可能需要遍历较多没有事件的对象。

poll:把描述符放进动态数组

poll 使用结构体数组描述要关注的文件描述符和事件类型,避免了 select 位图带来的固定数量限制。每个元素可以记录可读、可写和异常等条件,调用返回后再查看实际发生的事件。

poll 的表达能力比 select 更灵活,但它仍然需要在等待时检查整个描述符数组。连接数量很大而真正活跃的连接很少时,反复扫描大量无事件对象会带来额外开销。它解决了表示方式的问题,却没有完全改变遍历模型。

epoll:把关注关系留在内核中

epoll 将“要监视哪些描述符”和“这次等待哪些已经就绪”分成了不同操作。程序先把描述符加入内核维护的事件表,之后等待调用主要获取已经就绪的对象,而不必每次把完整集合重新传给内核。

当连接数量很多、活跃连接比例较低时,这种事件驱动方式更有优势。内核在事件发生时记录就绪对象,程序醒来后集中处理返回的结果。epoll 不是让每个 I/O 都变快,而是减少了无效扫描,让等待成本更多地随活跃事件数量变化。

select、poll 和 epoll 工作方式与开销对比图
select、poll 与 epoll 的关注点

select 和 poll 倾向于反复检查集合,epoll 维护内核事件表并返回已就绪对象。

水平触发和边缘触发要分清

水平触发可以理解为“只要条件仍然成立,就可以继续通知”。例如接收缓冲区里还有数据,下一次等待仍可能得到可读事件。它更容易使用,即使一次没有读完,后续仍有机会继续处理。

边缘触发更像“状态刚刚发生变化时通知一次”。如果程序收到通知后没有把数据处理干净,之后未必会再次收到同样的提醒,因此通常要配合非阻塞 I/O,一直读到暂时没有数据为止。边缘触发减少重复通知,但也要求程序更严谨地处理循环和状态。

多路复用不等于异步 I/O

I/O 多路复用解决的是等待多个对象的问题:线程在内核中等待,某些对象就绪后被唤醒。线程随后仍然要主动调用读取或写入操作,把数据从缓冲区取出或交给内核。

异步 I/O 的关注点不同,它更强调提交操作后由系统在完成时通知结果。两者都能减少线程阻塞,但完成通知的时机和程序承担的工作不同。把“就绪”误认为“数据已经处理完成”,是理解网络并发模型时常见的混淆。

怎样选择 select、poll 和 epoll

如果描述符数量很少,程序更在意接口简单和平台兼容,select 仍然可以完成任务。poll 适合需要更灵活描述符集合、但连接规模没有特别大的场景。Linux 上连接规模大、活跃事件稀疏时,epoll 通常更适合。

选择机制时还要看程序的其他部分:是否使用非阻塞套接字,是否需要边缘触发,是否有定时器和信号事件,是否需要跨平台运行。单看性能名称容易忽略真实瓶颈,事件处理、内存分配、日志和业务逻辑同样可能决定整体吞吐量。

用一条主线记住 I/O 多路复用

程序先准备多个文件描述符,再把关注的事件交给内核;内核等待其中一些对象就绪,返回可处理的描述符;程序逐个读取或写入,更新连接状态,并继续等待下一批事件。这个循环把“等待”和“处理”分开,也把线程从单个连接的阻塞中解放出来。

select、poll 和 epoll 的核心目标相同,差异主要在于描述符集合如何保存、就绪状态如何返回,以及等待时需要扫描多少对象。理解这条主线后,接口名称、触发模式和性能差异就能放回各自的位置。

常见问题

可读事件是不是代表收到了一条完整消息?

不是。可读通常表示当前读取不会立即阻塞,实际数据可能只有半条消息,应用还要自行处理缓冲和消息边界。

epoll 为什么适合大量连接?

它把关注的描述符保存在内核事件表中,等待时主要返回已经就绪的对象,减少了每次遍历全部描述符的开销。

使用 epoll 就一定要用边缘触发吗?

不需要。epoll 可以使用水平触发或边缘触发,边缘触发通常要求非阻塞读取并循环处理到暂时没有数据。