尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

设备树dma-coherent属性详解:DMA与CPU缓存一致性原理及实践

设备树dma-coherent属性详解:DMA与CPU缓存一致性原理及实践 项目标题里这个“dts中某个设备配置了dma-coherent起什么作用”其实是嵌入式Linux开发里一个很经典但经常被一句“表示设备支持一致性DMA”带过的知识点。我在用RK3566这类AIoT平台调试时也踩过因为没有正确理解dma-coherent而导致的坑——最典型的现象是驱动里用dma_alloc_coherent分配了内存DMA搬运完数据CPU去读内存读到的却是旧数据或者反过来CPU写完数据后没做cache flushDMA直接去内存搬运搬的全是脏数据。如果dts里这个设备没配dma-coherent或者配错了这类问题是很难排查的因为代码不会报错只会在特定数据量、特定场景下偶发地“数据不对”。这篇文章我把dma-coherent这个属性的原理、内核代码处理路径、以及实际调试时怎么验证它究竟有没有生效一次说清楚。1. dma-coherent到底在表达什么先从最直白的定义说起。设备树DTS里dma-coherent是一个无值boolean属性写在哪一个设备节点下就等于告诉内核这个设备的DMA访问路径和CPU访问内存的路径在硬件设计上是保持一致的。什么叫“一致”这里不是指访问结果一致而是指缓存一致性cache coherence。CPU读写内存时不会每次都直接访问DDR而是先经过CPU内部的cache。设备上的DMA控制器则不同它通常直接读写内存物理地址完全绕过CPU cache。这两个访问路径一旦同时操作同一块内存区域就会出现“谁看到的数据是旧版本”的问题。举一个我调试时遇到的实际例子。某个外设驱动做了一次DMA传输把数据从外设搬到了内存缓冲区然后DMA完成中断触发CPU在中断处理里直接读取这个缓冲区想拿数据去做运算。如果缓冲区在CPU cache里有残留的旧数据CPU读到的就不是DMA刚写进去的新数据而是cache里那份旧的。这个问题的根源就是CPU cache没有感知到DMA对内存的写入硬件又不具备自动同步能力。dma-coherent这个属性描述的就是这个设备是否“天生”具备自动同步能力。具备就写dma-coherent不具备就不写或者明确写dma-noncoherent。1.1 硬件层面的一致性是如何实现的如果一台设备在DTS里配了dma-coherent那说明硬件上是有对应的设计保障常见的有四种情况SoC内部的总线互联实现了硬件一致性协议例如ARM的CCI、CMN等一致性互连设备端的ACE/CHI接口和CPU的cache是相互感知的。DMA控制器内部自己有总线监听逻辑能够监听CPU对内存的访问或让CPU cache监听DMA的写入。设备被放在一个与CPU共享同一内存视图的地址空间里并且总线域的支持保证了no-snoop不被滥用。外设和CPU之间通过IOMMU做了一层缓冲IOMMU的映射策略和页表属性保证了两边看到的数据是一致访问coherent access。这里要特别注意一点dma-coherent是“设备属性硬件能力”的联合声明它只是让内核知道我可以不用做软件同步。硬件如果不支持光在DTS里写这个属性是没有用的反而会带来严重的数据完整性问题。1.2 与dma-noncoherent的对比内核里还有一个属性叫dma-noncoherent和dma-coherent正好相反表示这个设备不具备硬件一致性DMA访问时必须依靠软件维护cache的clean/invalidate。搬到一个具体场景里理解如果外设是非coherent设备驱动使用DMA前必须先调用dma_map_single/dma_sync_single_for_device等API保证CPU写的数据真正落到了内存里DMA传输完成后调用dma_sync_single_for_cpu把cache里的脏数据作废让CPU读到DMA写入的新数据。如果是coherent设备这些同步API调用就不会触发实际的cache操作因为硬件已经保证了双方看到的数据是一致的。DTS写法上这两个属性互斥。一般只需要在设备节点里写上其中一个如果都不写内核会通过of_dma_configure时向上遍历父节点继承父节点的coherent属性。2. 为什么DMA和cache的一致性这么关键很多人刚接触时会觉得“反正都是读写内存有什么不一致的”但这里的坑非常隐蔽。CPU和DMA操作内存的粒度、路径、时机完全不同不解决一致性问题轻则随机丢数据重则导致文件系统损坏、音视频花屏。2.1 cache工作原理的简化模型可以把CPU cache理解成放在CPU旁边的一张小抄本。CPU读内存时先去小抄本上找找到就直接拿不再去查大仓库DDR。CPU写内存时通常也只写小抄本之后某个时刻才把小抄本上的内容同步回大仓库。DMA控制器是另一个“员工”它不去看小抄本每次都直接去大仓库搬货、放货。问题就出在这里小抄本上的内容和仓库里的内容很可能不是一个版本。CPU修改数据后小抄本已经改了但仓库还没同步DMA直接去仓库拿拿到的还是老数据。DMA把新数据放进了仓库但小抄本上还保留着CPU之前读过的老数据CPU再去读读出来的是假的。2.2 传统非coherent架构的软件补偿思路在大多数中低端SoC上外设都不具备硬件一致性能力所以内核必须提供一套软件机制在合适的时机主动让cache和DMA“对齐”。这套机制就是内核DMA API的核心工作。它主要处理两类操作映射/反映射map/unmap通过dma_map_single、dma_unmap_single等接口在DMA传输前后做cache的clean或invalidate。一致性内存分配alloc/free通过dma_alloc_coherent分配的内存在非coherent设备上会先把cache属性设置为非cacheable或者每次访问时做同步。这样CPU和DMA访问同一块内存时都不会经过cache从根源上避免“小抄本”问题但代价是CPU访问这类内存的速度会慢下来。2.3 coherent设备为什么能省略很多事如果设备支持硬件一致性那么CPU核心、DMA控制器和内存之间通过一致性互连连接双方都能感知到对方对共享内存的修改。CPU不需要在每次DMA传输前做cache cleanDMA完成后也不需要做invalidate硬件会替你做这些操作。配置了dma-coherent之后最大的收益不是“代码少写几行”而是每次DMA传输节省下来的一组cache维护操作处于关键路径上。对于高速设备比如GBE网络、USB3.0、PCIE等这些cache操作的开销在高频传输时影响非常明显实测可能带来10%到30%的吞吐差距。2.4 生活化类比把CPU cache想象成自己办公桌上的便签本DMA控制器是去档案室跑腿的实习生前台。正常情况下你在便签本上改完内容过一会儿才把改好的内容誊回档案室。实习生去档案室看到的永远是历史版本就很容易出错。为了让双方看到一致的数据你得给他配一个实时对讲机硬件一致性互连这就是dma-coherent的本质或者你在每次交接前大声喊一遍让他等等然后自己赶紧把便签本上的内容先抄到档案室软件同步这就是非coherent的工作方式。3. DTS配置dma-coherent之后内核到底做了什么属性写进DTS只是第一步。设备节点被解析后内核会在设备驱动probe时候通过of_dma_configure等一系列流程把dts里的coherent信息翻译成struct device里的一个标志位并最终影响DMA API的行为。3.1 从设备树节点到struct device标志位当设备驱动调用platform_driver_register并成功匹配设备后内核会调用of_dma_configure来配置设备的DMA参数。在这个函数里有一段关键逻辑int of_dma_configure(struct device *dev, struct device_node *np, bool force_dma) { ... ret of_dma_is_coherent(np); if (ret 0) dev_set_dma_coherent(dev, ret); ... }of_dma_is_coherent会从当前设备节点向上遍历父节点bool of_dma_is_coherent(struct device_node *np) { struct device_node *node; ... for (node np; node; node node-parent) { if (of_property_read_bool(node, dma-coherent)) { return true; } if (of_property_read_bool(node, dma-noncoherent)) { return false; } } ... }例如I2C控制器挂在某个bus节点下bus节点又挂在soc节点下只要这些节点里有dma-coherent设备就会被认为是有一致性DMA能力的设备。这种向上继承的设计简化了设备树编写但也带来一个问题某个子设备本身不具备coherent能力但父节点写了dma-coherent子设备会错误地被当成coherent设备。所以在父节点写这个属性时要格外谨慎除非所有下游设备都真正支持。3.2 影响DMA API的选择路径从Linux内核DMA API的实现上看dev-dma_coherent这个标志位会影响很多函数的行为我这里挑几个最主要的路径分析。先看dma_alloc_coherent的简化流程void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *dma_handle, gfp_t flag) { ... return dma_alloc_attrs(dev, size, dma_handle, flag, 0); }在ARM64架构上如果设备是coherent的dma_alloc_coherent可以直接分配普通内存并做映射不需要特殊页表属性因为硬件已经保证一致性。如果设备是非coherent的内核就需要区分DMA_DIRECT不经IOMMU直接物理地址和IOMMU路径分配内存后还要设置成non-cacheable或使用一致性内存池。再看dma_map_single这类流式映射API在dma_direct_map_page的实现中会有类似这样的逻辑static dma_addr_t dma_direct_map_page(struct device *dev, struct page *page, unsigned long offset, size_t size, enum dma_data_direction dir, unsigned long attrs) { ... if (!dev_is_dma_coherent(dev) !(attrs DMA_ATTR_SKIP_CPU_SYNC)) arch_sync_dma_for_device(dev, phys, size, dir); ... }也就是只有非coherent设备才会走到arch_sync_dma_for_devicecoherent设备直接跳过。同理在DMA传输完成后的dma_unmap_single里非coherent设备会有arch_sync_dma_for_cpu操作coherent设备也不会执行。3.3 与IOMMU的联动如果设备还挂着IOMMU情况会再复杂一点。IOMMU提供了设备侧地址翻译但IOMMU本身并不能天然解决cache一致性问题它只是把物理内存重新映射给了设备。设备树中配置了dma-coherent的设备在IOMMU映射建立时不需要把页表属性设置为“非缓存”或执行额外的PTE属性调整。而如果设备是非coherent且走IOMMU路径内核在映射时往往需要额外处理缓存属性。在我调试RK3566平台的VPU、RGA等多媒体模块时这些模块有的挂在SMMU下面有的DMA能力有限混合使用时要特别区分各设备节点的coherent配置。一个常见问题是VPU设备节点本身没写dma-coherent但它的父节点soc写了dma-coherent结果整个多媒体通路都被当作coherent处理某些特殊操作就会出怪问题。3.4 对驱动开发者的可见影响从驱动开发的视角来看dma-coherent的差异基本被DMA API封装好了。驱动只需要正确调用dma_alloc_coherent、dma_map_single、dma_sync_single_for_device/cpu、dma_unmap_single内核会根据设备标志位自动决定是否做cache维护。但这不代表驱动可以完全无视一致性。尤其是在裸指针操作、用户态映射、以及绕过DMA API的场景里coherent与否决定了你是否需要手动加内存屏障或cache操作。4. RK3566平台上的实际使用与验证回到我开头提到的RK3566。这是瑞芯微面向AIoT场景的一颗四核A55平台很多方案的设备树里都会看到各种外设节点上的dma-coherent属性。比如以太网GMAC节点、USB控制器、SD/MMC控制器、PCIE控制器都可能被标注。4.1 一个常见驱动场景GMAC网卡DMARK3566的GMAC节点有些内核版本里自带dma-coherent具体以实际SDK为准有些没有。如果你拿到了一个没有配置dma-coherent的SDK而你用的PHY和外设硬件路径实际上是支持一致性访问的这时就可以在设备树里给GMAC节点手动加上dma-coherent。加完之后一个最直接的变化是网络吞吐在高负载下会更稳定CPU占用也会下降因为内核不再需要为每一个网络包做大量的cache clean/invalidate。反过来如果你的硬件路径并不支持一致性却强行加了dma-coherent那网络收发的数据包会随机出错——因为软件不再做cache同步而硬件本身又没有能力保证一致性。这类错误在TCP传输中表现为校验和失败、数据错乱在UDP中表现为花屏、乱码在存储场景中可能表现为文件损坏极其难以定位。4.2 怎么确认设备到底是不是coherent有两种比较靠谱的验证方法。方法一内核动态调试。启动内核时加上dma_debug1或者打开CONFIG_DMA_API_DEBUG然后在驱动probe之后去debugfs查看设备DMA映射情况。不过这个方法并不一定每个平台都方便而且dma_alloc相关调试信息不一定直接显示coherent状态。方法二直接读取标准设备文件更加直接。在设备模型注册完成后可以检查设备的dma_coherent标志位。常见做法是在驱动里加一行打印dev_info(dev, dma coherent: %d\n, dev-dma_coherent);我更喜欢在probe函数里加这种调试打印因为可以立刻确认设备树解析结果是否符合预期。如果dts里配置了dma-coherent这里应该打印1没有配置则打印0。在sysfs中设备节点下通常不直接暴露coherent标志。但通过/sys/kernel/debug/devices_debug如果内核开启了相关配置也能看到部分信息。4.3 硬件是否支持一致性的判断硬件支不支持需要查SoC的TRM或者总线框图。以ARM平台举例打开SoC的地址映射图看外设是挂在普通APB/AHB总线上还是挂在CCI/CMN一致性总线端口下。查询外设的DMA是否支持ACE/CHI接口或者是否支持监听cache操作。对于一些老的USB 2.0控制器、SDIO控制器很多都走的是普通总线不要指望它支持硬件一致性。如果拿不准比较保险的做法是先不配置dma-coherent让内核走常规的非coherent路径功能正确性优先。通过测试确认性能瓶颈确实在cache同步上之后再评估能不能开启硬件一致性。4.4 与dma-ranges的区别顺便说一个很多新手会混淆的点。dma-coherent描述的是“访问是否一致”dma-ranges描述的是“设备看到的地址范围和CPU物理地址范围的映射关系”。两者解决的不是同一个问题。dma-ranges一般用在总线控制器节点上比如PCIE RC节点里配置下游设备可以访问的DMA地址窗口。它是一个地址映射表定义的是DMA地址空间到CPU物理地址空间的翻译窗口。dma-coherent则是纯粹的布尔属性两者可以同时出现也可以只出现其中一个互不影响。实际调试中如果设备DMA能正常工作但无法访问大内存区域多半是dma-ranges没有配置好如果数据传输内容错误但DMA地址本身没问题多半是cache一致性问题要去查dma-coherent配置。5. 常见问题与排查技巧实录5.1 配置了dma-coherent但偶尔还是读到旧数据这种情况在SOC的新版本、或者硬件勘误表中其实并不罕见。某个IP在硬件设计上宣称支持一致性但在某些边界条件下做不到。排查思路先确认设备树属性确实解析成功驱动里dev-dma_coherent值为1。用连续大块DMA传输做压力测试超过一定长度后看是否必现。对比关闭dma-coherent后问题是否消失。如果去掉后正常说明一致性路径可能存在硬件缺陷或驱动误用。还有一种可能驱动自身在中断上下文里访问了非coherent的缓冲区或者没用dma_sync_*系列API去正确同步就访问了数据。即使设备是coherent的驱动也必须遵守DMA API的基本使用规则不能在DMA传输未完成时访问缓冲区。5.2 为什么我配置了dma-coherent性能一点没提升性能提升不是必然结果。如果设备本身是非高速设备、传输频率不高、每次传输的数据量又小那cache同步的开销本身就不大优化空间有限。只有网络、USB、存储这类高吞吐场景性能差异才明显。另外如果设备走的是IOMMU路径IOMMU页表建立和查找的开销可能占大头dma-coherent只能消除cache操作部分的损耗不要把它当成决定性能的唯一因素。5.3 设备树里同时写了dma-coherent和dma-noncoherent这个问题比较低级但确实见过。设备树解析时会向上遍历当前节点优先。如果同一节点同时写了两个属性of_dma_is_coherent会先检查dma-coherent找到就返回true。也就是说dma-noncoherent会被忽略。这种写法极容易误导后续维护者。建议设备树中同一个节点只保留一个相关属性如果父节点和子节点存在继承关系也要理清是继承还是覆盖。5.4 检查驱动是否正确使用了DMA API调试DMA一致性问题我强烈建议开启内核的DMA API debug功能CONFIG_DMA_API_DEBUGy启动参数里加上dma_debug1内核会检查常见的DMA API误用比如同一个缓冲区被重复映射、map/unmap不配对等。虽然它不能直接告诉你coherent属性对不对但很多一致性问题其实是API误用导致的先用这个工具排除掉低级错误再去分析属性和硬件路径。另一个有用的小技巧在驱动里对分配的DMA缓冲区做“写后读”自测在环回模式下向DMA缓冲区写入特定pattern完成一次DMA回环传输再校验数据内容。这个测试能很直观地暴露出cache一致性问题。5.5 移动设备的缓存一致性与内存屏障即便有了dma-coherent驱动代码里也不能省掉必要的内存屏障。DMA完成中断到达CPU后驱动在中断上下文里访问数据还是需要遵循“先读数据再读取DMA完成标志”的顺序。如果顺序反了或者缺少必要的barrier仍然可能在极端的乱序执行下读到半新不旧的状态。这点和dma-coherent没有直接关系但容易成为“偶现bug”的来源放在一起排查能省很多时间。6. 实际测试中的操作建议最后分享几个在项目里验证dma-coherent是否“真正生效”的方法都是可以直接落地执行的。6.1 用设备树叠加层快速试错如果你在调试某个平台想快速对比coherent和非coherent两种配置的性能与稳定性不用反复改内核更不用改UBoot直接在设备树叠加层DTBO里覆盖对应节点即可。例如/ { fragment0 { target-path /soc/ethernetfe2a0000; __overlay__ { dma-coherent; }; }; };这样改完重启后加载DTBO就能验证效果。如果发现异常去掉dma-coherent再重启一次对比。6.2 用perf观察cache相关开销在性能对比时可以使用perf stat观察cycle、cache-misses、dTLB-load-misses等指标。coherent和非coherent两种配置下DMA路径的开销差异会在这些指标上有明显体现。当然前提是系统开启了perf的硬件事件支持。6.3 量产固件里的建议量产前对于每个外设都应该把设备树里是否配置dma-coherent作为一个专项测试项而不能只听硬件工程师口头说“这个模块支持一致”。具体做法是把所有外设的dma-coherent配置列一个表格逐项确认硬件是否真的支持一致性访问。设备树配置是否与硬件一致。驱动在DMA传输前、传输后是否仍会做必要的同步操作。长时间高负载下是否稳定建议跑满24小时。在我个人的项目经验里很多时候问题并不是“要不要配置dma-coherent”而是“这个外设到底能不能配置”。把逻辑捋清楚再在设备树里动手比出了bug再各种抓包、抓cache要高效得多。这个属性的作用概括起来就四个字省活、提速。但省下来的活和提上去的速度必须建立在硬件能力真的够硬的基础上。否则你省掉的正是保护数据安全的那一道保险。
返回列表