【Linux】五种IO模型与非阻塞IO 完整详解
Linux 中,进程与外部设备(网络、磁盘、终端等)交互时,IO 操作的实现方式有五种经典模型。这五种模型是理解 Linux IO 性能、阻塞/非阻塞、同步/异步的核心基础,也是高频面试题。
一、先明确两个关键概念
- 阻塞(blocking) vs 非阻塞(non-blocking)
→ 关注的是进程在发起 IO 请求时,是否会立即返回 - 阻塞:调用 read/write 等函数时,如果数据没准备好,进程会被挂起(进入睡眠态),直到数据就绪
- 非阻塞:调用时如果数据没准备好,立即返回一个错误(通常是 EAGAIN/EWOULDBLOCK),进程不会睡眠
- 同步(synchronous) vs 异步(asynchronous)
→ 关注的是谁来完成数据从内核到用户空间的拷贝 - 同步:用户进程自己负责把数据从内核缓冲区拷贝到用户缓冲区
- 异步:内核完成拷贝后通知用户进程(信号、回调等)
二、Linux 五种经典 IO 模型
| 模型序号 | 模型名称 | 是否阻塞调用 | 是否阻塞拷贝 | 是否同步/异步 | 实际使用场景占比(现代系统) | 典型系统调用组合 |
|---|---|---|---|---|---|---|
| 1 | 阻塞式 IO(Blocking IO) | 是 | 是 | 同步 | ★★★★☆(最传统) | read / write |
| 2 | 非阻塞式 IO(Non-blocking IO) | 否 | 是 | 同步 | ★★☆☆☆(轮询开销大) | read + O_NONBLOCK + 轮询 |
| 3 | IO 多路复用(IO Multiplexing) | 否(select/poll/epoll 阻塞) | 是 | 同步 | ★★★★★(主流) | select / poll / epoll |
| 4 | 信号驱动式 IO(Signal-driven IO) | 否 | 是 | 同步 | ★☆☆☆☆(几乎不用) | sigaction + SIGIO |
| 5 | 异步 IO(Asynchronous IO) | 否 | 否 | 异步 | ★★★★☆(高性能场景) | aio_read / aio_write / io_uring |
三、每种模型详细原理与代码示例
1. 阻塞式 IO(最传统、最简单)
int fd = socket(...);
int n = read(fd, buf, sizeof(buf)); // 阻塞,直到有数据或错误
- 进程调用 read 时,如果内核缓冲区没有数据 → 进程进入睡眠(D 状态)
- 数据到达内核 → 内核拷贝到用户缓冲区 → 唤醒进程
- 优点:简单,代码最少
- 缺点:一个线程只能处理一个连接,高并发时需要大量线程
2. 非阻塞式 IO + 轮询(最容易理解的“非阻塞”)
int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
while (1) {
int n = read(fd, buf, sizeof(buf));
if (n > 0) {
// 读到数据
break;
} else if (n < 0 && errno == EAGAIN) {
// 没数据,继续轮询
usleep(10000); // 避免 CPU 100%
} else {
// 错误
break;
}
}
- 特点:read 立即返回,不会让进程睡眠
- 致命缺点:轮询导致 CPU 占用极高(忙等待)
- 结论:纯非阻塞 IO 几乎不用,真正高并发都搭配 IO 多路复用
3. IO 多路复用(目前最主流的模型)
三种实现方式:select → poll → epoll(Linux 主流用 epoll)
// epoll 经典用法(边缘触发 ET 模式)
int epfd = epoll_create1(0);
struct epoll_event ev, events[1024];
ev.events = EPOLLIN | EPOLLET; // 边缘触发
ev.data.fd = listenfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);
while (1) {
int nfds = epoll_wait(epfd, events, 1024, -1);
for (int i = 0; i < nfds; i++) {
if (events[i].data.fd == listenfd) {
// 接受新连接
int connfd = accept(...);
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = connfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);
} else if (events[i].events & EPOLLIN) {
// 读数据
int n = read(events[i].data.fd, buf, sizeof(buf));
// 处理
}
}
}
三种多路复用对比
| 模型 | 数据结构 | 最大描述符数 | 每次调用复杂度 | 事件触发方式 | 是否支持边缘触发 | 现代推荐度 |
|---|---|---|---|---|---|---|
| select | fd_set | 1024(硬编码) | O(n) | 水平触发 | 否 | ★★☆☆☆ |
| poll | pollfd 数组 | 无限制 | O(n) | 水平触发 | 否 | ★★★☆☆ |
| epoll | 红黑树 + 就绪链表 | 无限制 | O(1) + O(就绪数) | 水平/边缘触发 | 是 | ★★★★★ |
epoll 两大工作模式(面试必问):
- LT(水平触发):只要缓冲区有数据就一直通知(默认)
- ET(边缘触发):状态变化时只通知一次(性能更高,但必须一次性读完)
4. 信号驱动式 IO(几乎不用)
signal(SIGIO, handler);
fcntl(fd, F_SETOWN, getpid());
fcntl(fd, F_SETFL, O_ASYNC | O_NONBLOCK);
- 数据就绪时内核发送 SIGIO 信号
- 缺点:信号队列有限、高并发信号风暴、编程复杂
- 现代基本被 epoll 取代
5. 异步 IO(AIO / io_uring)—— 真正的异步
POSIX AIO(aio_read / aio_write)
struct aiocb cb;
cb.aio_fildes = fd;
cb.aio_buf = buf;
cb.aio_nbytes = sizeof(buf);
aio_read(&cb);
// 稍后检查
aio_error(&cb); // 检查是否完成
aio_return(&cb); // 获取结果
io_uring(Linux 5.1+,目前最强异步 IO)
- 提交队列 + 完成队列
- 零拷贝、批量提交
- Nginx、Redis 等现代软件已大量采用
六、总结对比表(面试必背)
| 模型 | 进程是否阻塞在发起调用 | 数据拷贝是否阻塞进程 | 是否需要轮询 | 是否真正异步 | 性能排序(高并发场景) |
|---|---|---|---|---|---|
| 阻塞 IO | 是 | 是 | 否 | 否 | ★★☆☆☆ |
| 非阻塞 IO | 否 | 是 | 是 | 否 | ★☆☆☆☆ |
| IO 多路复用 | 否(select/epoll 阻塞) | 是 | 否 | 否 | ★★★★★ |
| 信号驱动 IO | 否 | 是 | 否 | 否 | ★★☆☆☆ |
| 异步 IO (AIO/io_uring) | 否 | 否 | 否 | 是 | ★★★★★★ |
七、一句话总结
- 阻塞 IO:最简单,但高并发时线程爆炸
- 非阻塞 + 轮询:CPU 爆炸,几乎不用
- IO 多路复用(epoll):目前主流,单线程高并发王者
- 异步 IO(io_uring):未来趋势,内核帮你完成所有拷贝
如果你想深入某个模型的代码实现(比如 epoll ET/LT 对比、io_uring 入门、select vs epoll 性能测试等),可以告诉我,我可以继续给出详细示例代码和分析。