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 |
高性能存储、零拷贝场景 |
1.2 阻塞 IO vs 非阻塞 IO
阻塞 IO 是最直观的模型:进程调用 read() 后,如果设备没有数据,进程进入 TASK_INTERRUPTIBLE 状态,让出 CPU,直到数据到达才被唤醒。优点是编程简单,缺点是进程在等待期间完全无法做其他工作。
非阻塞 IO 通过设置 O_NONBLOCK 标志,使 read() 在数据未就绪时立即返回 -1 并设置 errno 为 EAGAIN 或 EWOULDBLOCK。应用程序需要主动轮询,消耗 CPU 但保持了进程的响应性。
read()/write() 方法中检查 file->f_flags & O_NONBLOCK,若数据不可用且设置了非阻塞标志,则直接返回 -EAGAIN,而不是调用 wait_event_interruptible() 让进程睡眠。
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() 方法。这是内核为设备驱动提供的统一接口,也是本课程的核心内容。
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 的首选方案。
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 接口 |