Linux驱动DMA一致性解析:dma_alloc_coherent与dma-coherent设备树配置

发布时间:2026/9/14 3:20:31
Linux驱动DMA一致性解析:dma_alloc_coherent与dma-coherent设备树配置 1. DMA一致性到底是什么为什么驱动开发者绕不开1.1 DMA和cache之间那点“恩怨”做内核驱动这几年但凡和外部设备打交道DMA几乎是绕不开的一关。DMA的全称是Direct Memory Access外设绕过CPU直接读写内存。这个机制本身不复杂但一旦和CPU的cache搅在一起就容易出大乱子。问题出在这CPU为了提速度会在cache里缓存一份内存的数据。现在外设DMA直接往内存里写了一段新数据CPU的cache里可能还保留着旧数据。这时候CPU再去读这块内存拿到的是cache里的旧值DMA写入的新数据就“看不见”了。反过来也一样CPU写数据时可能只是写进了cache还没回写到内存DMA直接从内存读读到的又是旧数据。用生活里的事打比方cache就是你工位上放着的一份文件复印件内存是公共档案室里的原件。别人去档案室改了案卷你手头的复印件还是老样子你改完复印件后随手塞进抽屉没归档别人去档案室看原件自然也看不到你的改动。两边各干各的没人同步必然对不上账。1.2 一致性问题在真实硬件上的表现在真实SoC上解决DMA和cache一致性问题有两条路线硬件维护和软件维护。硬件维护靠的是芯片内部的总线协议。比如ARM架构里的CCI、CCN、CMN这些总线互联组件以及部分GPU、显示控制器、视频编解码单元它们的DMA端口本身挂在支持一致性协议的总线上。外设DMA读写内存时总线会自动帮忙同步cache对CPU完全透明。这种方案性能好适合高带宽场景。软件维护则要求软件介入。如果硬件没有一致性能力Linux内核就自己动手要么在DMA映射/解映射时对cache做失效或回写操作保证CPU视角和DMA视角一致要么干脆把DMA缓冲区的内存页表属性改成uncached让CPU访问时绕过cache直接读写内存。dma_map_single走的是前一条路dma_alloc_coherent走的是后一条路。很多项目问题恰恰出在这里驱动开发者没搞清楚平台到底支持哪条路线就在代码里混用两种方式。用dma_alloc_coherent分配了缓冲区又天真地希望CPU访问能享受cache加速或者在硬件根本不支持一致性的时候设备树里错误地写了dma-coherent属性导致内核相信硬件省掉了cache维护最后数据随机错乱排查到怀疑人生。1.3 一个真实案例FPGA加速卡的数据错乱我印象很深的一次调试是给一块FPGA加速卡写Linux驱动。FPGA通过AXI总线挂到SoC上DMA传输在测试环境下怎么跑都正确一旦接上真实业务、CPU访问稍微频繁一点就偶发出现数据错乱。一开始我怀疑是中断丢失后来怀疑内存屏障没加对折腾了整整一个下午。最后实在没辙用devmem直接去读物理地址和驱动里的数据对比才确认问题出在cache一致性上CPU写进buffer的数据还在cache里没有回写FPGA的DMA直接从内存里读自然读到了旧数据。而这个问题之所以偶发是因为cache回写的时机不确定和CPU访问频率、cache替换策略都有关。从那以后我养成了习惯只要驱动里出现dma_alloc_coherent第一件事就是把设备树里dma-coherent属性和驱动里的dma_mask全部捋清楚。这篇文章就围绕dma-coherent这个关键词把一致性DMA映射的原理和实操经验完整梳理一遍希望对正在被DMA数据错乱折磨、或者刚开始接触DMA外设驱动的朋友有帮助。2. dma_alloc_coherent一致性DMA缓冲区的正统解法2.1 API原型和参数细节Linux内核提供了一致性DMA映射的核心接口dma_alloc_coherent。原型如下void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *dma_handle, gfp_t flag);先看参数每一个都不能马虎dev设备指针决定这套DMA映射走哪个DMA域、有没有IOMMU、设备树的coherent属性是什么。通常传pdev-dev。有些人图省事传空指针或者一个全局device一旦平台上有多个DMA域后果就是映射关系错乱。size要分配的字节数。内核实际按页对齐分配所以传几百字节也会占满一个page。批量小缓冲区建议用dma_pool后面会细说。dma_handle输出参数返回DMA地址。这个地址是给硬件填寄存器用的在有IOMMU/SMMU的平台上是IOVAI/O Virtual Address不一定是物理地址。flag内存分配标志。进程上下文用GFP_KERNEL中断上下文或持锁时必须用GFP_ATOMIC。dma_alloc_coherent返回的内存保证清零不用自己memset。配套的释放函数是void dma_free_coherent(struct device *dev, size_t size, void *cpu_addr, dma_addr_t dma_handle);注意释放时的size、cpu_addr、dma_handle必须和分配时完全一致否则DMA调试API会在内核日志里刷“DMA-API: device driver frees DMA memory with different size”之类的报错时间久了还可能掩盖真正的内存泄漏。2.2 内核在底层做了哪些“见不得人的事”dma_alloc_coherent不是简单封装了几个页面分配函数它为了满足“一致性”做了不少工作。第一步分配内存。为了满足DMA引擎对连续物理内存的要求内核通常走__get_free_pages或者CMA路径。CMA是专门为DMA预留的连续内存池在多媒体、GPU驱动里非常常见。如果平台CMA区域配置得太小分配大缓冲区就容易失败dmesg里会看到“DMA coherent allocation failed”之类的信息。第二步建立映射。在ARM32上内核会为这块内存重建一段页表映射把属性设成strongly ordered或uncached在ARM64上通过set_memory_uncached或者类似的机制处理。这样CPU访问这段地址时就不会再经过cache外设DMA读写的内存和CPU看到的内存是一致的。代价是CPU读写这块内存的性能会变差因为每次都直接怼到内存总线上。第三步处理IOMMU。如果设备后面挂着SMMU/IOMMU内核还会在IOMMU里建立一个映射把物理上可能不连续的页面映射成一个对设备来说连续的IOVA。这也是为什么绝对不要假设dma_handle就等于物理地址。我见过不少从老平台移植过来的驱动直接对dma_handle做virt_to_phys转换结果在新平台上完全乱套。理解这几步很多现象就都解释得通了为什么dma_alloc_coherent的CPU访问速度慢是因为uncached为什么分配大块内存容易失败是因为连续内存不够为什么填设备寄存器时应该用dma_handle而不是自己拿CPU虚拟地址去换算。2.3 与dma_map_single怎么选写驱动时经常会在两个接口之间纠结dma_alloc_coherent和dma_map_single。简单说前者适合长期驻留、数据量大、CPU和设备都需要频繁访问的缓冲区后者适合短期的、每次传输用完就释放的流式数据。举几个典型场景网卡收发包高频、短数据、缓冲区复用用dma_map_single更合适。帧缓冲、音视频缓冲区、DMA环形缓冲区常驻大块用dma_alloc_coherent更省心。一次性控制命令传输数据量小、频率低dma_map_single配合DMA_TO_DEVICE就行。从性能角度dma_alloc_coherent把整块内存设置成uncachedCPU高频小数据访问时性能打折明显。dma_map_single保留cache能力在每次map/unmap时做cache invalid/clean操作传输前有一次同步总开销可能更小但对驱动的使用流程要求更高必须严格配对map和unmap。从易用性角度dma_alloc_coherent明显更省心不需要每次传输处理sync也没有数据传输方向的问题。下表是我个人选型时的参考对比维度dma_alloc_coherentdma_map_single使用场景常驻缓冲区、大数据块短生命周期、流式传输cache行为通常uncachedCPU访问慢保留cachemap/unmap时同步每次传输开销无额外sync操作每次都要map/unmap地址要求CPU虚拟地址DMA地址已有内核缓冲区额外得到DMA地址适用典型硬件显示、音视频、FPGA网卡、USB、SD/MMC选型没有绝对标准核心是先搞清楚自己数据流的访问模式再决定用哪个。3. 设备树上dma-coherent属性该怎么配3.1 dma-coherent 与 dma-ranges 到底管什么从驱动开发的视角设备树里有两个DMA相关属性经常被搞混dma-coherent和dma-ranges。很多人以为它们是一回事其实完全不是。dma-coherent是一个布尔属性标记该设备的DMA操作是否由硬件自动维护cache一致性。如果设备挂在支持一致性协议的总线上比如SoC内部的部分高速总线、PCIe RC、或者带CCI/CMN的侧端口就可以加这个属性。加上之后内核在DMA映射时知道不需要额外维护cache一致性可以走更高效的路径dma_alloc_coherent分配的内存也不必强行设置成uncached。CPU访问缓冲区时还能继续享受cache加速。dma-ranges则描述DMA地址与CPU物理地址之间的映射关系通常出现在总线节点上。典型场景是PCIe RC节点dma-ranges告诉内核PCIe外设能够看到怎样的DMA地址窗口这些DMA地址对应到CPU物理地址的哪一段。它和cache一致性毫无关系。用一句话区分dma-coherent回答的是“设备和cache之间要不要软件搬数据”dma-ranges回答的是“设备眼中的地址和CPU眼中的地址怎么换算”。这是两个维度的问题不能混为一谈。3.2 不同外设场景下的配置经验写设备树时我的标准动作是先查芯片手册或参考BSP明确外设是不是挂在具备硬件一致性能力的总线上。对于SoC内部的高带宽外设比如显示控制器、GPU、视频编解码单元如果它们的总线端口具备一致性能力设备树里加上dma-coherent通常是正确且收益明显的。因为这类设备普遍需要大数据量DMA如果不加内核会把缓冲区设置为uncachedCPU访问性能直接掉一个档次。对于纯软件控制的普通外设比如SPI、I2C、UART、SDIO这些通常没有硬件一致性机制。设备树里不要加这个属性内核会走非一致路径自动做好cache维护。最危险的场景是硬件明明不支持一致性设备树里却加了dma-coherent。内核相信了硬件描述省掉了cache维护操作DMA数据就可能随机错乱。这种bug非常难排查因为它和cache命中情况、CPU调度都相关不是必现的可能跑几小时才出现一次而且表现极不稳定。我在实际项目里还遇到过一种情况芯片设计阶段总线端口明明支持一致性但因为某个桥接IP配置错误导致该设备DMA地址实际上绕过了一致性总线。客户按照芯片手册在设备树里加了dma-coherent结果跑几天就出一次数据异常。最后是抓取总线报文才定位到问题。所以在设备树里加属性之前一定要动手查SoC用户手册里关于coherent或cacheable DMA的章节最好看一眼底层参考设备树和内核自带dtsi。如果拿不准先用不加属性的方式跑一遍数据正确只是性能普通那再考虑加属性如果加上属性之后出现偶发错乱大概率就是硬件并不真正支持一致性。下面是一段典型设备树节点示例soc { my_device: my-driver1c00000 { compatible vendor,my-device; reg 0x0 0x1c00000 0x0 0x1000; interrupts GIC_SPI 123 IRQ_TYPE_LEVEL_HIGH; dma-coherent; }; };3.3 驱动代码里的配套设置设备树只是告诉内核设备侧的基本事实驱动代码里还必须设置DMA地址掩码。这步不做dma_alloc_coherent很可能直接失败或者返回不合适的地址。probe里最常见的一段代码static int my_probe(struct platform_device *pdev) { struct my_dev *mdev; int ret; mdev devm_kzalloc(pdev-dev, sizeof(*mdev), GFP_KERNEL); if (!mdev) return -ENOMEM; ret dma_set_mask_and_coherent(pdev-dev, DMA_BIT_MASK(32)); if (ret) { dev_err(pdev-dev, Failed to set DMA mask: %d\n, ret); return ret; } mdev-tx_buf dma_alloc_coherent(pdev-dev, TX_BUF_SIZE, mdev-tx_dma, GFP_KERNEL); if (!mdev-tx_buf) { dev_err(pdev-dev, Failed to allocate coherent DMA buffer\n); return -ENOMEM; } // 把tx_dma填到设备相关寄存器 // ... }dma_mask决定了DMA能访问的地址范围coherent_dma_mask决定了dma_alloc_coherent分配时使用的地址范围。如果设备只支持32位DMA地址强行设成64位掩码高32位地址写入寄存器时可能会丢位导致DMA访问完全错误。这类问题很隐蔽因为大部分时候地址低32位恰好命中内存地址系统跑起来看似正常数据量一大或内存布局变化就开始随机出错。另外如果驱动需要多个缓冲区分配时把size按页对齐是基本操作。如果有很多小的固定大小缓冲区强烈建议用dma_pool_create创建DMA缓冲池而不是反复调用dma_alloc_coherent这样能避免大量页对齐带来的浪费。4. 实操中的问题排查清单与避坑经验4.1 dma_alloc_coherent分配失败的常见原因dma_alloc_coherent分配失败内核日志通常会有提示。常见原因无非下面几种CMA内存不足或连续内存不足。老平台经常有CMA配置不当的问题大块缓冲直接分配失败。可以查看/proc/meminfo里的CmaTotal和CmaFree或者在设备树的reserved-memory节点里预留更大的CMA区域。调试阶段也可以用启动参数快速验证比如cma128M。coherent_dma_mask没有设置。如果设备没有调用dma_set_coherent_mask或dma_set_mask_and_coherent内核可能认为该设备不支持DMA分配直接返回失败。这个错误在新手写的驱动里特别常见probe里忘了设置掩码dma_alloc_coherent返回NULL然后各种“No memory”报错。在中断上下文用了GFP_KERNEL。GFP_KERNEL允许睡眠在中断上下文调用会导致内核检测到“在可睡眠上下文调用可睡眠分配”打印调度器警告或者直接分配失败。中断上下文应该用GFP_ATOMIC并且要接受分配失败概率变高的事实。有一种情况值得单独说驱动反复加载卸载DMA缓冲区没有正确释放。第二次insmod时分配失败dmesg里却不明显。这时可以用CONFIG_DMA_API_DEBUG编译内核跑一遍再配合/sys/kernel/debug/dma-api/下的统计信息看哪个设备泄漏了DMA映射。4.2 数据错乱与性能异常的排查路径数据错乱是最让人头大的DMA问题。我的排查顺序是固定的第一步确认填到设备寄存器里的地址是dma_handle不是CPU虚拟地址。这个错误低级但常见尤其从老驱动移植代码时最容易搞混。dma_handle是设备视角的地址CPU虚拟地址是CPU视角的地址两者完全不等价尤其在有IOMMU时。第二步确认CPU侧访问的缓冲区地址就是dma_alloc_coherent返回的虚拟地址不要自己另起炉灶ioremap一段地址去访问同一块物理内存。对uncached缓冲区来说ioremap出来的映射属性可能和原来的映射不一致反而引入新的问题。第三步确认设备树里的dma-coherent属性与硬件实际能力一致。可以临时去掉属性测试如果去掉后错乱消失说明硬件并不支持一致性属性加错了如果去掉后错乱依然问题可能在别处继续往下查。第四步打开CONFIG_DMA_API_DEBUG重新编译内核跑一段时间后看dmesg。这个选项能发现很多DMA API使用层面的问题比如map/unmap不配对、size不一致、非法free等。它本身有一定性能开销但不适合生产环境调试阶段强烈推荐。性能异常则多半和uncached访问有关。如果CPU需要高频读写缓冲区dma_alloc_coherent的uncached属性会让每一次CPU访问都直接走内存总线性能损失可能非常明显。这时候要么改用dma_map_single流式方案要么用dma_alloc_attrs加DMA_ATTR_WRITE_COMBINE后者对写操作有合并优化在部分场景下性能提升显著。4.3 调试工具和内核配置项我实际调试DMA问题时最常用的工具就这几样工具/选项用途注意事项devmem/devmem2直接读写物理地址绕过CPU cache验证硬件地址单位要小心误写可能刷坏系统/proc/iomem查看设备寄存器地址和设备树是否匹配排查reg配置错误/sys/kernel/debug/dma-api/查看每个设备DMA映射统计需要CONFIG_DMA_API_DEBUGftrace trace-cmd跟踪dma_alloc_coherent调用栈统计分配释放次数和上下文CONFIG_DMA_API_DEBUG检测DMA API使用错误有性能开销生产环境关闭cma 启动参数快速调整CMA大小调试阶段临时验证devmem访问的是物理地址和CPU cache无关用它来观察DMA写结果比较可靠。不过使用时要小心devmem直接操作物理地址地址写错可能写坏关键寄存器导致系统死机或文件系统损坏最好在测试环境里操作。ftrace追踪dma_alloc_coherent的调用栈对排查泄漏和乱释放非常好用。我曾经靠ftrace发现一个驱动在probe失败路径里漏掉了dma_free_coherent导致反复插拔设备后CMA耗尽。这个问题用代码走查半天也没看出来ftrace一跑调用栈清清楚楚。4.4 日常踩坑经验小结最后分享几个自己在实际项目中踩过、也帮别人排查过的坑不要在一个设备里混用dma_alloc_coherent分配的内存和普通kmalloc内存去填同一个DMA描述符。多个缓冲区的cache属性必须和DMA映射方式匹配混着用必出问题。dma_alloc_coherent分配的内存在dma_free_coherent之后绝对不能再访问也不要memset这类操作某些平台会直接触发page fault。这个错误在驱动卸载路径里比较常见释放顺序搞反缓冲区被设备中断回调访问崩溃现场非常难看。probe函数里如果有多个资源需要分配错误路径一定要按顺序释放。建议probe逻辑尽量简单一个资源分配失败前面已分配的资源要全部回滚包括dma_alloc_coherent、request_irq、ioremap等。否则反复probe/remove几次DMA泄漏和中断泄漏叠加最终导致系统不稳定。音频视频这类对延迟和带宽都敏感的场景用DMA_ATTR_WRITE_COMBINE往往比直接uncached性能更好但前提是驱动只在写方向需要高性能读方向要小心。写合并对读操作没有加速效果甚至会因为合并语义让读到的数据不是最新的所以用之前一定把访问模式想清楚。还有一个容易被忽略的问题设备树里加上dma-coherent之后系统如果偶尔出现cache相关panic比如“Unhandled fault: imprecise external abort”也要怀疑是不是一致性描述和实际硬件行为不一致。imprecise外部中止是ARM上的典型异步异常很多时候cache问题会以这种奇怪的方式冒出来和DMA访问内存的时序有关直接查设备树属性往往比查代码更快。我在实际项目里还踩过一个坑某个平台在设备树里加了dma-coherent去掉之后所有DMA访问都变慢到不可接受但数据一直是正确的。后来查芯片手册才发现硬件一致性能力是有的但需要额外配置一个寄存器来使能。设备树属性只告诉内核“硬件是一致性的”不会帮你配置硬件寄存器。这种坑只能靠认真读手册和参考BSP来避免。写到这里关于dma-coherent的主要内容算是梳理完了。说句实在话我调试DMA类bug最大的体会是先花几十分钟把设备树属性和硬件手册搞清楚远比在驱动代码里乱加cache操作和内存屏障高效得多。很多看起来玄学的DMA错乱最后都能在“一致性”这个源头上找到解释。这篇文章里的经验是我在多个平台上反复验证过的但愿能帮你少熬几个通宵。如果你项目里也遇到过更奇葩的DMA一致性bug欢迎在评论区聊聊说不定互相一碰撞就能把问题挖到根子上。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询