Linux IO 模型:阻塞、非阻塞、多路复用与异步 IO 全解析

1 ⚡ 问题切入:一个后端开发者必须回答的问题

假设你在面试中被问到: “一台 4 核 8GB 的服务器,为什么能支撑 10 万个并发连接?”

答案的关键不在于 CPU 有多快、内存有多大,而在于 IO 模型 。如果每个连接用一个线程、每个线程做阻塞 IO,10 万连接就需要 10 万个线程——每个线程消耗约 1MB 栈空间,仅线程栈就占 100GB 内存,4 核 CPU 也根本无法调度这么多线程。

真正让高并发成为可能的,是 非阻塞 IOIO 多路复用 (I/O Multiplexing,单个线程同时监听多个 IO 事件)。Nginx、Redis、Netty 的高性能都建立在正确的 IO 模型选择之上。

这篇博客从操作系统层面讲解 Linux 五大 IO 模型,聚焦于"数据如何从网卡/磁盘到达你的程序",为后续理解 Java NIO、Netty、Kafka 等框架打下理论基础。

2 💻 硬件架构:一次 IO 操作经历了什么

在讨论 IO 模型之前,必须先理解一次 IO 操作涉及哪些硬件组件以及数据如何流转。

Linux IO 硬件架构

如上图所示,一次典型的网络 IO 读取,数据经过以下路径:

步骤操作参与者说明
1网卡收到数据包NIC(网卡)硬件中断通知 CPU
2DMA 拷贝到内核DMA 控制器 → Socket Buffer不经过 CPU,直接内存访问
3内核协议栈处理TCP/IP 协议栈解析 TCP 头、重组数据、校验
4CPU 拷贝到用户空间Socket Buffer → 用户 BufferCPU 执行 copy_to_user()
5应用程序读取用户进程从用户 Buffer 读取数据并处理

两个核心概念

  • DMA Copy (Direct Memory Access,直接内存访问):硬件设备直接将数据写入内存,不经过 CPU。发生时 CPU 可以做其他事情,仅在传输完成时收到一个中断
  • CPU Copy :CPU 执行指令将数据从内核缓冲区复制到用户缓冲区(copy_to_user() / copy_from_user()),CPU 被占用

整个 IO 过程可以分为 两个阶段

  1. 等待数据 (Wait for Data):等待网卡收到数据、DMA 传输完成、内核协议栈处理完毕
  2. 拷贝数据 (Copy Data):内核缓冲区 → 用户缓冲区(CPU Copy)

五大 IO 模型的区别,本质上就是对这两个阶段的处理方式不同

3 🗺️ Linux 五大 IO 模型总览

flowchart LR
    %% ==========================================
    %% 五大IO模型分类
    %% ==========================================
classDef root fill:#0f172a,stroke:#3b82f6,stroke-width:2px,color:#bfdbfe,font-weight:bold;
classDef branch fill:#2d1a05,stroke:#f59e0b,stroke-width:2px,color:#fde68a,font-weight:bold;
classDef leaf fill:#1e1e24,stroke:#6b7280,stroke-width:1.5px,color:#e5e7eb;
classDef highlight fill:#450a0a,stroke:#dc2626,stroke-width:1.5px,color:#fecaca,font-weight:bold;

    ROOT[Linux IO 模型\n按两阶段处理方式分类]

    ROOT --> B1(同步IO)
    B1 --> M1["🔵 阻塞IO\n两个阶段都阻塞"]
    B1 --> M2["🟢 非阻塞IO\n阶段1轮询\n阶段2阻塞"]
    B1 --> M3["🟡 IO多路复用\nselect/epoll阻塞\n单线程监听多fd"]
    B1 --> M4["🟣 信号驱动IO\n阶段1信号通知\n阶段2阻塞"]

    ROOT --> B2(异步IO)
    B2 --> M5["🔴 异步IO\n两个阶段都不阻塞\n内核完成后回调"]

    class ROOT root;
    class B1,B2 branch;
    class M1,M2,M4 leaf;
    class M3,M5 highlight;

同步与异步的区分标准同步 IO 是指应用程序主动发起 IO 操作并等待(或轮询)其完成,在数据从内核缓冲区拷贝到用户缓冲区期间,应用程序线程参与其中。 异步 IO 是指应用程序发起 IO 操作后立即返回,内核完成所有工作(包括拷贝数据到用户空间),然后通知应用程序。

4 🔴 阻塞 IO(Blocking IO)

4.1 📖 原理

阻塞 IO 是最简单、最直观的模型。应用程序调用 recv(),内核在 两个阶段都阻塞

  • 阶段 1(等待数据):如果 Socket 缓冲区中没有数据,进程/线程被挂起,加入 等待队列 (Wait Queue,内核数据结构 wait_queue_head_t,存储等待此事件的进程列表),直到数据到达后被唤醒
  • 阶段 2(拷贝数据):内核将数据从 Socket 缓冲区拷贝到用户缓冲区,进程/线程在这期间也是阻塞的
sequenceDiagram
    %% ==========================================
    %% 阻塞IO时序图
    %% ==========================================
    participant APP as 应用程序
    participant KERNEL as 内核
    participant NIC as 网卡

    APP->>KERNEL: recv(sockfd, buf, len, 0)

    Note over KERNEL: 将进程加入等待队列
    Note over APP: 🔴 进程阻塞\n等待数据

    NIC->>KERNEL: 数据到达 + DMA 传输
    Note over KERNEL: 数据写入Socket Buffer
    Note over KERNEL: 协议栈处理完成

    KERNEL-->>APP: 唤醒进程
    Note over KERNEL: 🔴 进程仍阻塞\n拷贝数据: Socket Buffer → 用户Buffer
    KERNEL->>APP: recv() 返回 (数据已就绪)

    Note over APP: ✅ 进程继续执行

4.2 💻 C API 示例

#include <sys/socket.h>
#include <unistd.h>

void blocking_io_example() {
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    // ... connect to server ...

    char buf[4096];
    // 阻塞等待:没有数据就一直等,进程被挂起
    ssize_t n = recv(sockfd, buf, sizeof(buf), 0);
    // 只有收到数据或出错时才返回

    if (n > 0) {
        write(STDOUT_FILENO, buf, n); // 处理数据
    }
    close(sockfd);
}

代码解读recv() 默认是阻塞的(flags=0 表示阻塞模式)。如果 Socket 缓冲区为空,当前进程/线程会被操作系统挂起,直到数据到达。期间 CPU 可以调度其他进程运行 ,这是阻塞 IO 唯一的性能红利——进程阻塞时不占 CPU。

4.3 ⚠️ 特点与瓶颈

优点缺点
编程模型简单,代码易读一个线程只能处理一个连接
进程阻塞时不占 CPU高并发时需要大量线程
适合连接数少的场景线程切换开销大,内存消耗大

后端开发者应该记住 :传统 Tomcat/BIO 模式就是阻塞 IO——每个请求分配一个线程,请求处理完之前线程一直被占用。当并发连接达到数千时,线程数爆炸,性能急剧下降。

5 🟡 非阻塞 IO(Non-Blocking IO)

5.1 📖 原理

通过 fcntl() 将 Socket 设为 非阻塞模式O_NONBLOCK),recv() 的行为改变:

  • 阶段 1(等待数据): 立即返回 。如果数据未就绪,返回 -1errno=EAGAIN
  • 阶段 2(拷贝数据):如果数据就绪,仍然阻塞完成 CPU Copy

应用程序需要 主动轮询 (Polling):反复调用 recv() 检查数据是否就绪。

sequenceDiagram
    %% ==========================================
    %% 非阻塞IO时序图
    %% ==========================================
    participant APP as 应用程序
    participant KERNEL as 内核

    loop 轮询阶段
        APP->>KERNEL: recv() (非阻塞)
        KERNEL-->>APP: 返回 -1, errno=EAGAIN
        Note over APP: 进程继续运行\n做其他事情
        Note over APP: 等待一段时间...
    end

    APP->>KERNEL: recv() (非阻塞)
    Note over KERNEL: 数据已就绪
    Note over KERNEL: 🔴 拷贝数据\nSocket Buffer → 用户Buffer
    KERNEL->>APP: recv() 返回 N (成功读取N字节)

    Note over APP: ✅ 处理数据

5.2 💻 C API 示例

#include <fcntl.h>
#include <errno.h>

void nonblocking_io_example() {
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);

    // 设置为非阻塞模式
    int flags = fcntl(sockfd, F_GETFL, 0);
    fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);

    char buf[4096];
    while (1) {
        ssize_t n = recv(sockfd, buf, sizeof(buf), 0);

        if (n > 0) {
            // 成功读取到数据
            write(STDOUT_FILENO, buf, n);
            break;
        } else if (n == -1 && errno == EAGAIN) {
            // 数据未就绪,做一些其他事情
            // 然后继续轮询
            usleep(1000); // 等1ms再试
        } else {
            // 真正的错误
            break;
        }
    }
    close(sockfd);
}

代码解读 :设置 O_NONBLOCK 后,recv() 不再阻塞。数据未就绪时返回 -1,需要检查 errno 是否为 EAGAINEWOULDBLOCK(两者值相同)。如果是,说明只是暂时没有数据;如果是其他值,说明真的出错了。

5.3 ⚠️ 问题:轮询浪费 CPU

非阻塞 IO 的最大问题是 忙轮询 (Busy Polling):在数据未就绪期间,应用程序反复调用 recv(),虽然每次立即返回,但 频繁的系统调用本身消耗 CPU 。如果有 1000 个连接要做非阻塞检查,每轮询一遍就是 1000 次系统调用。

// 用 strace 可以看到大量 EAGAIN 返回
// strace -e trace=recvfrom ./nonblocking_app 2>&1 | head -20
//
// recvfrom(3, 0x..., 4096, 0, ...) = -1 EAGAIN
// recvfrom(3, 0x..., 4096, 0, ...) = -1 EAGAIN
// recvfrom(3, 0x..., 4096, 0, ...) = -1 EAGAIN
// ... 大量无效调用 ...

这就引出了 IO 多路复用—— 让内核来帮忙检查哪些连接就绪,一次系统调用检查所有连接

6 🟢 IO 多路复用(IO Multiplexing)

6.1 💡 核心思想

IO 多路复用的核心思想是: 用一个系统调用,让内核同时监控多个文件描述符(fd),当至少一个 fd 就绪时返回,应用程序再对有数据的 fd 做真正的 read() / recv()

sequenceDiagram
    %% ==========================================
    %% IO多路复用 select/epoll 时序图
    %% ==========================================
    participant APP as 应用程序(单线程)
    participant KERNEL as 内核
    participant FD1 as Socket fd1
    participant FD2 as Socket fd2
    participant FD3 as Socket fd3

    APP->>KERNEL: epoll_wait(epfd, events, max, timeout)
    Note over KERNEL: 监控 fd1, fd2, fd3
    Note over APP: 🔴 线程阻塞在 epoll_wait

    FD1->>KERNEL: fd1 数据到达
    KERNEL-->>APP: epoll_wait 返回\n就绪: [fd1, fd3]

    Note over APP: 🟢 进程恢复运行
    APP->>KERNEL: recv(fd1, ...)
    Note over KERNEL: 拷贝数据(阶段2阻塞)
    KERNEL->>APP: 返回数据

    APP->>KERNEL: recv(fd3, ...)
    KERNEL->>APP: 返回数据

    Note over APP: 处理完所有就绪fd\n重新调用 epoll_wait

6.2 📈 select / poll / epoll 演进

Linux 提供了三种 IO 多路复用接口,按出现顺序分别是 select、poll、epoll:

flowchart TD
    %% ==========================================
    %% select/poll/epoll 演进
    %% ==========================================
classDef startEnd fill:#701a4c,stroke:#e11d48,stroke-width:2px,color:#fce7f3,font-weight:bold;
classDef process fill:#1e1e24,stroke:#6b7280,stroke-width:1.5px,color:#e5e7eb;
classDef highlight fill:#450a0a,stroke:#dc2626,stroke-width:1.5px,color:#fecaca,font-weight:bold;
classDef reject fill:#450a0a,stroke:#dc2626,stroke-width:1.5px,color:#fecaca,font-weight:bold;

    START([IO多路复用演进]) --> SELECT

    subgraph S1 ["select (1983, 4.2BSD)"]
        SELECT["🔴 select()\n• fd_set 位图,最多1024个fd\n• O(N)遍历:每次调用重传整个集合\n• 修改传入的fd_set"]
    end

    SELECT --> POLL

    subgraph S2 ["poll (1997, SVR3)"]
        POLL["🟡 poll()\n• pollfd 结构体数组,无数量上限\n• O(N)遍历:每次仍要重传\n• 分离 events 和 revents"]
    end

    POLL --> EPOLL

    subgraph S3 ["epoll (2002, Linux 2.6)"]
        EPOLL["🟢 epoll()\n• 红黑树+就绪链表,无上限\n• O(1)获取就绪事件\n• 事件驱动,fd只需注册一次"]
    end

    class START startEnd;
    class SELECT reject;
    class POLL process;
    class EPOLL highlight;

6.3 📊 三者的核心区别

特性selectpollepoll
数据结构fd_set 位图(默认 1024 bits)struct pollfd[] 数组内核红黑树 + 就绪链表
fd 上限FD_SETSIZE(1024,可重编译)无上限(受系统限制)无上限(受系统限制)
fd 注册每次调用都传入全部 fd每次调用都传入全部 fd一次注册(epoll_ctl),持久有效
就绪查找O(N) 遍历所有 fdO(N) 遍历所有 fdO(1) 直接从就绪链表取
内核态数据结构每次重新构建每次重新构建红黑树持久,事件驱动回调
触发方式水平触发水平触发水平触发 + 边缘触发

6.4 💻 epoll API 示例

#include <sys/epoll.h>

void epoll_example() {
    // 1. 创建 epoll 实例
    int epfd = epoll_create1(0);   // 返回 epoll 文件描述符

    // 2. 注册要监控的 fd
    struct epoll_event ev, events[64];
    ev.events = EPOLLIN;           // 监控可读事件(数据到达)
    ev.data.fd = sockfd;           // 关联的 fd
    epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);

    // 3. 事件循环
    while (1) {
        // 阻塞等待事件,timeout=-1 表示无限等待
        int nfds = epoll_wait(epfd, events, 64, -1);

        // 只处理就绪的 fd —— O(1) 级别
        for (int i = 0; i < nfds; i++) {
            if (events[i].events & EPOLLIN) {
                int fd = events[i].data.fd;
                char buf[4096];
                ssize_t n = recv(fd, buf, sizeof(buf), 0);
                if (n > 0) {
                    // 处理数据
                }
            }
        }
    }
    close(epfd);
}

代码解读

  1. epoll_create1(0) 在内核中创建一个 eventpoll 对象,包含一棵 红黑树rbr,存储注册的 fd)和一个 就绪链表rdllist,存储就绪的事件)
  2. epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev) 将 fd 注册到红黑树中,同时向内核协议栈注册一个 回调函数ep_poll_callback)——当数据到达时,内核自动将事件加入就绪链表
  3. epoll_wait() 检查就绪链表,如果有事件直接返回。每次只传递发生事件的那几个 fd(events 数组),而不是全部 fd

epoll 高性能的本质回调 + 就绪链表 。fd 注册一次后永久有效,数据到达时由内核回调自动将事件加入就绪链表。epoll_wait() 不需要遍历所有监视的 fd,只需要检查就绪链表,真正的 O(1) 获取。

6.5 ⚡ 水平触发 vs 边缘触发

stateDiagram-v2
    %% ==========================================
    %% LT vs ET 触发模式
    %% ==========================================
    DATA_READY: 📥 数据到达\nSocket缓冲区有数据

    state "📤 LT(水平触发)\nepoll_wait 持续通知\n直到数据被读完" as LT
    state "📤 ET(边缘触发)\nepoll_wait 仅通知一次\n必须循环读直到EAGAIN" as ET

    DATA_READY --> LT
    DATA_READY --> ET

    LT --> READ1_LT: 应用 read() 部分数据
    READ1_LT --> LT: epoll_wait 再次通知\n(缓冲区还有数据)

    ET --> READ1_ET: 应用 read() 部分数据
    READ1_ET --> LOST: epoll_wait 不再通知\n(若没读完则丢失)
模式行为要求适用场景
水平触发(LT,默认)只要缓冲区有数据,epoll_wait() 就反复通知可以一次只读部分数据简单,不易出错
边缘触发(ET)只在状态变化时(无数据→有数据)通知一次必须循环读,直到 EAGAIN,fd 必须设为非阻塞高性能,配合非阻塞 IO

ET 模式必须用非阻塞 IO + 循环读取,代码更复杂但性能更高——减少了 epoll_wait 的调用次数。

7 🟣 信号驱动 IO(Signal-Driven IO)

7.1 📖 原理

通过 sigaction() + fcntl(F_SETOWN) + fcntl(F_SETFL, O_ASYNC) 设置。当 Socket 数据就绪时,内核发送 SIGIO 信号给进程。进程在 信号处理函数 中调用 recv() 读取数据。

  • 阶段 1(等待数据):进程继续运行,不阻塞。数据就绪时内核发信号
  • 阶段 2(拷贝数据):在信号处理函数中执行 recv() 时仍然阻塞
sequenceDiagram
    %% ==========================================
    %% 信号驱动IO时序图
    %% ==========================================
    participant APP as 应用程序
    participant SIG as 信号处理函数
    participant KERNEL as 内核
    participant NIC as 网卡

    APP->>KERNEL: sigaction(SIGIO, handler)
    APP->>KERNEL: fcntl(fd, F_SETFL, O_ASYNC)
    Note over APP: 🟢 进程继续运行\n不阻塞

    NIC->>KERNEL: 数据到达
    KERNEL-->>APP: SIGIO 信号
    Note over APP: 中断当前执行流

    APP->>SIG: 进入信号处理函数
    SIG->>KERNEL: recv(fd, buf, len, 0)
    Note over KERNEL: 🔴 拷贝数据期间阻塞
    KERNEL->>SIG: 返回数据
    SIG-->>APP: 信号处理完成

    Note over APP: ✅ 继续之前的工作

7.2 💻 C API 示例

#include <signal.h>
#include <fcntl.h>

void sigio_handler(int signo) {
    char buf[4096];
    // 在信号处理函数中执行 recv (复杂且容易出错)
    ssize_t n = recv(global_sockfd, buf, sizeof(buf), 0);
    // ...
}

void signal_driven_io_example() {
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);

    // 注册信号处理函数
    struct sigaction sa = { .sa_handler = sigio_handler };
    sigaction(SIGIO, &sa, NULL);

    // 设置 fd 的所有者(谁接收信号)
    fcntl(sockfd, F_SETOWN, getpid());

    // 启用异步通知
    int flags = fcntl(sockfd, F_GETFL, 0);
    fcntl(sockfd, F_SETFL, flags | O_ASYNC);

    // 进程继续做其他事情,数据到达时自动触发 sigio_handler
    while (1) {
        // 做其他工作...
    }
}

7.3 ⚠️ 为什么信号驱动 IO 很少用

  1. 信号处理函数限制多 :在信号处理函数中只能调用"异步信号安全"的函数(recv() 不是,使用它是一种灰色地带)
  2. 信号不可靠 :多个数据到达时信号可能合并,导致只触发一次
  3. 无法知道是哪个 fd 就绪 :需要遍历所有 fd 检查
  4. 调试困难 :信号的异步特性增加了程序的复杂性

JDK 的 NIO 框架没有采用信号驱动模型,而是使用了 IO 多路复用(epoll / kqueue)。

8 🔵 异步 IO(Asynchronous IO)

8.1 📖 原理

Linux 通过 io_submit() + aio_read() 实现真正的异步 IO。应用程序发起 IO 请求后 立即返回 ,内核完成 两个阶段 (等待数据 + 拷贝数据)后,通过 回调信号 通知应用程序。

  • 阶段 1 和阶段 2:内核全部完成, 应用程序完全不被阻塞
sequenceDiagram
    %% ==========================================
    %% 异步IO时序图
    %% ==========================================
    participant APP as 应用程序
    participant KERNEL as 内核
    participant NIC as 网卡

    APP->>KERNEL: aio_read(&iocb)
    Note over APP: 🟢 立即返回\n进程继续运行

    Note over KERNEL: 内核接管一切
    NIC->>KERNEL: 数据到达

    Note over KERNEL: 阶段1: 等待数据\n(由内核完成)
    Note over KERNEL: 阶段2: 拷贝到用户空间\n(由内核完成)

    KERNEL-->>APP: 回调/信号通知\n数据已在用户缓冲区中

    Note over APP: ✅ 直接使用数据\n无需调用 recv()

8.2 💻 C API 示例

#include <linux/aio_abi.h>
#include <sys/syscall.h>

void aio_example() {
    aio_context_t ctx = 0;
    // 1. 创建异步IO上下文
    syscall(SYS_io_setup, 128, &ctx);

    // 2. 准备读取缓冲区
    char buf[4096];
    struct iocb cb = {0};
    cb.aio_fildes = fd;          // 文件描述符
    cb.aio_lio_opcode = IOCB_CMD_PREAD;
    cb.aio_buf = (uint64_t)buf;  // 用户缓冲区地址
    cb.aio_nbytes = sizeof(buf);
    cb.aio_offset = 0;

    struct iocb *cbs[] = {&cb};
    // 3. 提交异步读请求 —— 立即返回!
    syscall(SYS_io_submit, ctx, 1, cbs);

    // 4. 进程继续做其他事情...
    // 做业务逻辑、处理其他请求等

    // 5. 查询是否完成(或设置回调/信号通知)
    struct io_event ev;
    syscall(SYS_io_getevents, ctx, 1, 1, &ev, NULL);
    // ev.res 包含实际读取的字节数
    // ev.obj->aio_buf 就是之前传入的 buf,数据已在其中
}

8.3 🔮 异步 IO 的现状

Linux 原生异步 IO(AIO) 只对 O_DIRECT 方式打开的文件有效 ,即绕过 Page Cache 的直接 IO。对于普通文件(使用 Page Cache 缓冲),AIO 实际上仍然是阻塞的。这使得 Linux 原生 AIO 的适用范围很窄(主要用在数据库直接读写裸设备)。

io_uring(Linux 5.1+, 2019)是新一代异步 IO 接口,通过 共享内存环形队列 (Submission Queue + Completion Queue)实现真正的零拷贝异步 IO,比 AIO 更高效、更通用,是 Linux 异步 IO 的未来方向。

9 🎯 五大模型对比总结

flowchart TD
    %% ==========================================
    %% 五大模型阶段对比
    %% ==========================================
classDef startEnd fill:#701a4c,stroke:#e11d48,stroke-width:2px,color:#fce7f3,font-weight:bold;
classDef process fill:#1e1e24,stroke:#6b7280,stroke-width:1.5px,color:#e5e7eb;
classDef highlight fill:#450a0a,stroke:#dc2626,stroke-width:1.5px,color:#fecaca,font-weight:bold;
classDef block fill:#450a0a,stroke:#dc2626,stroke-width:1.5px,color:#fecaca,font-weight:bold;
classDef nblock fill:#052e16,stroke:#16a34a,stroke-width:1.5px,color:#bbf7d0,font-weight:bold;

    %% ==========================================
    %% 决策树
    %% ==========================================
    START([发起IO操作]) --> Q1{阶段1\n等待数据?}

    Q1 -- 自己等 --> Q1A{怎么等?}
    Q1A -->|"死等"| B_IO["🔴 阻塞IO\n两阶段均阻塞\n最简单"]
    Q1A -->|"轮询"| NB_IO["🟡 非阻塞IO\n阶段1轮询\n阶段2阻塞"]
    Q1A -->|"内核帮我看多个fd"| MP_IO["🟢 IO多路复用\nepoll_wait等待\n阶段2逐fd读取"]

    Q1 -- 不用我等 --> Q1B{阶段2\n拷贝数据?}
    Q1B -->|"信号通知后自己拷"| SIG_IO["🟣 信号驱动IO\n阶段1非阻塞\n阶段2阻塞"]
    Q1B -->|"内核全包"| AIO["🟠 异步IO\n两阶段均非阻塞\nio_uring"]

    class START startEnd;
    class Q1,Q1A,Q1B process;
    class B_IO block;
    class NB_IO,SIG_IO process;
    class MP_IO,AIO highlight;
模型阶段1(等数据)阶段2(拷贝)关键系统调用复杂度并发能力代表框架
阻塞IO阻塞阻塞read/recv传统Tomcat BIO
非阻塞IO轮询阻塞recv+fcntl无(一般不单独用)
多路复用select/epoll阻塞逐fd阻塞epoll_wait+recvNginx、Redis、Netty
信号驱动非阻塞(信号)阻塞sigaction+fcntl极高几乎不用
异步IO非阻塞非阻塞aio_read/io_uring极高极高io_uring (下一代)

9.1 📌 后端开发者应该记住的结论

  1. 阻塞 IO 只有一个线程一个连接的场景适合,高并发下不可行
  2. 非阻塞 IO 单独使用轮询成本高,需要配合多路复用
  3. IO 多路复用 是现代高并发服务器的核心——一个线程可以管理数万个连接。epoll 的 O(1) 就绪查找是 Redis 单线程高性能的关键
  4. 信号驱动 IO 实际应用很少,主要在嵌入式或特殊场景
  5. 异步 IO (io_uring)是下一代方向,但目前主流框架仍基于 epoll 多路复用构建

9.2 ☕ 在 Java 中的对应

Linux 模型Java 对应
阻塞 IOjava.io(传统 BIO),InputStream.read() 阻塞当前线程
IO 多路复用(epoll)java.nio.channels.Selector(NIO),底层在 Linux 上调用 epoll_wait
异步 IOjava.nio.channels.AsynchronousSocketChannel(AIO, Java 7+),但在 Linux 上层是 epoll + 线程池模拟

Java NIO 的 Selector 封装了 epoll(Linux)/ kqueue(macOS)/ IOCP(Windows),为 Java 开发者提供了统一的 IO 多路复用 API。而 Netty 框架进一步封装了 NIO,提供了事件驱动的编程模型,是目前 Java 高性能网络编程的事实标准。