核心判断
DMA 最容易把人骗惨的地方,是它表面像“内存问题”,本质却同时牵扯 地址空间、cache、一致性、屏障、设备地址能力。这层地基没打稳,后面的 bug 基本都会长成玄学。
本章只解决三件事
- 分清 CPU 地址 / 物理地址 / DMA 地址 到底谁给谁看。
- 搞明白 可见性 和 顺序性 不是一回事。
- 建立排错时最该先检查的那条链,而不是一上来怀疑硬件中邪。
这一章解决什么问题
如果你见过这些现象:
- DMA 明明完成了,CPU 读出来还是旧数据
- 用户传进来的虚拟地址,设备根本不能直接用
- 某个平台上驱动能跑,换个平台就开始错帧
- descriptor 明明写好了,设备却像没看见
别急着拜神。这不是灵异事件,通常只是 地址模型、可见性或顺序治理没立住。
这一章要先把后面所有章节的地基钉死:
- 地址到底有几层视图
- cache 一致性为什么会出问题
- sync 和 barrier 各自管什么
- 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 前,至少先问自己四个问题:
- 传输长度是否小于等于 buffer 容量?
- 设备拿到的是 DMA 地址,而不是 CPU 指针?
- 当前页是否真的允许映射给用户态或设备?
- 出错时是否会立即停止提交并回收当前事务?
很多“看起来像玄学”的后续崩溃,其实根因只是第一步没把长度边界守住。
DMA mask:设备不是都能看完整个内存宇宙
有些设备只支持 32-bit DMA 地址,有些支持 36-bit、40-bit、64-bit。
这意味着:
- 不是所有内存地址它都能访问
- 不先声明设备能力,驱动后面可能在高地址内存上翻车
- 系统有时会借 bounce buffer 或 IOMMU 兜底,但那不是你该默认依赖的神迹
dma_set_mask_and_coherent() 这类动作的重要性就在这里:它是在告诉系统,这个设备到底看得见多大的地址空间。
如果你把这一步当成模板代码随手一贴,那就等于把“地址能力”外包给运气。
一条工程上可直接使用的排查链
当你怀疑 DMA 路径有问题时,按这个顺序排,效率最高:
- 这块内存是不是合法的 DMA 内存?
- 设备拿到的是 DMA 地址,还是你误传了 CPU 地址?
- 下一位读者是谁?是否做了对应 sync?
- descriptor、状态位、doorbell 顺序是否正确?
- 平台是否存在 IOMMU、non-coherent 行为或地址能力限制?
这个顺序很重要。很多人一上来就怀疑硬件,实际上错误早在地址层或同步层就埋下了。
一页速记版
你看到的问题 | 先怀疑什么 |
设备读到旧数据 | CPU 写后是否正确 sync 给设备 |
CPU 读到旧数据 | 设备写后是否正确 sync 给 CPU |
descriptor 明明写了但设备没反应 | barrier / doorbell 顺序是否错了 |
换平台后突然翻车 | IOMMU、coherency、DMA mask 是否变化 |
偶发错帧、随机污染 | 长度边界、ownership、同步纪律是否失控 |
本章结论
- DMA 至少涉及三类地址:CPU 虚拟地址、物理地址、DMA 地址。
- 设备真正使用的是 DMA 地址,不是你手里随便哪个指针。
- cache 一致性问题的核心,是“下一位读者是否能看到最新版本”。
- sync 解决可见性,barrier 解决顺序,ownership 解决权限边界;三者不能混为一谈。
- IOMMU、
mmap和 DMA mask 都会直接影响驱动设计,它们不是背景知识,而是行为边界。
下一章要进入什么
下一章开始进入这个专题真正的中轴:
- ownership
- 协议
- 生命周期
因为当地基打稳之后,DMA 真正难的部分才刚开始:
不是怎么分配 buffer,而是怎么让 buffer 在多方之间合法流转,而不出事故。






