Lazy loaded image
Words 0Read Time 1 min
Invalid Date
🧭
核心判断
DMA 最容易把人骗惨的地方,是它表面像“内存问题”,本质却同时牵扯 地址空间、cache、一致性、屏障、设备地址能力。这层地基没打稳,后面的 bug 基本都会长成玄学。
🎯
本章只解决三件事
  1. 分清 CPU 地址 / 物理地址 / DMA 地址 到底谁给谁看。
  1. 搞明白 可见性顺序性 不是一回事。
  1. 建立排错时最该先检查的那条链,而不是一上来怀疑硬件中邪。

这一章解决什么问题

如果你见过这些现象:
  • DMA 明明完成了,CPU 读出来还是旧数据
  • 用户传进来的虚拟地址,设备根本不能直接用
  • 某个平台上驱动能跑,换个平台就开始错帧
  • descriptor 明明写好了,设备却像没看见
 
别急着拜神。这不是灵异事件,通常只是 地址模型、可见性或顺序治理没立住
 
这一章要先把后面所有章节的地基钉死:
  1. 地址到底有几层视图
  1. cache 一致性为什么会出问题
  1. sync 和 barrier 各自管什么
  1. IOMMU、mmap、DMA mask 为什么会直接改写驱动设计

先建立一个总心智模型

你可以把 DMA 问题先粗暴但有效地拆成三类:
维度
你真正该问的问题
典型翻车方式
地址视图
CPU 用什么地址?设备用什么地址?两者是否相同?
把 CPU 指针误当成设备地址
数据可见性
下一位读者能看到最新数据吗?
CPU / 设备看到的不是同一版本
操作顺序
关键动作是不是按正确顺序发生?
设备过早看到半成品 descriptor
🧠
一句话心法
DMA 调试不要问“谁刚写过”,要问两件事:
  • 下一位读者是谁?
  • 它看到的是不是正确版本、正确顺序?

地址不是一个世界,至少是三个世界

1. CPU 虚拟地址

这是内核或用户态代码最熟悉的地址形式。
 
特点:
  • 方便软件访问
  • 受 MMU 管理
  • 不一定等于物理地址
  • 设备通常不能直接理解

2. 物理地址

这是内存芯片上的真实物理位置。
 
但别被“真实”两个字骗了:
设备能不能直接拿物理地址干活,不一定。
 
中间可能还隔着 IOMMU、总线映射或其他平台相关转换层。

3. DMA 地址 / 总线地址

这是设备真正拿来做 DMA 访问的地址。
 
它可能:
  • 恰好等于物理地址
  • 也可能经过 IOMMU 映射后完全不同
 
所以 DMA 世界里最基础、也最常见的事故就是:
CPU 可见地址 当成 设备可用地址
 
这不叫小失误,这叫地基开裂。

一张地址对照表

地址类型
谁使用
典型获取方式
常见误区
虚拟地址
CPU / 内核 / 用户态
kmalloc()vmalloc()、用户指针
误以为设备也能直接理解
物理地址
内存芯片视角
平台相关转换
误以为一定等于设备地址
DMA 地址 / 总线地址
设备
dma_map_*() 或 DMA 分配接口返回
误以为只是“换了个名字的指针”

一张地址关系图

驱动真正关心的不是“我手里拿到什么指针”,而是:
  • CPU 用什么地址访问
  • 设备用什么地址访问
  • 两者之间是否存在映射、限制或重定位

为什么设备不能直接吃用户态指针

因为用户态指针是 用户虚拟地址,只在该进程当前页表上下文里有意义。
 
设备没有这个上下文。它不会替你走页表,也不会理解“这个指针看起来很像真地址”。
 
所以 DMA 永远不是:
把一个随手拿到的指针扔给硬件,然后期待世界和平。
 
它必须经过:
  • 合法的内存准备
  • 合法的 DMA 映射
  • 合法的地址能力检查

Cache 一致性:为什么明明写了,别人还看不见

根因:CPU 看到的,不一定是主存里的最新版本

CPU 为了快,会把数据缓存进 cache。问题随之而来:
  • CPU 改了数据,可能还没写回主存
  • 设备 DMA 一般绕过 CPU cache,直接看主存
  • 所以设备可能读到旧版本
 
反过来也一样:
  • 设备 DMA 把新数据写进主存
  • CPU cache 里还留着旧副本
  • CPU 再读时,可能读到的是旧 cache,而不是设备刚写的新数据
 
DMA 一致性问题最朴素的根源就是:
不是“谁刚写过”,而是“下一位读者能不能看到最新版本”。

两类最典型的问题

问题 A:CPU 写 → 设备读

CPU 刚把数据写进 buffer,但 cache 还没刷回主存。设备开始 DMA 读时,看到的可能还是旧内容。

问题 B:设备写 → CPU 读

设备已经 DMA 写完结果,但 CPU cache 里还保留旧副本。CPU 读取时,读到的是旧数据。

一张“方向 → 首先该想到什么”速记表

数据方向
典型风险
你脑子里首先该想到什么
CPU -> 设备
CPU 刚写的数据还停在 cache,设备读到旧版本
提交前是否需要 clean / dma_sync_*_for_device()
设备 -> CPU
设备刚写完,CPU 还在读旧 cache
完成后读取前是否需要 invalidate / dma_sync_*_for_cpu()
双向反复交接
ownership 和可见性切换混乱
每次换手都重新确认“下一位读者是谁”

clean / invalidate 在工程上到底意味着什么

你不用把它们背成体系结构课术语,工程上只要记住:
  • clean:把 CPU 改过但还停在 cache 里的内容刷回去,让设备有机会看到新版本
  • invalidate:把 CPU 手里的旧 cache 副本作废,逼 CPU 下次去主存里看最新版本
 
所以:
  • 设备准备读时,你通常更关心 clean
  • CPU 准备读时,你通常更关心 invalidate
 
很多“偶发错帧”并不高级,根因只是:
交接前没把可见性准备好。

coherent 和 non-coherent 到底差在哪

coherent DMA

一般理解为:平台或内存属性帮你处理了 CPU 与设备之间的大部分可见性问题,软件负担较轻。
 
它通常意味着:
  • CPU 和设备更容易看到一致内容
  • 使用简单
  • 心智负担低
  • 但未必性能最优,也未必适合所有场景

non-coherent / streaming DMA

这类场景下,驱动必须更显式地管理同步语义。
 
它通常意味着:
  • 更灵活
  • 更适合高吞吐流式路径
  • 但你必须自己管好 sync、ownership 和交接纪律
 
简单记:
模式
优点
代价
coherent
语义简单,软件负担低
性能 / 灵活性未必最优
streaming
更适合高吞吐路径
必须显式同步,纪律要求更高

sync 和 barrier:名字像邻居,职责不是一回事

sync 解决“数据可见性”

典型语义是:
  • 给设备读之前,确保 CPU 的修改对设备可见
  • 给 CPU 读之前,确保设备的修改对 CPU 可见
 
也就是说,sync 主要关心的是:
数据版本有没有对下一位读者准备好。

barrier 解决“操作顺序”

barrier 更关注:
  • descriptor 更新和 doorbell 的顺序
  • 状态位更新和数据写入的先后
  • 读寄存器、写标志、提交任务之间的顺序关系
 
它关心的核心不是“数据新不新”,而是:
关键动作能不能被 CPU 或编译器乱重排。

一个最容易翻车的场景

如果你先写了部分 descriptor,就过早 doorbell,设备可能会看到一份半成品任务说明书。
 
这不是“cache 问题”,这是 顺序治理失败

一张职责划分表

机制
主要解决什么
你该问的典型问题
sync
CPU 与设备看到同一份最新数据
“下一位读者能看到最新内容吗?”
barrier
关键操作按正确顺序发生
“设备会不会太早看到半成品状态?”
ownership 状态机
谁现在可以改这块 buffer
“当前到底该谁动它?”
⚠️
高频混淆提醒
  • sync 不是 barrier
  • barrier 不是 cache flush
  • ownership 也不是“谁先抢到谁算数”

IOMMU:它不是背景板,而是改写地址语义的角色

IOMMU 的存在会改变一个关键事实:
设备地址不一定直接等于物理地址。
 
它通常带来这些能力:
  • 地址映射与重定位
  • 更好的隔离与保护
  • 支持设备访问更灵活的内存布局
  • 让低位宽设备也可能访问更高物理内存
 
但代价也很现实:
  • 映射管理成本
  • 额外复杂度
  • 调试时“为什么这个地址看起来不对”的心智负担
 
工程上真正要记住的是:
设备真正看的地址,可能是 IOMMU 产出的 DMA 地址,不是你脑补出来的物理地址。

mmap 为什么也和地址模型强相关

当你把 DMA buffer 暴露给用户态时,常见形态是:
  • 驱动持有内核视角的 CPU 地址
  • 驱动持有设备视角的 DMA 地址
  • 用户态通过 mmap 得到新的用户虚拟地址,映射到同一批物理页
 
也就是说,一块底层物理页可能同时对应:
  • 一个内核虚拟地址
  • 一个 DMA 地址
  • 一个用户虚拟地址
 
这也是为什么零拷贝系统要同时治理:
地址、权限、生命周期,而不是只会喊“映射成功了”。

一块 DMA 页,为什么会同时出现三个地址视图

你可以把它理解成:
  • 内核虚拟地址:驱动自己读写这块内存时用
  • DMA 地址:设备做 DMA 时真正使用
  • 用户虚拟地址:用户态通过 mmap 后看到的地址
 
这三个地址可能都不一样,但底下指向的是同一批物理页。
 
所以零拷贝从来不是“复制了一份给用户态”,而是:
让不同参与方,以各自能理解的地址视图,共享同一批底层页。

dma_mmap_coherent() 在这里扮演什么角色

当驱动把 coherent DMA buffer 暴露给用户态时,dma_mmap_coherent() 的价值不是“又分配了一块 DMA 内存”,而是:
  • 把已有 DMA 页映射进用户态地址空间
  • 保持这批页的 DMA 语义与映射属性
  • 让用户态和内核共享同一数据面
 
但别高兴太早:
mmap 解决的只是 地址桥接,真正的提交、完成、消费、回收秩序,仍然要靠协议定义。

DMA 溢出为什么特别脏

DMA 控制器不会像高级语言运行时那样替你优雅兜底。长度配错时,后果通常不是“这次报错”,而是:
  • 覆盖相邻 slab 对象
  • 污染 inode / dentry / file 这类完全不该被碰的内核对象
  • 触发随机 Oops、卡死、数据撕裂甚至 panic
 
也就是说,DMA 溢出不是普通越界,而是:
硬件拿着合法通行证,直接往物理内存里乱写。

最小边界守卫清单

在真正下发 DMA 前,至少先问自己四个问题:
  1. 传输长度是否小于等于 buffer 容量?
  1. 设备拿到的是 DMA 地址,而不是 CPU 指针?
  1. 当前页是否真的允许映射给用户态或设备?
  1. 出错时是否会立即停止提交并回收当前事务?
 
很多“看起来像玄学”的后续崩溃,其实根因只是第一步没把长度边界守住。

DMA mask:设备不是都能看完整个内存宇宙

有些设备只支持 32-bit DMA 地址,有些支持 36-bit、40-bit、64-bit。
 
这意味着:
  • 不是所有内存地址它都能访问
  • 不先声明设备能力,驱动后面可能在高地址内存上翻车
  • 系统有时会借 bounce buffer 或 IOMMU 兜底,但那不是你该默认依赖的神迹
 
dma_set_mask_and_coherent() 这类动作的重要性就在这里:
它是在告诉系统,这个设备到底看得见多大的地址空间。
 
如果你把这一步当成模板代码随手一贴,那就等于把“地址能力”外包给运气。

一条工程上可直接使用的排查链

当你怀疑 DMA 路径有问题时,按这个顺序排,效率最高:
  1. 这块内存是不是合法的 DMA 内存?
  1. 设备拿到的是 DMA 地址,还是你误传了 CPU 地址?
  1. 下一位读者是谁?是否做了对应 sync?
  1. descriptor、状态位、doorbell 顺序是否正确?
  1. 平台是否存在 IOMMU、non-coherent 行为或地址能力限制?
 
这个顺序很重要。很多人一上来就怀疑硬件,实际上错误早在地址层或同步层就埋下了。

一页速记版

你看到的问题
先怀疑什么
设备读到旧数据
CPU 写后是否正确 sync 给设备
CPU 读到旧数据
设备写后是否正确 sync 给 CPU
descriptor 明明写了但设备没反应
barrier / doorbell 顺序是否错了
换平台后突然翻车
IOMMU、coherency、DMA mask 是否变化
偶发错帧、随机污染
长度边界、ownership、同步纪律是否失控

本章结论

  1. DMA 至少涉及三类地址:CPU 虚拟地址、物理地址、DMA 地址。
  1. 设备真正使用的是 DMA 地址,不是你手里随便哪个指针。
  1. cache 一致性问题的核心,是“下一位读者是否能看到最新版本”。
  1. sync 解决可见性,barrier 解决顺序,ownership 解决权限边界;三者不能混为一谈。
  1. IOMMU、mmap 和 DMA mask 都会直接影响驱动设计,它们不是背景知识,而是行为边界。

下一章要进入什么

下一章开始进入这个专题真正的中轴:
  • ownership
  • 协议
  • 生命周期
 
因为当地基打稳之后,DMA 真正难的部分才刚开始:
不是怎么分配 buffer,而是怎么让 buffer 在多方之间合法流转,而不出事故。
上一篇
Data Structure and Algorithm
下一篇
用面试拷问嵌入式技术栈

Comments
Loading...