1.1 什么是 I/O 完成机制
Linux 中所谓“I/O 完成”,本质上是内核通知用户空间:“你提交的数据操作已经结束,可以继续处理后续逻辑”。这里的“结束”既可能是数据已经到达内核缓冲区,也可能是 DMA 已经完成,甚至可能是硬件真正完成了物理传输。
很多开发者会误以为 read/write 调用返回就意味着硬件操作完成,但实际上 Linux 中大量 I/O 都存在缓存、队列与异步执行。例如 write(socket) 返回时,数据可能仍停留在 TCP send buffer;write(serial) 返回时,UART FIFO 甚至还没开始发送。因此 Linux I/O 模型核心并不是“执行函数”,而是“什么时候认为 I/O 已完成”。
1.2 Linux 为什么需要异步完成机制
最早 Linux 大量采用同步阻塞模型:
这种模型实现简单,但线程会长时间阻塞。随着网络服务器、NVMe SSD、高速 DMA、工业实时系统出现,CPU 开始面临两个问题:大量线程切换,以及高并发 I/O 导致的睡眠唤醒开销,因此 Linux 后续逐渐引入:
poll/epollaioio_uringcompletionworkqueue
这些机制的核心目标都只有一个:让 CPU 不再等待 I/O,而是“提交任务后继续执行,完成时再通知”。
第二章 阻塞 I/O 与等待队列机制
2.1 阻塞 read 的底层原理
Linux 阻塞 I/O 的核心是 wait queue。以字符设备 read 为例:
wait_event_interruptible(dev->wq,data_ready);
如果 data_ready 不成立,当前进程会进入 TASK_INTERRUPTIBLE 状态,然后从运行队列移除。此时 CPU 可以调度其他线程运行,当硬件数据到达后,驱动 IRQ 会调用:
wake_up_interruptible(&dev->wq);
等待线程重新进入 runnable 状态。整个机制本质上属于“条件等待模型”,CPU 不再主动轮询,而是由事件驱动唤醒。
2.2 Linux 睡眠与唤醒链路
Linux I/O 阻塞链路:
用户 read ↓驱动发现无数据 ↓加入 waitqueue ↓schedule() ↓线程睡眠
硬件中断到达后:
IRQ Handler ↓写入 ringbuffer ↓wake_up_interruptible ↓调度器重新唤醒线程
因此 Linux 阻塞 I/O 真正关键的不是 read,而是 wait queue 与 scheduler 的配合。Linux 内核大量异步机制最终都会回到“等待队列 + 唤醒”这个基础模型。
第三章 completion 完成量机制
3.1 completion 的设计目的
Linux completion 是一种轻量级同步完成机制,专门用于:等待某件事情完成,例如:
等待 DMA 完成等待 firmware 加载等待线程退出等待硬件初始化
相比 semaphore,completion 更强调“一次性完成通知”,而不是资源计数。其本质上属于 Linux 内核中的“事件同步器”,核心结构:
struct completion { unsigned int done; wait_queue_head_t wait;};
done 表示完成状态,wait 表示等待队列。
3.2 completion 使用流程
典型使用方法:
DECLARE_COMPLETION(done);wait_for_completion(&done);
另一个线程或 IRQ:complete(&done);
wait_for_completion 内部本质上仍然会进入 wait queue 睡眠,而 complete 会唤醒等待线程。因此 completion 其实是 wait queue 的封装,优势在于语义更明确, DMA 驱动中:
提交 DMA ↓睡眠等待 ↓DMA IRQ ↓complete()
相比直接操作 wait queue,completion 更适合表达“任务完成”这一语义。
第四章 Linux 异步 I/O(AIO)机制
4.1 传统同步 I/O 的问题
同步 I/O 最大问题是线程阻塞。例如:read(fd, buf, size);,调用后线程必须等待数据返回。对于磁盘或网络 I/O,这意味着大量线程会处于 sleep 状态,高并发服务器如果采用:一个连接一个线程会迅速产生上下文切换风暴。因为 Linux 调度器需要维护海量 task_struct,而真正运行的线程可能只有少数几个,所以Linux 引入 AIO:
线程无需等待真正的数据完成。
4.2 Linux AIO 的内部实现
Linux 原生 AIO 主要面向 block device:
io_submit()io_getevents()
用户提交 IOCB:
用户态 ↓内核 aio context ↓block layer ↓DMA/磁盘
I/O 完成后,block layer 将 event 放入 completion queue,用户通过 io_getevents 获取结果,但 Linux 传统 AIO 存在明显缺陷:
仅支持部分文件系统实现复杂依赖线程模拟socket 支持弱
因此长期以来 Linux 网络服务器仍然主要依赖 epoll,而不是 AIO。
第五章 io_uring 现代 I/O 完成架构
5.1 io_uring 为什么出现
Linux io_uring 本质上是对传统 AIO 的重构。它试图统一:
最大目标是减少:
传统 epoll + read/write 模型中,用户与内核需要频繁切换:
而 io_uring 希望通过共享 ringbuffer 完成“批量提交 + 批量完成”。(关于io_uring 见我的另外一篇文字)
5.2 SQ/CQ 双环结构
io_uring 最核心设计:SQ (Submission Queue) CQ (Completion Queue),用户态向 SQ 提交请求:
内核处理完成后向 CQ 写入结果。整个过程通过 mmap 共享 ringbuffer,大量减少 syscall 开销,完整流程:
用户提交 SQE ↓内核消费 SQ ↓执行 I/O ↓生成 CQE ↓用户读取完成事件
其本质已经接近用户态驱动模型,Linux 正在逐渐从“同步系统调用架构”转向“共享队列架构”。
第六章 DMA 与硬件 I/O 完成机制
6.1 DMA 完成通知原理
DMA 是 Linux I/O 完成机制中最典型场景。CPU 提交 DMA 后:
配置 DMA Controller ↓DMA 自主搬运数据 ↓CPU 继续执行
DMA 完成后触发 IRQ:
DMA IRQ ↓complete()或 wake_up()
等待线程随后恢复运行,因此 DMA 是:
这正是 Linux 异步 I/O 思想的核心。
6.2 Linux DMAEngine 完成回调
Linux DMAEngine 通常采用 callback 模式:desc->callback = dma_done;DMA IRQ 到达后:
IRQ Handler ↓tasklet/workqueue ↓执行 callback
callback 中通常会:complete();wake_up();通知上层 subsystem 数据已经可用。
例如 UART DMA:
UART RX DMA ↓DMA buffer 满 ↓IRQ ↓唤醒 TTY 层
网络驱动、音频驱动、摄像头驱动大量依赖这一机制。
第七章 Linux I/O 完成机制工程实践
7.1 工业系统中的 I/O 完成模型
工业 Linux 系统最典型的架构:
IRQ ↓ringbuffer ↓waitqueue ↓epoll ↓业务线程
主要有:
都通过异步完成机制统一进入事件循环。CPU 永远不会主动等待硬件,而是让硬件完成后主动通知软件,这也是 Linux 能够同时处理:网络 存储 串口 图形 音频等的大规模并发事件的根本原因。
7.2 现代 Linux I/O 架构趋势
Linux I/O 正在逐渐从:阻塞模型转向:
传统 read/write 更强调“函数调用”,而现代 io_uring 更强调“任务提交与完成回调”,未来 Linux I/O 架构重点会继续围绕:
减少 syscall减少 wakeup减少 copy减少 context switch
展开。因为现代 CPU 的真正瓶颈已经不再是计算能力,而是内核同步与缓存一致性开销。Linux I/O 完成机制的发展就是不断降低“等待成本”的过程。
建了一个嵌入式Linux技术群,专门聊难题分析和求职面试,欢迎大家一起加入,共同解决工作中的疑难杂症问题