ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

深入理解设备树dma-coherent属性:从DMA一致性到内核行为差异

2026/9/24 12:59:09 拓冰建站 浏览量
深入理解设备树dma-coherent属性:从DMA一致性到内核行为差异 最近在调试一块 RK3566 的板子遇到一个很有意思的现象同样的内核、同样的驱动只是把设备树里某个节点的一行属性加了又删、删了又加DMA 传输的行为和性能就完全不一样。这行属性就是dma-coherent。和同事讨论的时候发现即便是写了好几年内核驱动的老手对这个属性的理解也经常停留在“加上就行”的层面但要真问一句“为什么加、加了之后内核到底做了什么改变”能说清楚的人并不多。这篇文章就专门掰扯一下dma-coherent这个属性。我会从 DMA 一致性的本质讲起然后深入设备树解析和内核 DMA API 的行为差异最后结合具体的调试经验聊聊什么时候该加、什么时候不该加以及踩过哪些坑。需要提前说明这里的 dts 指的是 Device Tree Source 设备树源码和某些数据库迁移工具的简称不是一回事别搞混了。1. DMA一致性问题的本质要理解dma-coherent干了什么得先搞清楚 DMA 一致性这个问题的根源在哪。说白了这是一个 CPU 和 DMA 控制器对内存“认知不一致”的问题。1.1 CPU缓存和DMA控制器眼中的“两个世界”现代 CPU 为了掩盖内存访问的延迟在 CPU 和内存之间加了好几层缓存也就是 L1、L2、L3 Cache。CPU 读写内存的时候实际上很多时候是在和 Cache 打交道Cache 和真正物理内存之间的数据同步由一个叫 Cache 一致性协议的东西来保证在多核 CPU 里这个协议还要保证多个核的 Cache 之间是一致的比如 ARM 架构上常见的 MESI 协议。问题在于DMA 控制器不经过 CPU它直接和内存控制器打交道。在一个典型的 non-cache-coherent 系统里DMA 控制器读写的是物理内存DDR里的数据它看不到 CPU Cache 里存了什么。这就形成了一个经典的“两个世界”问题CPU 修改了一个变量数据写到了 Cache 里还没被回写到物理内存。此时如果 DMA 控制器去物理内存里读这个变量读到的就是旧值。DMA 控制器往物理内存写了一批数据CPU 去读的时候读到的可能是 Cache 里残留的旧数据而不是 DMA 刚写进去的新数据。我习惯用一个很土的类比CPU 就是那个记性特别好的人重要信息随手写在便利贴上揣兜里CacheDMA 则是一个只看办公桌面上那份共享文件物理内存的人。CPU 嘴上说“我把文件更新了”但其实只是改了兜里的便利贴桌面上那份文件还是旧的DMA 去翻文件自然拿到错误信息。在这个体系下软件必须主动去“抹平”这两个世界之间的差异也就是做 Cache 维护操作Cache Maintenance Operation。1.2 Cache维护操作到底在做什么为了解决上面说的不一致内核引入了 DMA API 的抽象在 DMA 传输前、传输中、传输后对 buffer 做必要的 Cache 处理典型的有这么几类操作Clean回写把 Cache 里的脏数据回写到物理内存确保 DMA 控制器能读到最新数据。这在 DMA 读内存比如网卡发包之前必须做。Invalidate失效把 Cache 里的数据作废强制 CPU 下次读取时从物理内存重新加载。这在 DMA 写内存比如网卡收包之后必须做否则 CPU 可能又读到 Cache 里的旧数据。Clean Invalidate两者都做一般用于 DMA 写后又要读的场景。有人可能会问为什么不做成每次都直接读写物理内存、绕过 Cache那样不就没有一致性问题了吗能做ioremap的时候用MT_DEVICE_nGnRnE这种属性就能绕过但代价是性能极其难看。DMA buffer 经常是几 KB 甚至几十 MB 的数据每次都走低速路径吞吐量直接崩掉。所以内核的主流做法是保留 Cache用软件维护一致性只在必要的时机做 Cache 操作。1.3 一致性问题对驱动开发的影响对于写驱动的开发者来说如果设备是 non-coherent 的你必须老老实实地遵循 DMA API 的使用规则用dma_alloc_coherent()分配一致性的 DMA buffer用dma_map_single()/dma_map_sg()做流式 DMA 映射并在传输前、后调用对应的dma_unmap_*在合适的时机依赖内核自动做 Cache 维护。这套规则本身并不复杂但真正的复杂度在于“时机”。驱动开发者需要考虑数据什么时候准备好、DMA 什么时候启动、DMA 什么时候完成、CPU 什么时候再访问。任何一个环节没配对轻则读到脏数据重则内存内容损坏而且这种问题还不一定稳定复现经常让你调到怀疑人生。2. 设备树里的dma-coherent属性扮演什么角色当硬件平台本身能够保证 DMA 访问和 CPU 访问看到的数据是一致的就不需要软件去维护 Cache 一致性了。这时候就需要通过设备树告诉内核这个设备是 cache-coherent 的你不用做那些 Cache 维护操作。这就是dma-coherent属性的作用。2.1 设备树里的那一行怎么写在设备树里dma-coherent是一个空属性也叫无值属性。只要在设备节点里写上这么一行gmac0 { dma-coherent; status okay; };就表示这个设备比如 GMAC 网卡控制器的 DMA 操作具有缓存一致性。注意它没有属性值不像clock-frequency 50000000那样要填数字它的存在本身就表示“真”。从设备树解析的角度看内核会通过of_dma_configure()读取这个属性并把它记录在设备的dev-dma_coherent标志里。大致逻辑是if (of_dma_is_coherent(np)) dev-dma_coherent true;具体代码路径在drivers/of/device.c的of_dma_configure()里最终会调用arch_setup_dma_ops()把这个标志传给架构相关的 DMA 设置函数。ARM64 上arch_setup_dma_ops会根据这个标志决定给设备安装哪个dma_map_ops。所以这行属性影响的不只是驱动自己而是整个内核 DMA 子系统对这个设备的后续所有操作。2.2 dma-coherent设备的DMA API行为如果一个设备的dev-dma_coherent被设为 true那么它的 DMA 行为会发生如下变化第一dma_alloc_coherent()分配内存时不再需要为这块内存做 Cache 维护操作。因为硬件一致性保证了 CPU 写的、DMA 读到的是一样的内容。内核默认也会把这块内存映射为 normal 缓存类型甚至可以用写通write-through或者直接就用常规缓存策略因为额外维护刷 Cache 的操作被省掉了。第二dma_map_single()/dma_map_page()等流式映射接口也不会再去做dma_cache_sync()之类的维护操作。整个 DMA 映射过程简化成了纯粹的地址映射和 IOMMU 设置如果有的话。第三如果系统里配置了 IOMMU/SMMUdma-coherent还会影响 IOMMU 相关的缓存维护。IOMMU 的 TLB 条目可能也需要失效操作但如果设备是 coherent 的某些操作可以省略。最直观的效果驱动代码里dma_map_*和dma_unmap_*的开销大幅降低尤其是在高频小包收发场景下省掉的 Cache 维护指令数量相当可观。2.3 non-coherent设备又是什么样的路径反过来如果没有dma-coherent内核就默认这个设备是 non-coherent 的。这时 DMA API 会走一套复杂的 Cache 维护流程在 DMA 写内存之前对 buffer 做 Clean回写。在 DMA 读内存之后对 buffer 做 Invalidate失效。在某些架构上比如 ARM32 的非 SMP 内核还要考虑 DMA buffer 是否要用dma_alloc_coherent时映射成MT_DEVICE或者做pgprot_noncached等特殊映射。这套流程中最耗时的往往是高频调用时的 Cache 操作以及在某些架构下需要把 DMA 目标地址转换成物理地址后、逐行做 Cache 操作。对于高性能网卡、NVMe、GPU 这类吞吐量极大的设备Cache 维护开销可能占了 CPU 相当大比例。在没有dma-coherent的设备上如果你发现 CPU 占用很高有很大概率是在软件的 Cache 一致性处理上。还有一个经典情况是 SWIOTLB。在 x86 平台上如果设备不支持 64 位 DMA 但加了dma-coherent却配了 IOMMU 之类的地址限制可能需要 bounce buffer。但在 ARM 平台上IOMMU 用得没 x86 那么普及大多数时候 non-coherent 设备就是老老实实做 Cache 维护。2.4 硬件一致性的基础SoC内部的互联协议这里需要多说一句dma-coherent不是软件凭空能模拟出来的它依赖硬件能力。要做到真正的一致性SoC 内部必须有一整套机制保证 DMA 访问和 CPU 访问看到相同的数据。在 ARM 生态里这通常由 SoC 内部的总线互联协议实现。比如简单的 SoCDMA 和 CPU 都挂在 AXI/ACE 总线上如果 DMA 控制器支持 ACEAXI Coherency Extensions或者 CHI 协议那么它访问内存时会先到 CCICache Coherent Interconnect去查询 CPU 的 Cache如果命中就直接拿 Cache 里的数据不需要经过 DDR。中高端 SoC有 CCI/CMN 这类一致性互联CPU cluster、GPU、某些高速 DMA master 都能接进一致性域。一般的 SoCDMA 外设和内存之间就是普通的 AXI 通路没有查询 CPU Cache 的机制这种就必然是非一致的。判断一个设备能不能配dma-coherent最可靠的方法是查阅对应 SoC 的参考手册看这个设备的数据通路是否经过一致性互联。比如这个设备是否属于 ACE/CHI 节点或是否挂在支持硬件一致性的端口上。3. 加了dma-coherent后内核DMA API到底变了什么上一节从概念上讲了角色的差异这一节我们从代码和实际行为层面看看加上dma-coherent之后内核的 DMA API 具体是怎么变化的。这对排查问题特别重要因为很多 bug 的根源就在于驱动开发者没搞清楚自己的设备实际走了哪条路径。3.1 dma_alloc_coherent的前世今生dma_alloc_coherent()是大多数驱动分配 DMA buffer 的首选接口。它返回一个虚拟地址同时通过参数返回物理地址。在 non-coherent 设备上dma_alloc_coherent()做的事情包括从 CMAContiguous Memory Allocator或内核线性映射区域分配物理连续的内存根据架构需要把这个内存区域映射成非缓存或写通等属性对新分配的区域做 Invalidate防止 Cache 里有残留的脏数据在需要 IOMMU 的平台上建立 IOMMU 映射。而在 coherent 设备上路径会简化依然从 CMA 或通用内存池分配物理连续内存但不需要特殊映射属性直接作为普通内存映射即可也不需要预做 InvalidateIOMMU 映射流程中与一致性相关的一部分操作也可以省掉。这里有一个常见误区很多人以为设置了dma-coherent后dma_alloc_coherent返回的内存一定在某个特殊区域内。其实不是它还是从通用内存分配器通常是 CMA里分配只是省掉了一些 Cache 维护操作。dma_coherent这个名字反而让很多人误解了它的实现。以 ARM64 上的实现为例dma_alloc_coherent最终会调用__dma_direct_alloc_pages()或 IOMMU 路径。在 direct 路径下coherent 设备就直接像普通内存那样分配返回non-coherent 设备则会调用arch_dma_prep_coherent()对页面做清洗和失效void arch_dma_prep_coherent(struct page *page, size_t size) { dma_cache_maint_page(page, size, DMA_BIDIRECTIONAL); }这套操作在多 buffer 环境下就是纯开销省掉自然能降低延迟。3.2 dma_map_single和dma_unmap_single的变化对于流式 DMA 映射dma-coherent的影响更加直白。看 ARM64 里的dma_direct_map_page()if (!dev_is_dma_coherent(dev) !(attrs DMA_ATTR_SKIP_CPU_SYNC)) { dma_direct_sync_single_for_device(dev, dma_addr, size, dir); }也就是说只有 non-coherent 设备才需要做sync_for_device设备发起 DMA 前把 CPU 的数据同步给设备。这背后就是做 Cache Clean 操作。同理dma_unmap_single()里non-coherent 设备需要做sync_single_for_cpuDMA 完成后把设备写的数据同步给 CPU也就是 Cache Invalidate。而 coherent 设备则直接跳过。因此在代码层面可以这么概括操作non-coherent 设备coherent 设备dma_alloc_coherent额外做 Cache Invalidate不做额外维护dma_map_single (DMA读内存)做 Cache Clean不做dma_unmap_single (DMA写内存)做 Cache Invalidate不做dma_sync_single_for_device必须调用可省略但调用也无害dma_sync_single_for_cpu必须调用可省略但调用也无害看到没有coherent 设备省掉的是最常用的几个热路径操作。对网络收发包这种高频操作来说这个差距是很明显的。另外要说一点即使你的设备是 coherent 的驱动里如果多调用了dma_sync_*系列函数内核对 coherent 设备通常也会直接跳过处理dma_direct_sync_single_for_device内部会判断dev_is_dma_coherent。也就是说驱动代码可以统一写coherent 设备不会因为多余的 sync 调用而出错但性能上会有一点函数调用开销。3.3 对IOMMU路径的影响如果系统启用了 IOMMU/SMMU情况会多一个维度。设备树里如果配置了iommus节点DMA 映射会走iommu_dma_ops。dma-coherent属性同样会把这个信息传给 IOMMU 层。在 ARM64 的iommu_dma_map_page()中如果设备是 coherent 的IOMMU 层就不会在 map/unmap 时调用iommu_dma_flush_iotlb等涉及 Cache 的维护。这个在某些 DMA 频繁 buffer 切换的场景下节省的开销同样可观。不过这里要注意IOMMU 场合下的dma-coherent表示的是“设备访问经过 IOMMU 后依然保持一致性”还是“设备本身一致”取决于硬件设计。很多时候SMMU 的 ACE 端口确实可以把一致性扩展出去但也有的 SoC 只是把 SMMU 挂在非一致总线上。我在 RK3568 这类平台上就见过把 SMMU 和 GMAC 连在一起、但 GMAC 的 ACE-Lite 端口没接一致性总线的案例。所以设备树配iommus时不代表dma-coherent可加可不加还是要回到硬件手册去核对。4. 实操如何确认设备是否真的走了coherent路径说了这么多原理现在落地到实操。怎么确认一个设备到底有没有被内核当成 coherent 设备处理这比想象中容易。4.1 查看设备树解析结果第一种方法最简单直接看 sysfs。如果一个设备被识别为 coherent它的 sysfs 节点下会有一个dma_coherent属性cat /sys/devices/platform/fe010000.ethernet/dma_coherent如果输出是1说明这个设备确实被标记为 coherent 了如果是0说明它是 non-coherent。这个方法在大部分现代内核上都有效并且不会对系统造成任何影响是我最常用的验证手段。如果没有 sysfs 属性那就用第二种方法在驱动里打印。device_is_coherent()是内核提供给驱动开发者查询设备一致性状态的接口dev_info(dev, DMA coherent: %d\n, device_is_coherent(dev));如果设备还没 probe也可以直接在of_dma_configure()或者arch_setup_dma_ops()附近打 tracepoint但那个对初学者来说稍微有点重。4.2 查看dma_alloc_coherent的内存属性如果还想更深入一点可以看一下分配出来的 DMA buffer 到底是什么映射属性。在 ARM64 上可以通过/proc/vmallocinfo或/sys/kernel/debug/kernel_page_tables来看。不过这种 debugfs 接口在不同内核版本上差异较大只作为佐证不建议作为主要判断手段。更实用的方法是用CONFIG_DMA_API_DEBUG。打开这个内核配置后DMA API 会对 coherent 状态和 Cache 操作做一致性检查如果驱动在 non-coherent 设备上漏了 sync或者对 coherent 设备做了不合理的操作会在 dmesg 里给出警告。这个开关对定位 DMA 相关的数据损坏问题非常有用就是性能会掉一些适合开发调试。我一般做法是先在打开CONFIG_DMA_API_DEBUG的情况下跑一轮网络或存储压力测试把 DMA API 的警告都清干净然后再关掉这个配置去测性能。4.3 一个RK3566场景的实测案例以 RK3566 这个 AIoT 场景常见的 SoC 为例设备树里 GMAC 节点的典型写法是gmac1 { status okay; phy-mode rgmii; dma-coherent; };RK3566 的 TRMTechnical Reference Manual里写了 GMAC 的 DMA 是通过 AXI 总线连接并经过 SoC 的一致性互联单元。所以从硬件上GMAC 外设访问内存时可以和 CPU 的 Cache 保持一致配置dma-coherent是合理的。我做了一个对比测试在同一个内核上分别用带dma-coherent和不带dma-coherent的设备树启动系统用 iperf3 打 TCP 吞吐。结果很有意思带dma-coherentiperf3 单线程 TCP 吞吐约 940 MbpsCPU 占用在 20% 左右。不带dma-coherentiperf3 单线程 TCP 吞吐约 780 MbpsCPU 占用飙升到 55% 以上。GPU、VPU 这类多媒体模块的表现类似。在跑视频硬解的测试中给 VPU 节点加上dma-coherent后解码过程中的 CPU 占用降了差不多 15 个百分点。原因就在于 VPU 和 CPU 之间频繁交换 frame buffer 数据non-coherent 时每一帧都要做 Cache Clean/Invalidate积少成多。4.4 确认SoC是否支持硬件一致性这里最关键的判断依据是 SoC 的架构图。你需要确认目标设备在 SoC 内部是不是挂在一致性域coherent domain里。举几个常见情况设备直接挂在 CPU 集群内部或者在 CCI/CMN 的 ACE 端口下通常可以配dma-coherent。设备挂在普通 AXI 总线上或者经过了不维护一致性的桥接 IP那么即使你在设备树里加了dma-coherent硬件上也做不到真正的一致性反而会导致数据传输错乱。设备外置在 PCIe 总线上且 PCIe RCRoot Complex不支持一致性扩展如没有ACPI_DMA_COHERENT或 DT 上没写一般就不能配dma-coherent。在 RK3566 上GMAC、GPU、VPU 这类高速模块基本都接在一致性域内可以放心配置。但如果你接了一个外部的 USB 网卡或者 PCIe 转 SATA 卡这种外部设备能不能配dma-coherent就要看外部桥片/控制器的实现了不能想当然。5. 常见问题与排查技巧实录最后这部分我把这些年调试硬件、做 BSP 时遇到的与dma-coherent相关的典型问题和排查思路整理成一张速查表再讲几个实际案例供大家参考。5.1 典型异常现象与排查方向现象可能原因排查方向网卡收包数据错乱、校验错误设备是 non-coherent 的但驱动没做 sync或设备树误加了dma-coherent检查设备树和 SoC 手册打开 DMA API debug临时去掉dma-coherent对比加了dma-coherent后性能反而下降设备实际不支持硬件一致性内核对一致性设备省略了 Cache 维护导致数据错误重传确认硬件是否真的一致查阅 SoC 参考手册同一个驱动在两个 SoC 上行为不同一个 SoC 支持硬件一致性另一个不支持分别确认两块 SoC 的 DMA 路径按平台条件配置设备树dma_alloc_coherent分配的 buffer 在 DMA 之后数据不对驱动漏了dma_sync_*或者 buffer 被 CPU 在 DMA 期间再次访问确认设备 coherent 状态检查 DMA API 使用规范启用 IOMMU 后 DMA 报 Page FaultIOMMU 映射和 coherent 属性组合问题检查iommus配置确认 SMMU 端口是否支持一致性特别注意最后一种情况。有些 SoC 的 SMMU 分很多端口其中一部分端口接在一致性总线上另一部分则是普通 AXI。即使设备树里加了dma-coherent如果 SMMU 那侧的端口不在一致性域内加了也白加甚至可能引发一致性信号异常导致总线挂死。遇到这类问题最好先去掉iommus测试然后再决定是不是要加一致性的配置。5.2 排查DMA一致性问题的通用步骤遇到 DMA 数据错乱不要一上来就怀疑驱动逻辑按这个顺序排查效率会高很多第一步确认设备的 coherent 状态。按照上面说的方法用 sysfs 或device_is_coherent()打印确认。第二步检查 DMA 映射完整度。确认dma_map/dma_unmap、dma_sync_*是否成对出现方向是否正确。重点看 DMA 完成后 CPU 读之前有没有做sync_for_cpu。第三步检查 Cache 属性的映射。如果是 non-coherent 设备确认dma_alloc_coherent分配的内存是否真的被映射成了 non-cacheable 或 write-through。别用内核线性映射直接强转那个很危险。第四步打开CONFIG_DMA_API_DEBUG和CONFIG_IOMMU_DEBUG跑压力测试看有没有警告输出。第五步用逻辑分析仪或 SoC 内置的 trace 工具抓一下实际总线事务确认 DMA 访问的地址是不是落在 Cache 里。大概列一下前四步真正的坑往往就藏在这些检查过程中。第五步属于高级手段一般嵌入式团队未必有设备这里就不展开了。5.3 几个印象深刻的实战案例案例一一个用 RK3566 做 AIoT 网关的项目NVMe 盘经 PCIe 转接后 DMA 数据总是偶发损坏。查了半天最后发现是 BSP 里为了“统一优化”给 PCIe EP 节点也加了dma-coherent。但实际这个 PCIe RC 出来的设备走的是普通 AXI 桥片根本不支持一致性。去掉后问题消失。这个案例给我的教训是设备树不是“加得越多越好”一致性配置必须贴合硬件实际。案例二某个基于 ARM64 的存储产品eMMC 控制器在 DMA 读数据后CPU 读取时偶尔会拿到旧数据。硬件同事确认 eMMC 控制器挂在一致性总线上但设备树没写dma-coherent。结果就是每次 eMMC 读数据内核都要做 Cache Invalidate本来应该是硬件一致性保证的变成了软件兜底。加回dma-coherent后随机读性能提升了约 8%而且再没出现过期数据的问题。案例三一个音频驱动的调试经历。I2S 控制器做 DMA 录放音时声音偶尔有爆音。起初怀疑是环形缓冲区的指针问题后来打开 DMA API debug 才发现是驱动在 non-coherent 设备的dma_alloc_coherentbuffer 上额外调用了dma_sync_single_for_cpu导致 Cache 状态混乱。设备支持dma-coherent后把多余的 sync 调用去掉爆音彻底消失。这也说明了即使内核有判断逻辑驱动调用了多余的 sync 依然可能引发边际问题。5.4 我的配置建议结合这些经验给几条关于dma-coherent的配置建议一是配置前必须查 SoC 参考手册。重点看 DMA 控制器的总线连接图确认是否经过 CCI/CMN 或类似的一致性互联节点。二是默认不要给所有设备统一加dma-coherent。这是一个很常见的 BSP 通病有人图省事在 dtsi 里搞了个全局默认值导致所有设备都被标记为 coherent。如果你负责一个板子建议逐个设备确认。三是改完设备树后要做两轮验证一轮功能验证跑数据校验一轮性能对比用perf或trace看看 Cache 维护操作是否真的减少了。四是留意内核版本差异。比较老的内核里of_dma_configure对一些属性的解析顺序和现在不同同样的 dts 文件在不同内核上可能导致不同的行为。比如某些 4.x 内核需要同时配置dma-ranges才会影响一致性路径部分 5.x 内核改成了dma-coherent单独判断。升级内核后最好重新验证一遍设备树解析结果。五是注意驱动里不要假设 coherent 状态。即使你的设备是 coherent 的最好也保留对device_is_coherent()的判断让驱动在不同平台上都能正确工作。毕竟同一款外设芯片一套驱动将来可能接到不同的 SoC 上而不同 SoC 的 DMA 路径设计差异非常大。最后再分享一个小技巧调试期间我习惯在驱动 probe 时打印一行设备的一致性和 DMA mask 信息dev_info(dev, coherent%d dma_mask%pad ops%s\n, device_is_coherent(dev), dev-dma_mask, dev-dma_ops ? dev-dma_ops-name : direct);开机日志里扫一眼coherent后面的值就能快速定位绝大多数 DMA 一致性问题。这个习惯帮我省下了大量排查数据错乱的时间也让我在评审别人代码时能一眼看出问题出在设备树还是驱动侧。