操作系统的 IO 模型有哪些?
一次网络读(recv)实际分两个阶段:等待数据就绪和把数据从内核缓冲区拷到用户缓冲区。
按这两个阶段谁来等、谁来拷,历史上归纳出五种模型:阻塞、非阻塞、IO 多路复用、信号驱动、异步 IO。
前四种都至少要自己完成拷贝,只有最后的异步 IO 两个阶段全交给内核。
1. 五种模型的时间线
阻塞 IO app: recv──────(睡)──────────┬─拷贝─┬─返回
kernel: 等待数据就绪 └──────┘
非阻塞 IO app: recv──EAGAIN──recv──EAGAIN──recv─┬─拷贝─┬─返回
kernel: 等待数据就绪 (用户轮询烧 CPU) └──────┘
多路复用 app: select──(睡)──就绪返回──recv─┬─拷贝─┬─返回
kernel: 同时等待 N 个 fd └──────┘
信号驱动 app: 注册 SIGIO──继续干活────────(信号)──recv─┬─拷贝─┬─返回
kernel: 等待数据就绪 ──────────────┘ └──────┘
异步 IO app: aio_read──返回──(完全不管)──────────────┬─完成事件
kernel: 等待数据就绪 ──────── 拷贝到用户缓冲区 ┘
2. 对比表
| 模型 | 等待阶段 | 拷贝阶段 | 系统调用次数 | 并发能力 | 典型接口 |
|---|---|---|---|---|---|
| 阻塞 IO | 进程睡眠 | 进程参与 | 1 | 差,一连接一线程 | read/recv |
| 非阻塞 IO | 用户轮询 | 进程参与 | N(反复) | 差,空转烧 CPU | O_NONBLOCK |
| IO 多路复用 | 内核代等(可等多个) | 进程参与 | 2 | 好,单线程管万连接 | select/poll/epoll |
| 信号驱动 | 内核代等,信号通知 | 进程参与 | 2 | 一般,信号难管理 | SIGIO + fcntl |
| 异步 IO | 内核全包 | 内核全包 | 1~2 | 最好,真异步 | io_uring、IOCP、POSIX AIO |
判断"异步"的硬标准只有一条:函数返回时数据是否已经躺在你的缓冲区里。
epoll 返回时只是告诉你"可读了",所以你写的仍是同步代码。
3. 代码:多路复用的骨架
int ep = epoll_create1(0);
struct epoll_event ev = {.events = EPOLLIN | EPOLLET, .data.fd = listen_fd};
epoll_ctl(ep, EPOLL_CTL_ADD, listen_fd, &ev);
struct epoll_event evs[64];
for (;;) {
int n = epoll_wait(ep, evs, 64, -1); // 阻塞在这一处,只为等待"就绪"
for (int i = 0; i < n; i++) {
int fd = evs[i].data.fd;
if (fd == listen_fd) {
accept4(listen_fd, NULL, NULL, SOCK_NONBLOCK); // 新连接设非阻塞
} else {
ssize_t r;
while ((r = read(fd, buf, sizeof buf)) > 0) { // 拷贝仍由本线程做
handle(buf, r);
}
if (r == 0 || (r < 0 && errno != EAGAIN)) close(fd);
}
}
}
# 快速确认某个进程用的是哪种模型
strace -f -e trace=epoll_wait,read,write,io_uring_enter -p 1234 2>&1 | head -30
cat /proc/1234/stack # 阻塞在哪个内核函数
4. 常见追问
Q:epoll 能不能监听普通文件?
不能。普通文件的读写不经过 socket 层,poll 永远返回就绪(磁盘数据总"可读"),
多路复用对文件没意义。文件 IO 想异步只能用 io_uring 或线程池 + 阻塞读。
Q:为什么说 Redis 单线程也很快?
它把"等待"全部交给 epoll,把"处理"做成纯内存操作,单次处理只有微秒级,瓶颈根本不在 CPU;
真正的耗时是网络往返,用多线程反而增加锁和上下文切换。
Q:io_uring 和 epoll 的本质区别?
epoll 是就绪通知(readiness),内核只告诉你"能读了";io_uring 是完成通知(completion),
你把"读 4KB 到 buf"整条命令投进提交队列,内核做完把结果写进完成队列,用户态通过共享内存环收割,
连系统调用都能批量省掉。
小结
五种模型的分水岭是"拷贝阶段归谁"。工程上 99% 的网络服务选多路复用 + 非阻塞 fd, 因为它在并发和可控性之间最平衡;对极致吞吐或文件 IO,才上异步 IO。 把上面的时间线和对比表记住,再补一句"epoll 只解决等待,不解决拷贝",这一题就很稳。