Lazy loaded image
Words 0Read Time 1 min
Invalid Date
⚠️
核心判断
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 到来时必须回答三件事

  1. 当前 in-flight slot 归谁?
  1. 这次事务作废还是继续等?
  1. 是否需要 reset 硬件?
 
如果 timeout 处理里只是打一句日志然后返回错误,通常等于什么都没处理。

一个最小 timeout 策略

  • 把超时 slot 标记为 ERROR
  • 阻止该 slot 被误判为可复用
  • 记录当前代际 / epoch
  • 必要时触发 reset,把旧事务全部作废

reset:DMA 驱动的地狱入口

reset 最大的问题不是“会中断当前事务”,而是它会制造一个新世界,同时旧世界可能还在漏残影。
 
最典型的问题包括:
  • reset 后旧中断晚到
  • 旧 completion 被新会话误收
  • 软件 ring 和硬件 ring 不再一致

一个比较靠谱的 reset 思路

  1. 先阻止新提交
  1. 停止硬件 DMA
  1. 作废当前 in-flight slot
  1. 清空或重建 completion 队列
  1. 递增 epoch / generation
  1. 重新初始化 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 路径炸了,建议按这个顺序排:
  1. 当前症状是旧数据、错帧、卡死、崩溃还是内存污染?
  1. 当前 slot / ring 状态是什么?
  1. ownership 是否发生了非法迁移?
  1. sync / barrier 是否在正确边界执行?
  1. 最近是否发生 timeout / reset / close / remove?
  1. 是否存在旧 epoch 事件污染新会话?
  1. 传输长度和 buffer 边界是否一致?
 
这个顺序的好处是:先看状态机,再看细节;先看边界,再看局部。别一上来就冲着某个 API 骂娘,那通常只是情绪管理,不是问题定位。

本章结论

  1. DMA bug 看起来像玄学,实际上大多是可见性、顺序、ownership、回收和越界五类问题。
  1. timeout、reset、close、remove 不是附属逻辑,而是主逻辑的一部分。
  1. 最有价值的日志不是“函数进出了没”,而是“状态如何迁移”。
  1. 先把 DMA API debug、debugfs 和 trace 观测口打开,排障难度会直接下降一个量级。

排障时该回看哪些章节

  • 如果你怀疑是地址 / cache / mmap 视图问题,回看
  • 如果你怀疑是 slot 提前交接、重复提交、过早回收,回看
  • 如果你怀疑是 submit / complete / reclaim 流程断裂,回看
  • 如果你需要 命令、模板和速查表,直接翻

下一章要干什么

下一章开始回到真实代码世界:
  • 选成熟驱动
  • 看 probe、runtime、completion、error path
  • 把前面所有抽象原则映射到具体实现
 
因为到这里,理论已经够了。接下来该把“门道”从真实源码里抠出来了。
上一篇
Data Structure and Algorithm
下一篇
用面试拷问嵌入式技术栈

Comments
Loading...