一文彻底搞懂 I/O 模型:BIO、NIO、多路复用、信号驱动、异步
网络 I/O 模型是高并发服务器开发的核心知识点,尤其在 Java、Netty、Nginx、Redis 等技术栈中反复出现。
Linux 操作系统提供了 5 种经典 I/O 模型(《UNIX 网络编程》定义):
- 阻塞式 I/O(Blocking I/O) → BIO
- 非阻塞式 I/O(Non-blocking I/O) → 基础 NIO
- I/O 多路复用(I/O Multiplexing) → 主流 NIO 的核心
- 信号驱动式 I/O(Signal-driven I/O)
- 异步 I/O(Asynchronous I/O) → AIO
下面用最直观的图 + 流程 + 对比帮你彻底搞懂。
1. 两个关键阶段(理解所有模型的钥匙)
任何一次网络读写操作都分为两个阶段:
- 阶段1:等待数据到达内核缓冲区(等待就绪)
→ 这个阶段叫 “等待数据可读/可写” - 阶段2:把数据从内核缓冲区拷贝到用户缓冲区(或反过来)
→ 这个阶段叫 “数据拷贝”
同步 vs 异步:看阶段2 是否由用户线程亲自完成拷贝
阻塞 vs 非阻塞:看阶段1 是否会让线程挂起(sleep)
2. 五种 I/O 模型对比图 + 说明
| 模型 | 阶段1(等待就绪) | 阶段2(拷贝数据) | 是否同步 | 是否阻塞 | 线程利用率 | Java 对应实现 | 实际使用场景 |
|---|---|---|---|---|---|---|---|
| 阻塞 I/O (BIO) | 阻塞 | 阻塞 | 同步 | 阻塞 | 极低 | ServerSocket + Socket | 低并发、简单场景 |
| 非阻塞 I/O | 轮询(立即返回) | 阻塞 | 同步 | 非阻塞 | 低 | Socket.setSoTimeout(0) | 极少单独使用 |
| I/O 多路复用 | 阻塞(内核帮轮询) | 阻塞 | 同步 | 非阻塞 | 高 | Selector / epoll | 高并发主流(Netty) |
| 信号驱动 I/O | 异步信号通知 | 阻塞 | 同步 | 非阻塞 | 中 | SIGIO 信号 | 极少使用 |
| 异步 I/O (AIO) | 异步(内核完成) | 异步(内核完成) | 异步 | 非阻塞 | 极高 | AsynchronousSocketChannel | 高吞吐、长连接场景 |
3. 详细拆解每一种(带流程)
1. 阻塞式 I/O(BIO,最传统)
用户线程 → recvfrom() → 内核(数据没到就挂起线程)
← 数据到达 → 内核拷贝到用户缓冲区 → 返回
- 最简单、最常用(早期所有 socket 默认)
- 一个连接 → 一个线程
- 线程在 accept()、read()、write() 时都阻塞
- 高并发时线程爆炸(C10K 问题)
Java 典型代码:
ServerSocket server = new ServerSocket(8080);
while (true) {
Socket socket = server.accept(); // 阻塞
new Thread(() -> {
InputStream in = socket.getInputStream();
in.read(...); // 阻塞
}).start();
}
2. 非阻塞式 I/O(轮询方式)
while (true) {
recvfrom() → 数据没到?立刻返回 EAGAIN
→ 继续轮询
→ 数据到了 → 拷贝 → 返回
}
- 线程不会挂起,但 CPU 疯狂空转(轮询)
- 几乎没人单独用,通常作为多路复用的基础
3. I/O 多路复用(目前最主流)
一句话:一个线程监控多个 socket,哪个就绪了再去处理哪个。
内核提供三种实现方式(性能递增):
| 方式 | 数据结构 | 最大 fd 数 | 时间复杂度(查询就绪) | 水平触发 / 边缘触发 | Linux 主流度 |
|---|---|---|---|---|---|
| select | fd_set(位图) | 1024 | O(n) | 水平 | 很老 |
| poll | pollfd 数组 | 无硬限 | O(n) | 水平 | 一般 |
| epoll | 红黑树 + 就绪链表 | 无硬限 | O(1) + O(就绪个数) | 水平 / 边缘 | 最高 |
Java NIO 的 Selector 底层就是 epoll(Linux) / kqueue(macOS) / IOCP(Windows)
流程:
Selector selector = Selector.open();
channel1.register(selector, OP_READ);
channel2.register(selector, OP_READ);
while (true) {
selector.select(); // 阻塞,直到至少一个 channel 就绪
Set<SelectionKey> keys = selector.selectedKeys();
处理就绪的 key...
}
4. 信号驱动式 I/O(很少用)
开启 socket 的信号驱动功能
设置 SIGIO 信号处理函数
内核数据到达 → 发 SIGIO 信号给进程
进程收到信号 → 调用 recvfrom() 去拷贝数据(仍阻塞)
- 阶段1 异步通知,但阶段2 还是同步阻塞
- 实际使用极少(信号队列溢出、不可靠)
5. 异步 I/O(AIO,真正的异步)
用户线程 → aio_read() → 立即返回
内核负责:等待数据 + 拷贝到用户缓冲区
数据准备好 → 内核发信号 / 回调通知用户线程
- 用户线程全程不阻塞
- 内核完成所有工作(阶段1 + 阶段2)
- Java 中通过
AsynchronousSocketChannel实现(NIO.2)
Java AIO 典型代码:
AsynchronousServerSocketChannel server = AsynchronousServerSocketChannel.open();
server.bind(new InetSocketAddress(8080));
server.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
public void completed(AsynchronousSocketChannel client, Void att) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
client.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
// 读取完成回调
});
}
});
4. 同步 vs 异步 终极区分(最容易混淆)
- 同步:用户线程必须亲自参与阶段2(数据拷贝)
- 异步:用户线程把所有事都交给内核,完成后被通知
所以:
- BIO、NIO、非阻塞、IO多路复用、信号驱动 → 都是同步
- 只有 AIO → 才是真正的异步
5. 总结对比表(面试/理解神器)
| 模型 | 线程数(高并发) | CPU 利用率 | 编程复杂度 | 吞吐量 | 典型框架/场景 |
|---|---|---|---|---|---|
| BIO | 连接数 | 低 | 低 | 低 | Tomcat(默认BIO模式) |
| NIO + Selector | 少量(1~few) | 高 | 中 | 高 | Netty、Mina、Redis |
| AIO | 少量 | 高 | 高 | 极高 | 高吞吐文件服务器 |
| 多路复用 epoll | 少量 | 极高 | 中 | 极高 | Nginx、Netty 默认 |
6. 一句话总结
- BIO:简单粗暴,一个连接一个线程,线程爆炸
- NIO:同步非阻塞 + 多路复用,一个线程搞定海量连接(主流)
- AIO:真正的异步,内核全包,编程复杂但性能极致
- 信号驱动:鸡肋,几乎不用
- 多路复用:目前性价比最高(epoll 是王者)
下一篇文章可以深入 Netty 如何基于 Reactor + epoll 实现高性能,或者对比 select/poll/epoll 的源码级差异。
有哪部分还想再细讲?(比如 epoll ET/LT 区别、Netty Reactor 线程模型、AIO 真实性能表现等)欢迎留言~