
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 架构中页表项里的 MAIRMemory Attribute Indirection Register定义了不同内存属性的编码其中最关键的是 Normal Cacheable 和 Normal Non-Cacheable 以及 Device 内存类型。Normal CacheableCPU 访问时允许缓存DMA 如果不一致则需要软件同步。Normal Non-CacheableCPU 访问不走 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/invalidate2.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/ethernetff640000: coherent devicedev_is_dma_coherent()返回 true 时dma_map_single路径上会跳过arch_sync_dma_for_device()调用。这套机制对驱动是透明的。所以很多驱动作者从没意识到自己设备的寄存器配置、中断处理都一切正常但数据就是不对根因往往就在这一步——设备树里多了一个属性导致内核跳过了原本必要的 cache 维护步骤。理解了这个对应关系我们再回头看第 1 节那个以太网案例网络控制器硬件并不支持系统级一致性但设备树里错误地声明了dma-coherent于是内核不再维护 cacheDMA 读取的描述符和缓冲区数据可能是 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/ethernetff640000/ cat /sys/firmware/devicetree/base/soc/ethernetff640000/dma-coherent如果dma-coherent存在cat会输出空内容但返回成功如果不存在会提示 No such file。注意设备树属性被覆盖或者被 bootloader 修改是常见的事。我遇到过 U-Boot 在启动时通过fdt set给所有网络设备强制加上dma-coherent的情况板上真实运行的设备树和.dts源文件完全不同。所以排查时一定要以运行时的/sys/firmware/devicetree/为准不能只相信源码。另外也可以通过内核设备模型来查看。在驱动里加打印或者用 ftracedev_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_NCNon-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 同步操作也不要轻易把这个属性加上去。许多时候那些多余的同步正是保护数据正确性的最后一道防线。