1. dma-coherent 到底在说什么:一个容易被忽略的缓存一致性开关
1.1 从一次现场故障说起
上个月我调试一个千兆以太网的 DMA 丢包问题,现象很典型:跑长 ping 大包超过 1472 字节就开始丢包,抓包看到内容尾部有规律性的错位。一开始我怀疑是 PHY 芯片配置问题,调了 MDIO 时序,没用;又怀疑是 DMA 描述符环溢出,改大环形缓冲区,问题依旧。最后把设备树节点里的一行dma-coherent;去掉,整个世界安静了。
这个属性在设备树里看似无害——它只是告诉内核"我这个设备和 CPU 之间是缓存一致的"。但很多工程师对这个字段的理解停留在表面,甚至把它当成"加了能让 DMA 更稳定"的万能药。实际上,一个小小的dma-coherent属性会让内核的整个 DMA 分配路径、缓存同步策略发生剧变,理解不到位就会埋下类似上面这种隐蔽的坑。
这个项目让我决定把dma-coherent相关的原理、代码路径、调试方法和工程经验完整整理出来。这篇内容适合嵌入式 Linux 驱动开发、BSP 工程师,以及所有被 DMA 缓存一致性问题折磨过的人。
1.2 一致性与非一致性:CPU 和 DMA 眼中的内存差异
要理解dma-coherent,先得搞清楚什么叫"缓存一致性"。CPU 访问内存时,为了提高速度,会经过多级 Cache。Cache 里存的是内存数据的副本。正常情况下,CPU 读数据时如果 Cache 命中就直接读副本,写数据时也会先写入 Cache,再通过一定策略回写到内存。而 DMA 外设(比如网卡、SD 控制器、显示控制器)访问内存时,走的是总线直接读写物理内存,完全不经过 CPU 的 Cache。
问题就出在这里。假设 CPU 往某个缓冲区里写了一帧网络数据,数据可能还躺在 Cache 里没有回写。此时网卡的 DMA 引擎直接去读物理内存,读到的是旧数据,或者读到一半时 Cache 才回写,导致数据错乱。反过来,DMA 往内存里写入了一包数据,CPU 再去读时,Cache 里存的还是旧副本,读到的是过时数据。
用生活里的例子类比:CPU 和 DMA 是两个人合作写同一份纸质报告,CPU 会把某一页复印一份(Cache)放在自己桌上修改,DMA 却始终盯着保险柜里的原件。只要 CPU 不把复印件拿回保险柜换掉,两个人永远不知道对方改了什么。这个过程在计算机体系结构里就叫DMA 缓存一致性问题(DMA cache coherence)。
1.3 设备树里的 dma-coherent 属性到底干了什么
在设备树中,dma-coherent;是一个布尔属性,写在某个设备节点下,表示该设备访问内存时,硬件层面已经保证了 DMA 操作与 CPU 缓存的一致性。更具体地讲,它告诉内核:这个设备不需要软件去维护 CPU Cache 和 DMA 数据之间的一致性。
例如:
&sdmmc0 { status = "okay"; dma-coherent; };如果 SoC 的 SD/MMC 控制器硬件上通过系统级一致性总线(如 CCI、CHI、NoC 上的 ACE/ACE-Lite 端口)连接到了 CPU 的缓存层次结构,DMA 访问内存时会自动穿透或监听 Cache,那么就可以声明dma-coherent。如果硬件没有这种机制,或者系统集成时没有正确连接一致性总线,那么这个设备必须走"软件同步 Cache"的路径,即不能声明dma-coherent,或者明确声明dma-noncoherent。
很多人会问:一个属性而已,内核怎么就知道该走哪条路?答案是把决策留给了驱动和 DMA 子系统。当驱动调用dma_alloc_coherent()、dma_map_single()等 API 时,内核会根据设备树中解析出的 coherent 标志,决定是否需要执行 Cache 的 clean 或 invalidate 操作。这是整个问题的核心,也是后面所有现象和坑的根源。
2. 硬件行为与内核机制的对应:为什么不能随便加这个属性
2.1 一致性问题产生的三个必要条件
先做一个判断:什么样的设备才可能"真正缓存一致"?必须同时满足几个条件,缺一个都不成立。
- CPU 侧有 Cache,且 DMA 访问会经过 Cache 的监听或者 DMA 本身不经过 Cache 但硬件支持共享一致性。
- 外设 DMA 可以读写内存,并且存在一个硬件机制保证 DMA 访问与 CPU Cache 之间的同步。
- 系统级互连(Interconnect)支持一致性协议,比如 ARM 的 ACE/ACE-Lite、CHI 总线,或者 Intel 平台上的 DMI/PCIe 相关的 coherency 机制。
如果 SoC 内部有 CCI-400/CCI-550 这种缓存一致性互连,把 CPU 簇、GPU、视频编解码器都挂在一致性端口上,这些内部设备是有可能做到硬件一致性的。但注意,这只是"有可能",具体还要看设备的 DMA 端口是否连到了 ACE 端口,还是连到了普通 AXI 端口。如果连的是普通 AXI,没有监听机制,那就算 SoC 支持 CCI,这个设备依然是非一致的。
很多项目里,芯片厂商的 BSP 会为某些性能关键的设备(如 GPU、显示控制器、硬件视频编解码器)开启dma-coherent,因为这些设备往往跑在系统级一致性总线上,这样做可以免去驱动里频繁的 cache 操作,显著提升吞吐。但并非所有设备都具备这个资格。
2.2 从寄存器角度看 coherent 和 noncoherent 的区别
在硬件层,coherent 和 noncoherent 的差异可以通过内存属性体现出来。ARM 架构中,页表项里的 MAIR(Memory Attribute Indirection Register)定义了不同内存属性的编码,其中最关键的是 Normal Cacheable 和 Normal Non-Cacheable 以及 Device 内存类型。
- Normal Cacheable:CPU 访问时允许缓存,DMA 如果不一致,则需要软件同步。
- Normal Non-Cacheable:CPU 访问不走 Cache,天然"一致",但性能很差。
- Device 内存:一般用于 MMIO,不允许猜测访问,也不缓存。
当驱动通过 DMA API 为 non-coherent 设备分配内存时,内核通常会映射为 Normal Cacheable,然后在合适的时机执行dmac_map_area()或dma_unmap_area()来 clean/invalidate Cache。而如果设备声明了dma-coherent,内核对这块内存的映射则会选择 Non-Cacheable 或使用硬件一致性的 Cacheable 映射,本质上不再需要软件干预。
所以本质上是这样一个对应关系:
| 设备声明 | DMA API 行为 | CPU 缓存策略 | 软件 cache 操作 |
|---|---|---|---|
dma-coherent | 直接分配/映射 | 可 Cacheable(硬件保证)或 Non-Cacheable | 无 |
dma-noncoherent | 分配后需要同步 | Cacheable | 每次 map/unmap 时 clean/invalidate |
2.3 arm64 和 RISC-V 平台上的实际差异
我在 ARM32 和 ARM64 平台上都踩过这个坑。ARM32 时代,绝大多数 SoC 没有系统级一致性端口,外设 DMA 基本都要软件维护 cache,所以dma-coherent很少用。到了 ARM64 时代,高端 SoC 普遍有 CCI/CHI 总线,某些内部加速器确实可以硬件一致,于是很多 BSP 开始给设备树加dma-coherent。
但这事在 ARM64 上也不是无条件的。内核中有个重要的概念叫DMA 直接映射(Direct DMA)。当设备没有 IOMMU/SMMU,且内存是线性映射时,DMA API 会走dma_direct_*路径。dma_direct_alloc()会调用dma_direct_alloc_pages(),其中对 coherent 设备就直接分配页,不做任何 cache 维护;而 non-coherent 设备可能会分配__GFP_DMA32之类的内存,并且需要额外调用arch_dma_prep_coherent()来清理 Cache。
RISC-V 平台的实现思路类似,但细节上又不同。如果你用的是 vendor 定制内核,很可能看到arch/riscv/mm/dma-noncoherent.c之类的文件。重点不在于具体文件名,而在于你必须清楚:dma-coherent属性直接决定了内核是否调用arch_sync_dma_for_device()和arch_sync_dma_for_cpu()这两个架构函数。如果属性配置错误,要么多调了(性能下降),要么少调了(数据错乱)。性能下降还好说,数据错乱就够你查一个星期的。
3. 驱动开发中与 dma-coherent 深度绑定的 API 路径
3.1 分配一致内存:dma_alloc_coherent 的使用逻辑
我见过不少驱动工程师,写 DMA 驱动的第一步就是调用dma_alloc_coherent(),然后就不管不顾了。实际上这个 API 对于 non-coherent 设备隐含了大量工作。看一段典型的驱动代码:
struct device *dev = &pdev->dev; dma_addr_t dma_handle; void *cpu_addr; cpu_addr = dma_alloc_coherent(dev, buf_size, &dma_handle, GFP_KERNEL); if (!cpu_addr) return -ENOMEM;当设备是 coherent 时,dma_alloc_coherent()会返回一块能够被 CPU 和 DMA 同时安全访问的内存,且不需要任何额外的 cache 维护。内部实现通常是通过dma_direct_alloc()分配页,然后建立映射。当设备是 non-coherent 时,这个 API 会尽量分配 Non-Cacheable 的内存(例如 ARM32 上可以通过__get_free_pages加pgprot_noncached映射),或者分配 Cacheable 内存后立即 clean Cache。
这里有个容易误用的点:dma_alloc_coherent()返回的地址,在 coherent 设备上不必是非缓存的。它可能是 Cacheable 的,但硬件能保证一致。在 non-coherent 设备上,内核对这块内存的处理策略是用 Non-Cacheable 映射,这样 CPU 和 DMA 看到的数据天然一致,代价是 CPU 访问性能很差(所有读写都要经过总线)。如果驱动里对这块缓冲区做了频繁读写,你会发现性能莫名其妙地低,这时候要意识到可能是 Non-Cacheable 映射导致的。
对于需要 CPU 频繁读写的 DMA 缓冲区(比如网络收发包),更合理的做法是用dev_alloc_skb()+dma_map_single(),而不是dma_alloc_coherent()一把梭。这背后就是一致性内存和流式映射的区别。
3.2 非一致内存的同步操作:dma_map_single 与 dma_sync_*
流式映射(Streaming DMA Mapping)是更精细的管理方式。设备树里如果没写dma-coherent,那么驱动的每一次 DMA 读写前都必须手动做 cache 同步。典型流程:
dma_addr_t dma_addr = dma_map_single(dev, buffer, len, DMA_FROM_DEVICE); if (dma_mapping_error(dev, dma_addr)) return -ENOMEM; /* 启动 DMA 传输 */ writel(dma_addr, regs + DMA_ADDR); writel(len, regs + DMA_LEN); writel(1, regs + DMA_START); /* 等待中断 */ wait_for_completion(&done); dma_unmap_single(dev, dma_addr, len, DMA_FROM_DEVICE);在 non-coherent 设备上,dma_map_single(..., DMA_FROM_DEVICE)会执行 cache invalidate(使 Cache 中对应地址失效),确保 CPU 后续读取时能看到 DMA 写入的新数据。而dma_map_single(..., DMA_TO_DEVICE)会执行 cache clean,把 CPU 写的数据回写到内存,确保 DMA 能读到最新值。
但如果设备声明了dma-coherent,这些 map/unmap 操作基本就变成了空操作,因为硬件已经保证一致性。驱动的代码不用变,内核在 API 内部做了分流。可偏偏就是这个"不用变",让很多人忽视了dma-coherent对路径的影响,出了问题才追悔莫及。
3.3 dma-coherent 属性如何影响内核的 DMA 操作
我建议每个做 DMA 驱动的人至少通读一遍kernel/dma/direct.c。核心逻辑大概是这样的:内核通过dev_is_dma_coherent(dev)来判断设备是否声明了 coherent。这个函数的底层调用会去设备树里查dma-coherent属性(或者 ACPI 的_CCA方法)。在dma_direct_map_sg()中,会有类似下面的判断:
if (dev_is_dma_coherent(dev)) return; /* 否则调用 arch_sync_dma_for_device() */如果你在内核里打印一下设备节点,可以看到类似输出:
/soc/ethernet@ff640000: coherent devicedev_is_dma_coherent()返回 true 时,dma_map_single路径上会跳过arch_sync_dma_for_device()调用。这套机制对驱动是透明的。所以很多驱动作者从没意识到,自己设备的寄存器配置、中断处理都一切正常,但数据就是不对,根因往往就在这一步——设备树里多了一个属性,导致内核跳过了原本必要的 cache 维护步骤。
理解了这个对应关系,我们再回头看第 1 节那个以太网案例:网络控制器硬件并不支持系统级一致性,但设备树里错误地声明了dma-coherent,于是内核不再维护 cache,DMA 读取的描述符和缓冲区数据可能是 CPU Cache 里尚未回写的旧数据,最终表现为收包时数据错位、校验失败。这是个典型的"属性加错"导致的问题。
4. 识别与排查 dma-coherent 配置错误的完整链路
4.1 现象:数据错位、零星校验失败、随机崩溃
dma-coherent配置错误引起的故障有个特点:它不是必现的,而是负载越高越容易出现。因为 Cache 的回写时机是不确定的,少量数据时 CPU 可能刚好在 DMA 前回写了 Cache,看起来一切正常;压力上来后 Cache 回收频繁,回写顺序不再凑巧,问题就爆发了。
常见现象包括:
- 网卡收包偶发数据错位或 CRC 错误,重启后恢复,跑一段时间又出现。
- DMA 收到的数据始终是旧版本,比如拍屏图像冻结在某一帧,但寄存器状态正常。
- 向 DMA 发送的数据偶尔丢失尾部内容,用逻辑分析仪抓总线能看到数据不完整。
- 驱动里加
dma_sync_single_for_cpu()后问题消失,去掉又出现——这本身就是强烈的信号。 - 随机内核崩溃,
Unable to handle kernel paging request,地址指向刚释放的 DMA 缓冲区。
如果遇到这类问题,第一反应不要总是怀疑硬件,先把设备树里dma-coherent和驱动中的 DMA API 对照检查一遍。
4.2 排查第一步:确认设备树属性是否生效
首先要确认设备树中的dma-coherent到底有没有被内核解析到。可以通过/sys/firmware/devicetree/base/下的节点查看:
ls /sys/firmware/devicetree/base/soc/ethernet@ff640000/ cat /sys/firmware/devicetree/base/soc/ethernet@ff640000/dma-coherent如果dma-coherent存在,cat会输出空内容但返回成功;如果不存在,会提示 No such file。注意,设备树属性被覆盖或者被 bootloader 修改是常见的事。我遇到过 U-Boot 在启动时通过fdt set给所有网络设备强制加上dma-coherent的情况,板上真实运行的设备树和.dts源文件完全不同。所以排查时一定要以运行时的/sys/firmware/devicetree/为准,不能只相信源码。
另外,也可以通过内核设备模型来查看。在驱动里加打印或者用 ftrace:
dev_info(dev, "is dma coherent: %d\n", dev_is_dma_coherent(dev));如果设备节点拿到的dev正确,这里就会直接告诉你结果。
4.3 排查第二步:用 DMA API 调试开关和 ftrace 追踪
Linux 内核提供了一个很强大的 DMA API 调试工具:CONFIG_DMA_API_DEBUG。开启后,内核会检查 DMA API 的使用合法性,还能报告未映射/重复映射/错误长度等问题。
echo 1 > /sys/kernel/debug/dma-api/error配合dma_debug,能看到类似:
DMA-API: device driver maps memory from [stack] or [vmalloc]?? DMA-API: device driver exceeded the map/unmap ratio虽然这个工具不一定能直接告诉你 coherent 属性是否错误,但它能帮你排除驱动调用层面的错误。如果 DMA API 使用完全规范,但数据还是错,那就要把目光收回 cache 同步路径。
更直接的排查方法是打开内核的 cache 同步函数 tracepoint。在我用的 5.15 内核上,可以这么做:
echo 'dma:*' > /sys/kernel/debug/tracing/set_event echo 1 > /sys/kernel/debug/tracing/tracing_on cat /sys/kernel/debug/tracing/trace观察dma_map_single前后是否有dma_sync_single_for_device事件。如果是 coherent 设备,你几乎看不到 cache 维护调用;如果 non-coherent 设备,你会看到每次 DMA 操作前都有同步回调。这里能直观地印证内核的行为。
4.4 排查第三步:检查 Cache 策略和内存属性
如果设备树属性没问题,驱动 API 也没问题,但数据就是不对,那就要往内存属性上看。特别是在既有 IOMMU 又有dma-coherent的地形中,页表属性很重要。对于 ARM64,可以通过CONFIG_ARM64_PTDUMP_DEBUGFS导出内核页表:
cat /sys/kernel/debug/kernel_page_tables重点看 DMA 缓冲区对应的页表项里,MT_NORMAL_NC还是MT_NORMAL。如果设备声明了 coherent 但页表显示是MT_NORMAL_NC(Non-Cacheable),这本身不矛盾——有些实现就是把 coherent 内存映射成 Non-Cacheable。但如果设备声明了 non-coherent,而页表又显示这个区域是 Cacheable 且没有做同步,那就非常可疑了。
还可以用devmem或者内核里的/proc/iomem确认物理地址范围,再对比该区域的内存类型。/sys/kernel/debug/dma-api/dump也能把已分配的 DMA 内存信息打出来,配合页表检查,基本能定位到是映射策略的问题。
5. 工程实践中的正确姿势与经验教训
5.1 不同总线/平台下的 dma-coherent 设置建议
先说几种典型设备的经验值。
- 普通 AXI/APB 挂载的外设,比如简单的 SPI、UART、I2C 控制器,内部没有一致性端口,一律不要写
dma-coherent。 - 挂在 ACE/ACE-Lite 端口上的内部加速器,比如 GPU、ISP、视频编解码器,如果 SoC 手册确认这些端口支持一致性,可以写
dma-coherent,但要实测验证。 - PCIe 设备,通常默认 non-coherent。除非系统通过 ACPI
_CCA明确声明 coherent,否则不要手动给 PCIe 设备添加dma-coherent。在 ARM64 服务器上,很多 PCIe 控制器是 non-coherent 的,而 x86 平台则大多是 coherent 的。 - USB 控制器,多数 SoC 里 USB 的 DMA 是普通 AXI,不写
dma-coherent。
平台差异方面,TI 的 AM335x、NXP i.MX6 这类老平台基本全部 non-coherent。而在一些带 CCI 的高通、海思、瑞芯微平台上,内部多媒体设备有可能支持硬件一致,但必须看 TRM(技术参考手册)里的 Interconnect 章节。不要轻易参考其他开发板的设备树,芯片不同,硬件设计天差地别。
5.2 常见误用场景:把 dma-coherent 当万能药
我见过最典型的误用场景有三类。
第一类是"性能优化流"。工程师发现驱动频繁调用dma_map_single和 cache flush 耗时严重,于是在设备树里加上dma-coherent,期望跳掉同步操作,性能立竿见影。在硬件不具备一致性的前提下,这等于埋了一个随机炸弹。数据量大以后必然翻车。
第二类是"抄板流"。参考设计里某个设备写了dma-coherent,自己的主控芯片换了,还照抄,结果问题百出。同是无线网卡,在 SoC A 上是硬件一致的,在 SoC B 上就可能不是。这一属性必须严格跟着硬件走,不能跨平台复用。
第三类是"启动变慢流"。某些外设的驱动在 coherent 设备上会走不同的初始化路径,如果错误标记为 coherent,可能跳过某些必需的 cache 初始化,导致系统启动随机 hang。这时候你很难联想到设备树属性,就需要通过 git bisect 设备树改动来排查。
5.3 我在项目里总结的检查清单
总结这几年跟dma-coherent打交道的经验,我给自己定了下面这份检查清单,每次移植新平台或者排查 DMA 问题时都会过一遍:
- 读 SoC TRM 的存储系统章节,确认该设备 DMA 端口是否连接一致性总线。
- 查看芯片原厂提供的最新设备树,对比该设备节点的
dma-coherent属性。 - 运行时检查
/sys/firmware/devicetree/base/下实际加载的属性,与源码比对。 - 驱动里打印
dev_is_dma_coherent(dev)的结果,确认 DMA 子系统视角。 - 开启
CONFIG_DMA_API_DEBUG验证驱动 API 调用是否规范。 - 用 ftrace 观察
dma_map_single到dma_unmap_single之间是否触发了 sync 回调。 - 高负载压力测试,覆盖 cache 回写最活跃的场景,例如连续大包收发或视频编解码长时间工作。
- 对可疑缓冲区,用
readl直接读物理地址和驱动看到的虚拟地址做比对,快速发现一致性问题。
这份清单帮我避免了好几次线上事故。上一次以太网问题,也是靠着第 3、4、6 步才迅速定位到属性错误。排查过程本身并不复杂,复杂的是很多人一开始并不愿意相信一个设备树属性会有这么大影响。
我把dma-coherent比作 DMA 世界里的"信任开关"。声明它,等于告诉内核:"我的硬件很可靠,你不需要插手 Cache。"但如果硬件并没有这个能力,内核的放手就是灾难。写驱动也好,改设备树也好,永远记住一句话:不要替你还不了解的硬件做担保。在确认硬件一致性机制之前,宁可多保留几次 cache 同步操作也不要轻易把这个属性加上去。许多时候那些"多余"的同步正是保护数据正确性的最后一道防线。