Note
select、poll、epoll 有什么区别?
从数据结构、拷贝成本、就绪通知方式与规模扩展性,对比三种经典 I/O 多路复用接口。
省流
select、poll、epoll都用于在一个进程里同时监视多个文件描述符的可读、可写或异常事件。差别主要在监视集合如何表示、内核如何返回就绪集合、以及在大量连接下的时间与空间开销。
select用固定长度位图(历史上常见上限约 1024),每次调用都要把完整集合从用户态拷入内核,返回后再由用户态线性扫描;poll用pollfd数组去掉了位图上限,但每次仍要拷贝并扫描整个数组。epoll把关心的描述符放进内核中的事件表,用epoll_ctl增删改,用epoll_wait只取当前就绪事件,避免每次全量扫描与全量拷贝。连接数少、事件稀疏时三者差别不大;连接数大、多数描述符长期空闲时,
epoll通常更合适。epoll是 Linux 特有接口;可移植代码仍可能使用select/poll或更高层封装。
三者在做什么?
I/O 多路复用的目标是:不必为每个连接阻塞一个线程,而是由内核告诉调用方“哪些描述符已经就绪”。
应用:我要监视 fd1, fd2, ... fdN
内核:其中一部分可读/可写了,把就绪集合还你
应用:只对就绪的 fd 做 read/write,再继续等待
对比
| 维度 | select | poll | epoll |
|---|---|---|---|
| 监视集合 | fd_set 位图 | pollfd 数组 | 内核事件表 + 就绪队列 |
| 描述符上限 | 受 FD_SETSIZE 等限制 | 无固定位数上限 | 主要受系统资源限制 |
| 每次等待时的输入 | 整份集合拷入内核 | 整份数组拷入内核 | 一般不必重传全部集合 |
| 返回结果 | 原地改写位图,需扫描 | 原地改 revents,需扫描 | 直接给出就绪事件列表 |
| 时间复杂度(常见说法) | O(n) 扫描 | O(n) 扫描 | 与就绪数量更相关 |
| 平台 | 较通用 | 较通用 | Linux |
select
调用前在用户态设置关心的读/写/异常集合;内核检查后把结果写回同一集合。应用必须再次遍历全部关心的描述符,判断哪一位被置位。
问题:
- 位图大小有上限,扩大描述符范围不方便。
- 每次调用都要重新传入完整集合。
- 返回后仍要 O(n) 扫描,活跃连接比例很低时浪费明显。
poll
用 struct pollfd { fd; events; revents; } 数组代替位图,去掉了固定位数限制,语义更清晰。但“每次全量拷贝 + 全量检查/扫描”的模式没有本质改变。
epoll
拆成三步:
epoll_create:创建内核中的 epoll 实例。epoll_ctl:把某个 fd 的关心事件注册、修改或删除到该实例。epoll_wait:阻塞等待,返回当前就绪的一批事件。
内核为 epoll 维护红黑树(或等价结构)保存监视集合,并用就绪链表收集已触发事件。因此平常增删是针对单个 fd,等待时主要处理已经就绪的那一部分。
怎么选?
- 少量连接、可移植性优先:
select/poll足够。 - Linux 上大量长连接、多数时间空闲:优先
epoll。 - 还要考虑触发模式(水平触发 / 边缘触发)、非阻塞 I/O 与正确的读写循环;接口更快不能替代业务侧的正确性。