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的平台上是IOVA(I/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把整块内存设置成uncached,CPU高频小数据访问时性能打折明显。dma_map_single保留cache能力,在每次map/unmap时做cache invalid/clean操作,传输前有一次同步,总开销可能更小,但对驱动的使用流程要求更高,必须严格配对map和unmap。
从易用性角度,dma_alloc_coherent明显更省心,不需要每次传输处理sync,也没有数据传输方向的问题。下表是我个人选型时的参考:
| 对比维度 | dma_alloc_coherent | dma_map_single |
|---|---|---|
| 使用场景 | 常驻缓冲区、大数据块 | 短生命周期、流式传输 |
| cache行为 | 通常uncached,CPU访问慢 | 保留cache,map/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,如果不加,内核会把缓冲区设置为uncached,CPU访问性能直接掉一个档次。
对于纯软件控制的普通外设,比如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-driver@1c00000 { 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区域。调试阶段也可以用启动参数快速验证,比如cma=128M。
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_DEBUG |
| ftrace + 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,欢迎在评论区聊聊,说不定互相一碰撞,就能把问题挖到根子上。