I/O 模型是操作系统处理“等待数据 + 拷贝数据”这两个阶段的不同策略,核心区别在于进程/线程在哪个阶段被卡住。下面按经典分类讲清楚,顺带把常见误解一次理清。
一、I/O 的两个阶段(必须先懂)
一次网络 I/O(比如 recv)通常分两步:
- 等待数据就绪:内核等待网卡收到数据,放入内核缓冲区
- 数据拷贝:内核把数据从内核缓冲区拷到用户缓冲区
不同 I/O 模型的区别,本质就是这两步里,用户线程是不是被阻塞。
二、五种 I/O 模型(POSIX 标准分类)
1. 阻塞 I/O(Blocking I/O)
- 调用
read()后:- 数据没到 → 线程一直睡
- 数据到了 → 内核拷贝完 → 返回
- 两个阶段都阻塞
- 最简单,最常见(
socket默认模式)
int n = recv(fd, buf, len, 0); // 阻塞在这
✅ 编程简单
❌ 一个连接一个线程,高并发直接炸
2. 非阻塞 I/O(Non-blocking I/O)
fcntl(fd, F_SETFL, O_NONBLOCK)- 调用
read():- 数据没好 → 立刻返回
EAGAIN - 数据好了 → 拷贝数据,返回
- 数据没好 → 立刻返回
- 第一阶段不阻塞,第二阶段仍阻塞
while (true) {
int n = recv(fd, buf, len, 0);
if (n > 0) break;
if (errno == EAGAIN) continue; // 忙等
}
✅ 单线程能“试”多个 fd
❌ 忙轮询浪费 CPU
3. I/O 多路复用(IO Multiplexing)
代表:select / poll / epoll / kqueue
- 一个线程同时盯多个 fd
- 调用
epoll_wait():- 没事件 → 阻塞
- 有事件 → 返回就绪 fd 列表
- 真正读数据时还是
recv(可能阻塞,但通常配合非阻塞)
流程:
epoll_wait() ── 阻塞等事件
↓
recv() ── 读数据(一般非阻塞)
✅ 单线程扛几万连接(C10K 解法)
✅ 仍是同步 I/O(用户态自己拷数据)
⚠️ 多路复用 ≠ 异步 I/O
它只是“等”变成了批量等
4. 信号驱动 I/O(Signal-driven I/O)
fcntl(fd, F_SETOWN, pid)SIGIO通知数据就绪- 收到信号后再调用
recv()
现实里很少用,主要在老 UNIX / 某些嵌入式场景。
5. 异步 I/O(Asynchronous I/O,AIO)
代表:
- Linux:
io_uring(现代)、libaio(坑多) - Windows:
IOCP
特点:
- 调
aio_read() - 两阶段都不阻塞
- 内核搞定“等 + 拷”,完了再通知你
aio_read(&cb); // 立刻返回
// 内核拷完,回调/信号通知
✅ 真正的异步
❌ Linux 传统 AIO 不好用,io_uring 才翻身
三、一张表总结
| 模型 | 等数据 | 拷数据 | 是否同步 |
|---|---|---|---|
| 阻塞 I/O | 阻塞 | 阻塞 | 同步 |
| 非阻塞 I/O | 不阻塞 | 阻塞 | 同步 |
| I/O 多路复用 | 阻塞(批量) | 阻塞 | 同步 |
| 信号驱动 | 不阻塞 | 阻塞 | 同步 |
| 异步 I/O | 不阻塞 | 不阻塞 | 异步 |
📌 同步 I/O:数据拷贝必须由用户线程做
异步 I/O:内核全包了
四、工程上怎么选
- 低并发 / 简单服务:阻塞 I/O + 线程池
- 高并发网络服务:
epoll+ 非阻塞(Redis/Nginx) - 极致性能 / 新项目:
io_uring - Java 世界:
- BIO → NIO(
Selector)→ Netty - 底层仍是多路复用,不是真 AIO
- BIO → NIO(