1. IO模型概述

在 Linux 内核驱动开发中,IO 模型决定了应用程序如何与设备进行数据交互。不同的 IO 模型在资源利用率、响应速度和编程复杂度上各有取舍。本章从概念出发,系统对比五种经典 IO 模型,为后续深入 非阻塞 IO 与 poll 机制 打下理论基础。

1.1 五种 IO 模型核心概念

IO 操作通常分为两个阶段:等待数据就绪 和 数据从内核拷贝到用户空间。不同模型在这两个阶段的行为差异,构成了它们的本质区别。

IO 模型 等待阶段行为 拷贝阶段行为 典型系统调用 适用场景
阻塞 IO 进程挂起,直到数据就绪 阻塞拷贝 read()、write() 简单、低并发设备
非阻塞 IO 立即返回,轮询检查 阻塞拷贝 read() + O_NONBLOCK 需要保持响应性的单线程
IO 多路复用 一次等待多个 fd,任一就绪即返回 阻塞拷贝 select()、poll()、epoll() 高并发网络服务器
信号驱动 IO 注册信号,数据就绪时异步通知 阻塞拷贝 fcntl() + SIGIO 低延迟、事件驱动应用
异步 IO 发起请求后立即返回 内核自动拷贝,完成后通知 aio_read()、io_uring 高性能存储、零拷贝场景
关键区分: 前四种模型在数据从内核拷贝到用户空间时都是阻塞的,只有异步 IO 连拷贝阶段也是非阻塞的。这是判断是否为"真正异步"的核心标准。

1.2 阻塞 IO vs 非阻塞 IO

阻塞 IO 是最直观的模型:进程调用 read() 后,如果设备没有数据,进程进入 TASK_INTERRUPTIBLE 状态,让出 CPU,直到数据到达才被唤醒。优点是编程简单,缺点是进程在等待期间完全无法做其他工作。

非阻塞 IO 通过设置 O_NONBLOCK 标志,使 read() 在数据未就绪时立即返回 -1 并设置 errno 为 EAGAIN 或 EWOULDBLOCK。应用程序需要主动轮询,消耗 CPU 但保持了进程的响应性。

内核视角: 驱动中实现非阻塞 IO 的关键是在 read()/write() 方法中检查 file->f_flags & O_NONBLOCK,若数据不可用且设置了非阻塞标志,则直接返回 -EAGAIN,而不是调用 wait_event_interruptible() 让进程睡眠。
阻塞 IO 与非阻塞 IO 流程对比 阻塞 IO:read(fd, buf, len) 数据未就绪 → 进程睡眠 等待唤醒(TASK_INTERRUPTIBLE) 数据就绪 → 唤醒进程 拷贝数据到用户空间 → 返回 非阻塞 IO:read(fd, buf, len) 数据未就绪 → 立即返回 -EAGAIN 应用程序轮询重试 数据就绪 → read() 成功 拷贝数据到用户空间 → 返回 阻塞 IO 在等待阶段让出 CPU,非阻塞 IO 通过轮询保持进程运行但消耗更多 CPU

1.3 IO 多路复用

IO 多路复用允许单个进程同时监视多个文件描述符,当其中任何一个 fd 就绪时才返回。它解决了阻塞 IO 中"一个线程只能处理一个连接"的问题,也避免了非阻塞 IO 中"对所有 fd 盲目轮询"的低效。

Linux 提供了三种多路复用机制:

  • select():最早出现,支持最大 fd 数量受 FD_SETSIZE 限制(通常 1024),每次调用需要将整个 fd 集合从用户空间拷贝到内核,效率随 fd 数量增加而下降。
  • poll():使用 pollfd 结构体数组,没有最大 fd 数量限制,但仍有全量拷贝和线性扫描的性能问题。
  • epoll():Linux 2.6 引入,使用事件驱动机制,只返回就绪的 fd,避免全量拷贝和扫描,适合处理大量并发连接。
驱动开发者注意: 无论上层使用 select()、poll() 还是 epoll(),最终都会调用驱动中的 poll() 方法。这是内核为设备驱动提供的统一接口,也是本课程的核心内容。
IO 多路复用核心架构 用户空间:应用程序调用 select() / poll() / epoll_wait() 内核空间:VFS 层调用各驱动注册的 poll() 方法 驱动 A:poll() 检查设备状态 驱动 B:poll() 检查设备状态 驱动 C:poll() 检查设备状态 返回就绪 fd 掩码 → 用户空间遍历或事件驱动处理

1.4 信号驱动 IO

信号驱动 IO 允许应用程序在数据就绪时通过信号(通常是 SIGIO)获得通知,无需主动轮询。应用程序通过 fcntl() 设置文件描述符的属主进程,并注册信号处理函数。当数据到达时,内核发送信号,应用程序在信号处理函数中调用 read() 读取数据。

这种模型在等待阶段是非阻塞的,但数据拷贝阶段仍然是阻塞的。它适合对延迟敏感且 IO 事件不频繁的场景,但信号处理函数中能做的事情有限,且信号可能丢失或排队溢出。

1.5 异步 IO

异步 IO(AIO)是真正的"非阻塞一切"模型。应用程序发起 aio_read() 或使用 io_uring 提交 IO 请求后立即返回,内核负责等待数据就绪并将数据拷贝到用户空间指定的缓冲区,完成后通过回调、信号或完成队列通知应用程序。

Linux 原生 AIO 最初主要支持 O_DIRECT 模式下的文件 IO,对普通文件缓存 IO 支持有限。近年来 io_uring 框架(Linux 5.1+)提供了更高效、更通用的异步 IO 接口,成为高性能 IO 的首选方案。

课程聚焦: 本课程重点讲解 非阻塞 IO 和 IO 多路复用 在设备驱动中的实现,因为这两者直接依赖驱动提供的 poll() 方法。信号驱动 IO 和异步 IO 虽然也涉及驱动层,但实现机制不同,将在后续进阶课程中展开。

1.6 五种模型对比总结

特性 阻塞 IO 非阻塞 IO IO 多路复用 信号驱动 IO 异步 IO
等待阶段是否阻塞 是 否(轮询) 是(但可同时等待多个 fd) 否(信号通知) 否
拷贝阶段是否阻塞 是 是 是 是 否
CPU 利用率 低(睡眠等待) 高(轮询消耗) 中(事件驱动) 低(信号触发) 低(回调完成)
并发处理能力 低(1 线程/1 fd) 中(1 线程轮询多 fd) 高(1 线程管理多 fd) 中(信号驱动) 高(提交-完成分离)
编程复杂度 低 中 高 中 高
驱动实现要求 实现 read/write 实现 read/write + 检查 O_NONBLOCK 实现 poll() 方法 实现 fasync() 方法 实现 aio_read/write 或 io_uring 接口