I/O 模型(阻塞/非阻塞/多路复用)

I/O 模型是操作系统处理“等待数据 + 拷贝数据”这两个阶段的不同策略,核心区别在于进程/线程在哪个阶段被卡住。下面按经典分类讲清楚,顺带把常见误解一次理清。


一、I/O 的两个阶段(必须先懂)

一次网络 I/O(比如 recv)通常分两步:

  1. 等待数据就绪:内核等待网卡收到数据,放入内核缓冲区
  2. 数据拷贝:内核把数据从内核缓冲区拷到用户缓冲区

不同 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
上一篇
下一篇