【Linux】五种IO模型与非阻塞IO

【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 + 轮询
3IO 多路复用(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));
            // 处理
        }
    }
}

三种多路复用对比

模型数据结构最大描述符数每次调用复杂度事件触发方式是否支持边缘触发现代推荐度
selectfd_set1024(硬编码)O(n)水平触发★★☆☆☆
pollpollfd 数组无限制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)★★★★★★

七、一句话总结

  • 阻塞 IO:最简单,但高并发时线程爆炸
  • 非阻塞 + 轮询:CPU 爆炸,几乎不用
  • IO 多路复用(epoll):目前主流,单线程高并发王者
  • 异步 IO(io_uring):未来趋势,内核帮你完成所有拷贝

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

文章已创建 4391

发表回复

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

相关文章

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

返回顶部