核心判断
DMA 驱动最讨厌的地方,不是 bug 多,而是bug 看起来都像玄学:偶发旧数据、错帧、重复完成、reset 后随机炸。可惜这些东西大多不玄,通常只是 ownership、同步、顺序或回收某一环烂了。
这一章解决什么问题
这一章专门处理真实工程里最脏、但也最值钱的部分:
- 常见症状如何反推根因
- timeout / reset / close / remove 怎么收口
- 日志、trace、压测该怎么设计
- 内核配置、debugfs、DMA API 调试能力怎么开
说白了,这一章讲的不是“如何写一篇漂亮教程”,而是:
当 DMA 路径开始抽风时,怎么别把自己也整疯。
先打掉一个幻想:happy path 从来不够
你可以把 submit、complete、consume 这条链写得很优雅,但真实系统照样会遇到:
- completion 丢失
- 中断延迟
- 用户态不消费
- 设备 reset
- remove 时仍有 in-flight buffer
所以 DMA 的正确性,不是看正常路径能不能跑,而是看异常路径会不会把系统带进未知状态。
高频症状 → 高频根因
症状 | 常见根因 |
CPU 读到旧数据 | 缺少 sync_for_cpu,或 ownership 过早切给 CPU |
设备读到旧数据 | 提交前未 sync_for_device,或 buffer 仍被 CPU 修改 |
偶发错帧 / 数据撕裂 | buffer 被过早复用,或 ring 边界定义不清 |
重复完成 / 幽灵 completion | reset 后旧状态未清,或 completion 队列未隔离 |
一段时间后随机卡死 | slot 泄漏、未回收、指针失步、等待条件不完整 |
remove/close 时崩溃 | 还有 in-flight DMA,资源先释放了 |
随机 Oops / 数据污染 | DMA 长度越界,覆盖相邻对象 |
四类问题怎么区分
1. 可见性问题
典型表现:
- 明明完成了,但读到旧内容
- 明明写过了,设备却像没看到
优先怀疑:
sync_for_cpu
sync_for_device
- cache 一致性处理缺失
2. 顺序问题
典型表现:
- descriptor 偶发不生效
- doorbell 后设备看到半成品
优先怀疑:
- barrier 缺失
- 状态位与数据写入顺序错误
3. ownership 问题
典型表现:
- 重复提交
- 设备仍在写,CPU 已开始读
- 用户态还在碰
DEVICE_OWNED的 slot
优先怀疑:
- 状态机定义不完整
- 非法迁移没被拦住
4. 生命周期 / 回收问题
典型表现:
- 跑久了卡死
- slot 数量越来越少
- reset 后一切开始变得随机
优先怀疑:
- reclaim 缺失
- close/remove/reset 清理不全
DMA 溢出:最蠢、也最致命的事故之一
DMA 控制器通常不会替你检查“这次长度是不是超过 buffer 边界”。
所以一旦传输长度配错,后果不是“这次失败”,而是:
- 覆盖相邻 slab 对象
- 损坏 inode / dentry / 其他内核对象
- 触发随机 Oops、panic 或更脏的数据污染
最小防线
这看着像废话,但很多系统最后就是死在这句废话没写。
另外三类特别高频、但常被低估的工程故障
1. dma_mapping_error() 不是形式校验
很多人把 map 成功默认成理所当然,但真实系统里:
- 地址能力不满足
- IOMMU 映射失败
- 资源不足
都可能导致映射失败。
所以 map 后第一反应应该是:
真正高分的地方不在于你会写这句,而在于你知道:
映射失败意味着“设备侧地址世界没有成功建立”,后面所有 descriptor 都不该继续写。
2. dma_alloc_coherent() 失败时别只会重试
分配失败常见根因包括:
- 请求过大
- 内存碎片化
- CMA 区域耗尽
这时不要只会写“多试几次”,而要重新思考:
- 能不能拆小 buffer
- 能不能改成 pool 或分段结构
- 能不能把大块长期缓冲改成更适合平台的分配策略
因为这类失败往往不是偶发,而是在提醒你:
当前内存模型和你的资源设计并不匹配。
3. 32 位设备 + 高地址内存,是典型能力错配
如果设备只支持 32-bit DMA 地址,而系统物理内存已经跑到更高地址空间,就可能出现:
- 直接无法映射
- 依赖 bounce buffer
- 借 IOMMU 做地址重映射
所以像
dma_set_mask(DMA_BIT_MASK(32)) 这类动作,绝不是模板仪式,而是在提前声明设备的真实寻址边界。timeout:不是等久一点,而是宣布系统进入异常分支
timeout 到来时必须回答三件事
- 当前 in-flight slot 归谁?
- 这次事务作废还是继续等?
- 是否需要 reset 硬件?
如果 timeout 处理里只是打一句日志然后返回错误,通常等于什么都没处理。
一个最小 timeout 策略
- 把超时 slot 标记为
ERROR
- 阻止该 slot 被误判为可复用
- 记录当前代际 / epoch
- 必要时触发 reset,把旧事务全部作废
reset:DMA 驱动的地狱入口
reset 最大的问题不是“会中断当前事务”,而是它会制造一个新世界,同时旧世界可能还在漏残影。
最典型的问题包括:
- reset 后旧中断晚到
- 旧 completion 被新会话误收
- 软件 ring 和硬件 ring 不再一致
一个比较靠谱的 reset 思路
- 先阻止新提交
- 停止硬件 DMA
- 作废当前 in-flight slot
- 清空或重建 completion 队列
- 递增 epoch / generation
- 重新初始化 ring 与状态
这样后面即使旧 completion 漏进来,也可以通过代际检查把它扔掉,而不是把旧世界的尸体拿来当新世界的早餐。
close / release:别假装用户态总会善终
当用户态关闭 fd 时,至少要检查:
- 是否还有未 consume 的 slot
- 是否还有
mmap映射活着
- 是否还有 in-flight DMA 与当前会话绑定
如果用户态退出后,驱动仍让设备继续往旧 buffer 写,那不是容错,那是定时炸弹。
remove:检验你是不是只会写 happy path
remove 时的正确提问不是“怎么 free”,而是:
- 还有没有新请求在进入
- 设备是否已经停机
- 所有 in-flight buffer 是否已收回或作废
- 用户态接口是否已失效
- 中断是否已禁止
如果这些顺序错了,就可能出现:
- 资源先 free 了
- 中断后到
- DMA 还在写
- 然后系统在最丑的时刻崩掉
中断完成路径里一个常被漏掉的点:posted write / 刷状态
有些平台上,完成中断进来后,先读一次状态寄存器是有意义的,因为它能帮助你把 posted write 刷出来,再进入后续同步和读数据逻辑。
也就是说,别把“先读状态再同步”当成迷信。有时候那是硬件接口语义的一部分。
先把调试能力打开,再谈优雅排障
推荐打开的配置
这些配置能帮你看到什么
- DMA API 使用错误
- SG 列表使用异常
- IOMMU 映射状态
如果你连这些观察口都没开,很多 DMA 问题只能靠情绪猜测。
运行时观测入口
一组最值得打印的日志
1. 状态迁移日志
2. 关键时序日志
3. 异常路径日志
4. 映射与地址日志
重点不是日志多,而是日志要能还原状态演化。
trace 和 debug 该怎么上
优先追踪这些点
- submit
- irq/completion
- consume/reclaim
- timeout
- reset
优先关联这些字段
- slot id
- descriptor index
- 当前状态
- ring head/tail
- epoch/generation
- DMA 地址 / 长度
如果日志里连 slot id 和 epoch 都没有,那排查 reset 后幽灵 completion 时基本等于盲人摸鱼。
压测和故障注入建议
压测不要只测 happy path
要主动做:
- 高频 submit / consume
- 用户态故意慢消费
- completion 延迟
- 超时注入
- reset 中插入 in-flight 请求
- close 与 remove 并发触发
- 传输长度边界测试
故障注入要观察什么
- 是否有 slot 永远回不来
- 是否出现非法状态迁移
- 是否出现旧 completion 污染新事务
- 是否出现用户态仍能看到已作废 buffer
- 是否存在越界写污染相邻对象
一个实战排障顺序
当 DMA 路径炸了,建议按这个顺序排:
- 当前症状是旧数据、错帧、卡死、崩溃还是内存污染?
- 当前 slot / ring 状态是什么?
- ownership 是否发生了非法迁移?
- sync / barrier 是否在正确边界执行?
- 最近是否发生 timeout / reset / close / remove?
- 是否存在旧 epoch 事件污染新会话?
- 传输长度和 buffer 边界是否一致?
这个顺序的好处是:先看状态机,再看细节;先看边界,再看局部。别一上来就冲着某个 API 骂娘,那通常只是情绪管理,不是问题定位。
本章结论
- DMA bug 看起来像玄学,实际上大多是可见性、顺序、ownership、回收和越界五类问题。
- timeout、reset、close、remove 不是附属逻辑,而是主逻辑的一部分。
- 最有价值的日志不是“函数进出了没”,而是“状态如何迁移”。
- 先把 DMA API debug、debugfs 和 trace 观测口打开,排障难度会直接下降一个量级。
排障时该回看哪些章节
- 如果你怀疑是地址 / cache /
mmap视图问题,回看 。
- 如果你怀疑是 slot 提前交接、重复提交、过早回收,回看 。
- 如果你怀疑是 submit / complete / reclaim 流程断裂,回看 。
- 如果你需要 命令、模板和速查表,直接翻 。
下一章要干什么
下一章开始回到真实代码世界:
- 选成熟驱动
- 看 probe、runtime、completion、error path
- 把前面所有抽象原则映射到具体实现
因为到这里,理论已经够了。接下来该把“门道”从真实源码里抠出来了。






