一、IO 多路复用基础定义
文件描述符 FD:Linux 中一切皆文件,FD 是标识文件 /socket 的递增整数。 IO 多路复用:单线程监听多个 FD,当任意 FD 可读 / 可写时收到通知,避免阻塞 IO 卡死、非阻塞 IO 空转,提升 CPU 利用率。
对比:阻塞 IO 会卡死线程;非阻塞 IO 循环轮询耗 CPU;多线程方案线程开销大。多路复用就是 "一个服务员等多个顾客,谁准备好了就处理谁"。 两个阶段:①内核等待数据就绪;②内核把数据拷贝到用户空间。 三大实现:select、poll、epoll。select/poll 只告知 "有 fd 就绪",需要用户遍历查找;epoll 直接返回就绪 fd 集合。
二、select
1. 原理与结构
fd_set:用 bit 位图存储监听 fd,固定 1024bit,最多监听 fd 编号小于 1024 的文件描述符。bit 位 0 = 未监听 / 未就绪,1 = 就绪。- select 函数参数:
nfds:最大 fd 值 +1- readfds/writefds/exceptfds:读、写、异常事件集合
- timeout:超时控制(NULL 永久阻塞,0 非阻塞,>0 限时等待)
- 流程:
- 用户构造 fd_set 位图
- 用户态 → 内核态,拷贝整个 fd_set
- 内核遍历所有 fd,阻塞等待就绪
- 就绪后修改位图,拷贝回用户态
- 用户程序循环遍历位图,找出就绪 fd 执行 IO
2. select 三大缺点(高频考点)
- 拷贝开销大:每次调用都要把整个 fd_set 在用户、内核之间来回拷贝
- 需要全量遍历:只会通知有 fd 就绪,不知道是哪一个,用户态必须遍历全部 fd
- 上限 1024:位图固定大小,监听 fd 不能超过 1024
三、poll(select 改良版)
1. 结构
struct pollfd {int fd; short events; short revents;}
- fd:要监听的文件描述符
- events:感兴趣的事件(POLLIN 读、POLLOUT 写、POLLERR 错误)
- revents:内核返回、实际触发的事件
2. 执行流程
- 自定义大小 pollfd 数组,放入所有待监听 fd 和关心事件
- 调用 poll,数组拷贝进内核,内核用链表保存(无 1024 上限)
- 内核遍历全部 fd,等待就绪 / 超时
- 就绪后把 pollfd 数组拷贝回用户空间,返回就绪 fd 数量
- 用户判断 n>0,遍历 pollfd 数组,看 revents 找到就绪 fd
3. 和 select 对比
✅ 改进:抛弃固定位图,用数组 + 链表,解除 1024fd 上限 ❌ 遗留问题:
- 每次调用仍然需要全数组用户↔内核拷贝
- 内核依旧遍历全部 fd
- 返回后用户态仍然需要遍历数组查找就绪 fd
结论:poll 只是解决了 fd 数量限制,时间复杂度依旧 O (n),性能提升有限。
四、select vs poll 对比简表
表格
| 对比项 | select | poll |
|---|---|---|
| 存储结构 | 固定 bit 位图 fd_set | 动态 pollfd 数组 |
| FD 上限 | 最大 1024 | 无硬性上限 |
| 就绪通知 | 位图标记,遍历查找 | revents 标记,遍历查找 |
| 拷贝 | 每次全量拷贝位图 | 每次全量拷贝数组 |
| 时间复杂度 | O(n) | O(n) |
| 适用场景 | 少量 fd 场景,老旧兼容 | 中少量 fd,需要超过 1024 监听 |