核心判断
写 DMA 驱动之前,最应该先看清的不是 API,而是数据路径和控制路径到底怎么分层。很多实现之所以写着写着就乱,不是因为函数不会调,而是因为脑子里根本没有一张全链路图。
这一章解决什么问题
你后面会碰到一堆名词:
- buffer
- descriptor
- doorbell
- completion
- ring
mmap
ioctl
- 中断
- poll
如果没有全景图,这些词就会像一堆散装零件;有了全景图,它们才会变成一台机器。
本章的任务很简单:
先把一次 DMA 传输从设备到用户态的完整链路画出来。
一张总图先压住全场
这张图里,最核心的不是“设备连内存”,而是下面三件事:
- 数据面:真正承载 payload 的 buffer
- 控制面:提交、完成、回收、错误处理
- 同步边界:CPU、设备、用户态之间何时能看见同一份最新数据
先分清数据面和控制面
数据面:真正承载数据的地方
数据面通常包括:
- 设备读的发送 buffer
- 设备写的接收 buffer
- ring buffer 中每个 slot 的 payload
- 用户态
mmap看到的那块共享区
数据面的目标通常是:
- 高吞吐
- 尽量少拷贝
- 可持续流转
控制面:保证这条通路不翻车
控制面通常包括:
- descriptor
- head / tail
- valid / done / owner 状态位
ioctl/poll/ 中断
- timeout / reset / reclaim
控制面的目标不是快,而是:
- 明确谁能提交
- 明确谁已完成
- 明确谁可以回收
- 明确异常怎么收口
一句话:
数据面负责搬东西,控制面负责防止大家同时动同一块东西。
一次 DMA 传输,从设备视角看是什么
场景 A:设备从内存读数据(TX)
比如 CPU/用户态准备好一块发送数据,让设备拿去发。
在这个路径里,设备的角色像一个消费者:
- CPU/用户先生产数据
- 驱动完成提交
- 设备消费 buffer
场景 B:设备把数据写入内存(RX)
比如音频采样、网络收包、ADC 连续采集。
在这个路径里,设备的角色像一个生产者:
- 驱动先准备空 buffer
- 设备往里写
- CPU/用户再来消费
这类 RX 场景,也是本专题的主线。因为它天然会引出 ring buffer、ownership、completion、零拷贝这些核心问题。
descriptor、doorbell、completion 到底各管什么
组件 | 作用 | 本质问题 |
descriptor | 描述某个 buffer 的地址、长度、控制位 | “设备该处理哪块内存?” |
doorbell | 通知设备有新任务可处理 | “现在可以开始干活了吗?” |
completion | 表示一次处理已完成 | “干完了吗?结果现在归谁?” |
descriptor 不是数据本身,而是任务说明书
descriptor 常常包含:
- DMA 地址
- 长度
- 控制标志
- ownership / valid / done 之类的状态位
别把 descriptor 理解成“附属结构”。很多 DMA 驱动真正的状态机,其实就藏在 descriptor 和 ring 推进语义里。
doorbell 是交接动作,不是业务逻辑
doorbell 可能是:
- 写某个寄存器
- 推进 producer index
- 设置某个位
它的语义通常是:
“驱动这边准备好了,你可以开始看了。”
completion 是所有收尾逻辑的起点
completion 可能来自:
- 硬件中断
- 状态寄存器轮询
- descriptor done bit
- DMAEngine 回调
completion 不是“整个系统结束”,而只是:
设备阶段结束,下一阶段可以开始交接。
从驱动视角看,一次传输究竟做了什么
驱动做的第一件事:组织内存
驱动需要先准备:
- DMA buffer
- descriptor/ring
- 同步原语
- 中断和完成通知路径
说白了,设备擅长搬,驱动擅长组织。
驱动做的第二件事:完成 ownership 交接
这一步包括:
- 什么时候 buffer 从 CPU 交给设备
- 什么时候设备交回给 CPU
- 什么时候 CPU 再交给用户态
这也是为什么后面的重头戏不是“分配内存”,而是“定义生命周期”。
驱动做的第三件事:把异常路径写完整
真实系统里,驱动必须准备面对:
- completion 丢失
- timeout
- reset
- 用户态不消费
- 进程提前退出
如果没有这些设计,你的 happy path 越漂亮,翻车时越壮观。
从用户态视角看,全链路意味着什么
当用户态介入时,系统会多出一层协作关系:
- 用户态可能通过
mmap直接看到数据面
- 用户态通过
ioctl/poll与驱动走控制面
- 用户态不应直接绕过协议修改正在被设备使用的 buffer
所以用户态不是“旁观者”,而是全链路里的第三个参与方。
从这一刻起,系统里至少有三方:
- 设备
- 驱动 / CPU
- 用户态
三方都能看到某些资源,但不是三方都能在任意时刻改它。这个限制,就是后面 ownership 状态机存在的理由。
一条典型的流式采集全链路
这条链路里,最值钱的观察不是“发生了几步”,而是:
- 数据面只在必要时被访问
- 控制面负责推进状态
- 每次推进都在做 ownership 切换
写驱动时最容易丢掉的全局视角
只盯着 DMA API,不看系统边界
结果就是:
- buffer 有了
- sync 也调了
- 但 nobody knows 下一步该谁动
这类代码最容易出现“偶发旧数据”“重复提交”“早回收”这类半鬼不鬼的问题。
只盯着设备,不看用户态接口
一旦需要零拷贝,用户态是否知道:
- 什么时候能写
- 什么时候能读
- 什么时候必须停止碰 buffer
如果答案含糊,那问题已经埋好了。
只盯着 happy path,不看回收路径
全链路图里最不该省略的,就是:
- completion 后谁接手
- 超时后谁清场
- reset 后旧 buffer 如何作废
这些看起来“像细节”,其实决定系统有没有资格上线。
本章结论
- 一次 DMA 传输不是一个 API 调用,而是一条跨设备、内存、驱动、用户态的完整链路。
- 这条链路里必须分清数据面与控制面。
- descriptor、doorbell、completion 分别解决“处理谁”“何时开始”“何时结束”的问题。
- 一旦用户态进入链路,DMA 问题就升级成多方协作协议问题。
下一章要干什么
下一章开始打地基:
- 地址空间
- cache 一致性
- sync / barrier
- IOMMU
- DMA mask
因为如果你连“设备看到的地址”和“CPU 看到的地址”都没分清,后面所有生命周期设计都会建立在幻觉上。






