蔚淦丞
发布于 2026-09-04 / 9 阅读
0
0

Select/Poll使用总结

一、核心总览

select / poll 是 Linux 经典水平触发(LT) IO多路复用模型。

核心作用:单线程监听多个文件描述符,阻塞等待IO事件,避免忙轮询,大幅提升并发能力。

两者事件触发机制、内核调度、快照逻辑、唤醒机制完全一致,唯一区别是数据结构与限制。

二、Select 完整原理与特性

1. 底层数据结构:位图(fd_set)

fd_set 本质是一个 long 数组,不是整数,不能直接位运算、不能下标索引。

  • 默认大小:1024 bit,对应 fd 0~1023

  • 每一个 bit 代表一个 fd 的监听/就绪状态

  • 64位系统:16个 long(16*64=1024bit)

必须使用 FD_ISSET / FD_SET / FD_CLR 操作,禁止手动 1<<fd、禁止 fd_set[fd] 索引(溢出、未定义行为)。

2. Select 致命特性(高频考点)

  • 内核会直接销毁用户态位图:select 返回后,原图监听集合被覆盖,只剩下就绪位。

  • 必须每次循环select重新拷贝原始监听位图,不能复用。

  • 存在 fd≤1023 硬上限,超过直接内存越界。(是fd的值有上限,不是fd的数量,也就是说fd的数量更少)

  • 遍历开销 O(N):必须遍历 0~maxfd 所有位查找就绪fd。

三、Poll 完整原理与特性

1. 底层数据结构:struct pollfd 数组

struct pollfd {
    int fd;        // 用户设置,内核只读、永不修改
    short events;  // 用户监听事件,内核只读、永不修改
    short revents; // 内核输出事件,每次 poll 回填
};

2. Poll 核心优势(对比 select)

  • 无1023 fd上限

  • 不会破坏监听配置:fd、events 永久保留,可全程复用数组

  • 只需遍历有效监听fd,不需要遍历0~maxfd

3. Poll 唯一坑点

revents 不会自动清零,残留旧事件会导致虚假就绪。

解决方案:每次遍历结束顺手清零 / 每次poll前批量清零。

四、内核统一执行时序(Select/Poll 完全一致)

这是本次学习最核心、最本质的底层流程,彻底打通所有疑惑。

  1. 进入系统调用:内核拷贝用户监听数据到内核副本

  2. 第一轮扫描:如果已有就绪fd,直接返回

  3. 无就绪则休眠:线程挂起,让出CPU,无任何轮询

  4. IO事件唤醒线程:第一个到达的IO事件唤醒休眠线程(只唤醒一次)

  5. 唤醒后第二轮全局扫描:遍历所有监听fd,生成本次快照

  6. 快照定型,不可回溯:扫描过的fd,后续新事件本轮不再收录

  7. 拷贝结果回用户态,系统调用返回

关键结论

  • 休眠期间累积的多条连接,会被一次扫描全部打包

  • 扫描过程中新到来的IO事件,分两种情况判定:未遍历到的fd,新事件可被本轮快照收录;已经遍历完成的fd,本轮不再回溯收录,交由下一轮LT上报处理

  • 扫描单个fd时,socket内部有单fd自旋锁,保证状态读取原子性;无全局大锁

五、水平触发 LT 终极理解(Select/Poll 默认唯一模式)

触发原则:看「当前状态」,不看「状态变化」

  • 只要内核缓冲区/队列不为空,每一次 select/poll 都会上报就绪

  • 一次网络事件 → 可能触发多次poll上报

  • 不会丢失事件,只会延迟一轮上报

对比边沿触发ET:仅状态切换瞬间上报一次,不取干净永久丢失(epoll专属)

六、工程致命坑点(本次学习重点避坑)

1. 绝对禁止:检测就绪后,主线程立刻重入poll,把处理丢给子线程

场景:主线程poll就绪 → 抛给worker线程accept → 主线程马上再次poll

后果:

  • 连接还在内核队列未被取走,fd仍然就绪

  • LT重复上报,重复派发任务

  • 阻塞listen fd直接卡死工作线程(二次accept队列为空永久阻塞)

2. 两种正确架构铁律

方案1(业界标准半Reactor,推荐)

主线程 poll/select + 主线程 accept 榨干队列 → 将 client-fd 丢工作队列

优势:当场消费就绪状态,无重复上报、无卡死、无需增删fd

方案2(子线程处理accept,不推荐)

检测到listen就绪 →立刻从poll数组移除该fd → 交给worker处理 → 处理完再加回

3. Listen fd 必须循环 accept 榨干队列

一次poll就绪不代表只有一条连接,队列可能积压多条。

必须设置非阻塞listen fd,循环accept直到 EAGAIN。

七、Select vs Poll 终极对比

特性

select

poll

底层结构

1024bit 位图

pollfd 结构体数组

fd上限

≤1023(硬限制)

无上限

监听数据是否保留

不保留,每次必须重制

永久保留,可复用

需要重置内容

重置整张位图

仅清零 revents

遍历范围

0~maxfd 全遍历

仅遍历监听数组

触发模式

水平触发 LT

水平触发 LT

唤醒机制

事件唤醒、一次唤醒批量处理

完全一致

八、最终黄金总结

  1. select/poll 无定时轮询,只在进入调用、被唤醒后两次扫描fd。

  2. 快照是串行遍历生成的,已扫描fd的后续新事件,本轮不处理,下一轮LT重新上报。

  3. 内核只有单fd自旋锁,无全局锁,保证单状态读取原子性。

  4. LT机制:缓冲区不空就一直上报,是重复上报的根本原因。

  5. 工程规范:谁检测IO,谁就地消费干净,再重入多路复用;异地处理必须先移除监听fd。

  6. 优先使用 poll 替代 select,无1023限制、代码更干净、坑更少。


评论