本文最初写于2017年,2026年重新核对并整理。

一个线程如果只阻塞等待一个连接,模型很容易理解,但连接数上升后会带来大量线程及上下文切换。I/O复用提供了另一种方式:让一个调用同时等待多个文件描述符的就绪事件,再处理其中已经可以读写的部分。

它解决的是“等待很多连接”的问题,不会自动解决业务处理过慢,也不意味着单线程可以完成所有工作。

select

select通过多个fd_set分别描述需要关注的可读、可写和异常事件。调用返回后,应用需要检查这些集合,找出已经就绪的文件描述符。

它的优点是接口历史悠久、可移植性较好;限制也很明显:描述符集合存在容量约束,每次调用都要重新准备集合,返回后还要遍历检查。

poll

poll使用pollfd数组表达描述符和关注的事件,不再依赖fd_set的固定表示方式,因此通常没有select那样的描述符编号上限。

但它仍然需要把关注集合交给内核,返回后也需要扫描数组寻找就绪项。当监听集合很大、每次只有少量连接活跃时,这部分工作会逐渐明显。

epoll

Linux的epoll把关注集合和等待动作拆开:

  • epoll_ctl负责增加、修改或删除关注的文件描述符;
  • epoll_wait等待并返回就绪事件。

应用不必在每次等待时重新提交整个集合,返回结果也直接面向就绪项。这使它适合长期监听大量连接、但同一时刻只有少量连接活跃的场景。

epoll支持两种通知方式:

  • 水平触发:只要描述符仍处于就绪状态,就可能再次通知;
  • 边沿触发:主要在状态变化时通知,通常需要配合非阻塞I/O,并持续读写直到返回EAGAIN

边沿触发并不天然代表更好的整体性能,它换来的是更严格的处理要求。遗漏一次读取或错误处理就绪状态,都可能造成连接长时间得不到处理。

epoll并不总是更快

如果监听的描述符很少,或者大部分描述符每次都处于活跃状态,selectpollepoll之间的差异可能并不重要;关注集合频繁变化时,epoll_ctl本身也有系统调用成本。

选择模型时更值得关注的是:

  • 同时需要监听多少连接;
  • 活跃连接占比有多高;
  • 关注集合是否长期稳定;
  • 是否需要跨平台;
  • 业务处理是否会阻塞事件循环。

I/O复用只是并发网络程序的一层。事件拿到之后如何分配计算任务、控制慢请求和避免单个连接占满循环,仍然需要在应用层解决。

参考