I/O多路复用
I/O多路复用
I/O 多路复用 = 一个线程 / 进程,同时监听多个 I/O 对象的状态变化
通过一个系统调用,让内核同时监视多个文件描述符(fd),
当其中任意一个 fd 满足条件(可读 / 可写 / 异常)时,返回通知用户态
为什么需要 I/O 多路复用?
Linux 中的 I/O 特性
read / write默认是阻塞的- socket / pipe / eventfd / timerfd 本质上都是 fd
常见错误模型
| 模型 | 问题 |
|---|---|
| 一个 fd 一个线程 | 线程爆炸、上下文切换高 |
| while + 非阻塞轮询 | CPU 空转 |
| 阻塞 read 顺序读 | 谁慢卡死谁 |
I/O 多路复用解决了什么?
I/O 多路复用做了三件事:
- 把“等待”交给内核
- 让进程只在“有事发生”时被唤醒
- 一次返回“已经就绪的 fd 集合”
抽象模型
1 | ┌──────────┐ |
select —— 最原始的 I/O 多路复用
1️⃣ select 的工作方式
- 用户传入:
- 读 fd 集合
- 写 fd 集合
- 异常 fd 集合
- 内核:
- 遍历所有 fd
- 检查状态
- 返回:
- 修改后的 fd 集合(哪些就绪)
2️⃣ select 的核心特点
| 特点 | 说明 |
|---|---|
| fd 表示方式 | 位图(fd_set) |
| fd 上限 | FD_SETSIZE(通常 1024) |
| 内核扫描 | 全量扫描 |
| 数据拷贝 | 每次调用都拷贝 |
📌 时间复杂度:O(n)
3️⃣ select 的适用场景
- 老代码维护
- fd 数量极少(<10)
- 教学 / 调试
poll —— select 的结构改良版
1️⃣ poll 的工作方式
- 使用
struct pollfd[] - 每个 fd 自带关心的事件
- 内核遍历整个数组
2️⃣ poll 相比 select 的改进
| 改进点 | 说明 |
|---|---|
| fd 数量 | 理论无限 |
| 数据结构 | 更清晰 |
| 接口易用性 | 更好 |
3️⃣ poll 的本质问题
| 问题 | 是否解决 |
|---|---|
| 每次传 fd | ❌ |
| 全量扫描 | ❌ |
| O(n) 复杂度 | ❌ |
📌 poll 是“结构改进”,不是“机制升级”
epoll —— Linux 的事件驱动模型
1️⃣ epoll 的核心思想
把 fd 注册给内核,由内核长期维护“监听列表”和“就绪队列”
2️⃣ epoll 的三步模型
① 创建 epoll 实例
1 | epoll_create1(0); |
② 注册 / 修改 fd
1 | epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev); |
③ 等待事件
1 | epoll_wait(epfd, events, maxevents, timeout); |
3️⃣ epoll 为什么快?
| 机制 | 说明 |
|---|---|
| 红黑树 | 管理监听 fd |
| 就绪链表 | 只存“已就绪 fd” |
| 回调机制 | fd 状态变化时主动上报 |
| 无重复拷贝 | fd 不反复传入 |
📌 复杂度 ≈ O(就绪 fd 数量)
4️⃣ epoll 的两种触发模式
LT(水平触发,默认)
- 数据没读完 → 一直通知
- 安全稳定
ET(边沿触发)
- 只通知一次
- 必须非阻塞 + 读到 EAGAIN
5️⃣ epoll 的适用场景
| 场景 | 推荐 |
|---|---|
| 多 socket | ✅ |
| IPC / Unix socket | ✅ |
| Redis / ROS2 | ✅ |
| 嵌入式 Linux | ✅ |
整体对比总结表(工程视角)
| 维度 | select | poll | epoll |
|---|---|---|---|
| 最大 fd | 1024 | 无限 | 无限 |
| 性能 | 差 | 中 | 高 |
| 内核扫描 | 全量 | 全量 | 就绪 |
| 并发能力 | 低 | 中 | 高 |
| Linux 推荐 | ❌ | ⚠️ | ✅ |
内核扫描
一、内核扫描到底“扫”的是什么?
扫的不是数据,而是 状态
内核检查的是:
- socket 接收队列有没有数据?
- socket 发送缓冲区有没有空间?
- pipe 是否可读?
- fd 是否异常?
📌 本质是:
检查 fd 对应内核对象的状态标志
二、select / poll 中的“内核扫描”过程
以 poll 为例(select 本质一样):
1 | poll(fds, nfds, timeout); |
内核会做:
1 | for (i = 0; i < nfds; i++) { |
关键点
- 每一次 poll 调用
- 每一个 fd
- 无论它是否有数据
👉 这就叫 内核扫描
三、为什么内核扫描是性能瓶颈?
1️⃣ 时间复杂度:O(n)
- 100 个 fd → 扫 100 次
- 10 万个 fd → 扫 10 万次
哪怕只有 1 个 fd 真正有数据
内核也必须把 10 万个全扫一遍
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 涵风 Blog!

