一、核心总览
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 完全一致)
这是本次学习最核心、最本质的底层流程,彻底打通所有疑惑。
进入系统调用:内核拷贝用户监听数据到内核副本
第一轮扫描:如果已有就绪fd,直接返回
无就绪则休眠:线程挂起,让出CPU,无任何轮询
IO事件唤醒线程:第一个到达的IO事件唤醒休眠线程(只唤醒一次)
唤醒后第二轮全局扫描:遍历所有监听fd,生成本次快照
快照定型,不可回溯:扫描过的fd,后续新事件本轮不再收录
拷贝结果回用户态,系统调用返回
关键结论
休眠期间累积的多条连接,会被一次扫描全部打包
扫描过程中新到来的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 无定时轮询,只在进入调用、被唤醒后两次扫描fd。
快照是串行遍历生成的,已扫描fd的后续新事件,本轮不处理,下一轮LT重新上报。
内核只有单fd自旋锁,无全局锁,保证单状态读取原子性。
LT机制:缓冲区不空就一直上报,是重复上报的根本原因。
工程规范:谁检测IO,谁就地消费干净,再重入多路复用;异地处理必须先移除监听fd。
优先使用 poll 替代 select,无1023限制、代码更干净、坑更少。