一文彻底搞懂 I/O 模型:BIO、NIO、多路复用、信号驱动、异步

一文彻底搞懂 I/O 模型:BIO、NIO、多路复用、信号驱动、异步

网络 I/O 模型是高并发服务器开发的核心知识点,尤其在 Java、Netty、Nginx、Redis 等技术栈中反复出现。

Linux 操作系统提供了 5 种经典 I/O 模型(《UNIX 网络编程》定义):

  1. 阻塞式 I/O(Blocking I/O) → BIO
  2. 非阻塞式 I/O(Non-blocking I/O) → 基础 NIO
  3. I/O 多路复用(I/O Multiplexing) → 主流 NIO 的核心
  4. 信号驱动式 I/O(Signal-driven I/O)
  5. 异步 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 主流度
selectfd_set(位图)1024O(n)水平很老
pollpollfd 数组无硬限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 真实性能表现等)欢迎留言~

文章已创建 4455

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

相关文章

开始在上面输入您的搜索词,然后按回车进行搜索。按ESC取消。

返回顶部