【Linux】五种IO模型与非阻塞IO 完整详解

Linux 中,进程与外部设备(网络、磁盘、终端等)交互时,IO 操作的实现方式有五种经典模型。这五种模型是理解 Linux IO 性能、阻塞/非阻塞、同步/异步的核心基础,也是高频面试题。

一、先明确两个关键概念

  • 阻塞(blocking) vs 非阻塞(non-blocking)
    → 关注的是进程在发起 IO 请求时,是否会立即返回

    • 阻塞:调用 read/write 等函数时,如果数据没准备好,进程会被挂起(进入睡眠态),直到数据就绪
    • 非阻塞:调用时如果数据没准备好,立即返回一个错误(通常是 EAGAIN/EWOULDBLOCK),进程不会睡眠
  • 同步(synchronous) vs 异步(asynchronous)
    → 关注的是谁来完成数据从内核到用户空间的拷贝

    • 同步:用户进程自己负责把数据从内核缓冲区拷贝到用户缓冲区
    • 异步:内核完成拷贝后通知用户进程(信号、回调等)

二、Linux 五种经典 IO 模型

模型序号 模型名称 是否阻塞调用 是否阻塞拷贝 是否同步/异步 实际使用场景占比(现代系统) 典型系统调用组合
1 阻塞式 IO(Blocking IO) 同步 ★★★★☆(最传统) read / write
2 非阻塞式 IO(Non-blocking IO) 同步 ★★☆☆☆(轮询开销大) read + O_NONBLOCK + 轮询
3 IO 多路复用(IO Multiplexing) 否(select/poll/epoll 阻塞) 同步 ★★★★★(主流) select / poll / epoll
4 信号驱动式 IO(Signal-driven IO) 同步 ★☆☆☆☆(几乎不用) sigaction + SIGIO
5 异步 IO(Asynchronous IO) 异步 ★★★★☆(高性能场景) aio_read / aio_write / io_uring

三、每种模型详细原理与代码示例

1. 阻塞式 IO(最传统、最简单)
int fd = socket(...);
int n = read(fd, buf, sizeof(buf));   // 阻塞,直到有数据或错误
  • 进程调用 read 时,如果内核缓冲区没有数据 → 进程进入睡眠(D 状态)
  • 数据到达内核 → 内核拷贝到用户缓冲区 → 唤醒进程
  • 优点:简单,代码最少
  • 缺点:一个线程只能处理一个连接,高并发时需要大量线程
2. 非阻塞式 IO + 轮询(最容易理解的“非阻塞”)
int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);

while (1) {
    int n = read(fd, buf, sizeof(buf));
    if (n > 0) {
        // 读到数据
        break;
    } else if (n < 0 && errno == EAGAIN) {
        // 没数据,继续轮询
        usleep(10000);  // 避免 CPU 100%
    } else {
        // 错误
        break;
    }
}
  • 特点:read 立即返回,不会让进程睡眠
  • 致命缺点:轮询导致 CPU 占用极高(忙等待)
  • 结论:纯非阻塞 IO 几乎不用,真正高并发都搭配 IO 多路复用
3. IO 多路复用(目前最主流的模型)

三种实现方式:select → poll → epoll(Linux 主流用 epoll)

// epoll 经典用法(边缘触发 ET 模式)
int epfd = epoll_create1(0);
struct epoll_event ev, events[1024];

ev.events = EPOLLIN | EPOLLET;  // 边缘触发
ev.data.fd = listenfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);

while (1) {
    int nfds = epoll_wait(epfd, events, 1024, -1);
    for (int i = 0; i < nfds; i++) {
        if (events[i].data.fd == listenfd) {
            // 接受新连接
            int connfd = accept(...);
            ev.events = EPOLLIN | EPOLLET;
            ev.data.fd = connfd;
            epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);
        } else if (events[i].events & EPOLLIN) {
            // 读数据
            int n = read(events[i].data.fd, buf, sizeof(buf));
            // 处理
        }
    }
}

三种多路复用对比

模型 数据结构 最大描述符数 每次调用复杂度 事件触发方式 是否支持边缘触发 现代推荐度
select fd_set 1024(硬编码) O(n) 水平触发 ★★☆☆☆
poll pollfd 数组 无限制 O(n) 水平触发 ★★★☆☆
epoll 红黑树 + 就绪链表 无限制 O(1) + O(就绪数) 水平/边缘触发 ★★★★★

epoll 两大工作模式(面试必问):

  • LT(水平触发):只要缓冲区有数据就一直通知(默认)
  • ET(边缘触发):状态变化时只通知一次(性能更高,但必须一次性读完)
4. 信号驱动式 IO(几乎不用)
signal(SIGIO, handler);
fcntl(fd, F_SETOWN, getpid());
fcntl(fd, F_SETFL, O_ASYNC | O_NONBLOCK);
  • 数据就绪时内核发送 SIGIO 信号
  • 缺点:信号队列有限、高并发信号风暴、编程复杂
  • 现代基本被 epoll 取代
5. 异步 IO(AIO / io_uring)—— 真正的异步

POSIX AIO(aio_read / aio_write)

struct aiocb cb;
cb.aio_fildes = fd;
cb.aio_buf = buf;
cb.aio_nbytes = sizeof(buf);

aio_read(&cb);

// 稍后检查
aio_error(&cb);   // 检查是否完成
aio_return(&cb);  // 获取结果

io_uring(Linux 5.1+,目前最强异步 IO)

  • 提交队列 + 完成队列
  • 零拷贝、批量提交
  • Nginx、Redis 等现代软件已大量采用

六、总结对比表(面试必背)

模型 进程是否阻塞在发起调用 数据拷贝是否阻塞进程 是否需要轮询 是否真正异步 性能排序(高并发场景)
阻塞 IO ★★☆☆☆
非阻塞 IO ★☆☆☆☆
IO 多路复用 否(select/epoll 阻塞) ★★★★★
信号驱动 IO ★★☆☆☆
异步 IO (AIO/io_uring) ★★★★★★

七、一句话总结

如果你想深入某个模型的代码实现(比如 epoll ET/LT 对比、io_uring 入门、select vs epoll 性能测试等),可以告诉我,我可以继续给出详细示例代码和分析。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐