
1. DMA读到旧数据根子出在Cache一致性做过嵌入式Linux驱动的人多半都遇到过这种诡异场景中断明明对了DMA的描述符状态字也确实更新了但CPU去读接收缓冲区时拿到的却是上一次甚至更早的陈旧数据。发送方向也一样数据明明通过CPU写进了buffer外设收到的却是几个节拍之前的旧内容。排了半天指针错误、地址错位最后才意识到是Cache在捣鬼。把这句话摆在最前面没有硬件Cache一致性机制的平台DMA绕过CPU缓存直接访问物理内存而CPU写数据时可能只落在Cache里还没写回内存DMA读到的自然就是旧数据DMA写进来的内容也可能被CPU缓存里的旧副本挡住让CPU看不到新数据。这几乎是每个非一致性DMA系统都会踩的坑。这篇文章会先讲清楚为什么会这样再给出我实际处理这类问题最常用的三招正确使用DMA映射API自动刷Cache、手动Cache维护配合内存屏障、以及直接分配一致性DMA缓冲区。这套方法既支持Linux驱动也适合裸机环境参考。内容偏ARM嵌入式方向x86和其他自带IO一致性DMA的平台可以直接跳到后面看如何判断。1.1 为什么硬件Cache会让DMA犯糊涂现代CPU一般有L1/L2甚至L3 Cache。Cache的存在是为了让CPU快速访问高频使用的数据。但在DMA场景下CPU和DMA是两个“看不见彼此”的访存发起方。CPU日常读写内存时往往不是在改物理内存而是在改Cache。采用write-back策略的Cache只有在Cache行被替换、手动Clean或硬件事件触发时才会把脏数据回写到内存。也就是说CPU说“我写好了”数据的真正归宿可能还在Cache里躺着。DMA控制器通常直接挂在内存总线上它不知道CPU的Cache里有什么也不会先看一眼Cache再去访问内存。于是在两个方向都出问题发送场景CPU → DMA → 外设CPU往缓冲区写数据Cache中产生脏行。DMA从物理内存直接读读到的自然还是老数据。接收场景外设 → DMA → CPUDMA把新数据写进物理内存但CPU之前可能读过这块缓冲区Cache里保留了旧副本。CPU去读缓冲区命中的是Cache里的旧数据DMA刚写入的新内容被无视了。用个粗糙但好懂的类比CPU是秘书手边有一堆“桌面草稿”Cache正式档案存放在“文件柜”内存。DMA是另一个部门只认文件柜里的版本。秘书改了一版文件只放在桌上没有归档DMA来取的时候拿到的当然还是旧版反过来DMA把新版本放进文件柜秘书却还盯着桌上的旧草稿自然也对不上。1.2 先确认你的SoC到底要不要处理Cache一致性很多人一上来就常规性地刷Cache其实第一步应该是查硬件的Cache一致性机制。部分现代SoC比如带CCI/CMN总线的ARM高性能平台或者x86架构配合IOMMU、PCIe硬件SnoopDMA访问已经做到了硬件级一致。也就是说DMA操作同样会参与CPU Cache的一致性协议硬件会自动做“监听”不需要软件刷Cache。在这种平台上如果你还是傻乎乎地每次收发都做clean/invalidate不仅白白损耗性能还可能把状态搞坏。举个真实踩过的例子某平台手册里写了DMA引擎能自动监听L2 Cache驱动里却保留了早期的刷Cache逻辑结果每次收发都会偶发“描述符状态异常”去掉手动刷Cache之后一切恢复。问题不在刷的动作而在于多余的操作破坏了硬件管理的一致性视图。判断方法一是查芯片参考手册里的Memory Coherency或DMA章节二是看内核设备树里对应设备节点是否有dma-coherent属性。dma-coherent表示该设备DMA与Cache保持一致Linux这一侧的通用DMA API会对应走捷径驱动代码里不需要再做同步操作。如果你确定板子硬件不具备一致性能力再继续往下看三招都基于“非一致性系统”这个前提。2. 第一招用DMA映射API按方向自动刷CacheLinux内核的DMA API其实是把最标准的做法写进了规范里。只要在合适的时机、按正确的方向调用dma_map_single和dma_unmap_singleCache维护这个脏活就交给内核去办了。这也是我在驱动里优先推荐的方法。因为它不要求你自己动手写任何刷Cache的底层操作只要守规矩就能保证CPU和DMA双方都看到正确数据。2.1 DMA_TO_DEVICE 和 DMA_FROM_DEVICE 的正确姿势搞清楚方向最重要。内核DMA API用direction参数表达的是数据的“最终流向”DMA_TO_DEVICE数据由CPU写入内存然后由设备通过DMA读取。对应发送路径。DMA_FROM_DEVICE数据由设备通过DMA写入内存然后由CPU读取。对应接收路径。对应到Cache操作上方向传输前传输后说明DMA_TO_DEVICE需要把CPU在Cache中写入的脏数据Clean回内存不需要额外操作DMA读内存不会改内存关键是启动DMA前让数据真正落到RAMDMA_FROM_DEVICE需要处理Cache中的旧数据防止脏写回覆盖DMA内容需要Invalidate CacheCPU再读时才会取内存中的新数据接收完成后必须让CPU的Cache失效因此dma_map_single在传输前调用dma_unmap_single在传输完成后调用内核会根据direction完成上述Cache维护。别小看API这套约定很多问题恰恰是调用时机错了。一个规范的发送流程是这样/* 发送 */ void *buffer ctx-tx_buf; size_t len packet_len; dma_addr_t dma_handle; memcpy(buffer, data, len); dma_handle dma_map_single(ctx-dev, buffer, len, DMA_TO_DEVICE); if (dma_mapping_error(ctx-dev, dma_handle)) { dev_err(ctx-dev, dma_map_single failed\n); return -EIO; } /* 把dma_handle和len写到设备寄存器然后启动传输 */ writel(len, ctx-regs TX_LEN); writel(dma_handle, ctx-regs TX_ADDR); writel(CTRL_START, ctx-regs TX_CTRL); /* 记住dma_handle传输完成中断里做unmap */ ctx-tx_dma dma_handle; ctx-tx_len len;/* 发送完成中断 */ if (status TX_DONE) { dma_unmap_single(ctx-dev, ctx-tx_dma, ctx-tx_len, DMA_TO_DEVICE); }接收流程要区分两个阶段。启动接收时需要先把缓冲区map好传输完成后在中断或tasklet里unmap并invalidate/* 接收前 */ dma_addr_t rx_dma; rx_dma dma_map_single(ctx-dev, ctx-rx_buf, RX_BUF_SIZE, DMA_FROM_DEVICE); writel(ctx-rx_buf_len, ctx-regs RX_LEN); writel(rx_dma, ctx-regs RX_ADDR); writel(CTRL_START, ctx-regs RX_CTRL); /* 接收完成中断 */ if (status RX_DONE) { dma_unmap_single(ctx-dev, ctx-rx_dma, ctx-rx_len, DMA_FROM_DEVICE); /* 到这里rx_buf里的数据才是DMA写进来的新数据 */ process_packet(ctx-rx_buf, ctx-rx_len); }这里有个非常容易犯的错在调用dma_map_single之后到dma_unmap_single之前CPU不能随意访问这块缓冲区。API建立的是一个“外设访问期”这期间CPU再写缓冲区内存里的数据是否最新就没人能保证了。有些新手在map后还往buffer里memcpy结果DMA传出去的还是旧内容排查半天发现是访问序违规。2.2 高频传输时的替代方案dma_sync_single_for_cpu/device有人会问每次收发都map/unmap开销是不是大了点对于高频小包场景确实如此。尤其是有IOMMU的平台map/unmap还涉及页表操作频繁调用代价不可小觑。这时可以用同步API替代反复map/unmap。思想是把map/unmap只看作“建立/释放设备对内存访问权限”的过程而把Cache维护用dma_sync_single_for_cpu和dma_sync_single_for_device单独完成。缓冲区只映射一次之后每次收发只做同步。接收路径典型写法/* 初始化时映射一次 */ ctx-rx_dma dma_map_single(ctx-dev, ctx-rx_buf, RX_BUF_SIZE, DMA_FROM_DEVICE); /* 每次启动接收 */ static void arm_rx(struct my_ctx *ctx) { dma_sync_single_for_device(ctx-dev, ctx-rx_dma, RX_BUF_SIZE, DMA_FROM_DEVICE); writel(RX_BUF_SIZE, ctx-regs RX_LEN); writel(ctx-rx_dma, ctx-regs RX_ADDR); writel(CTRL_START, ctx-regs RX_CTRL); } /* 接收完成 */ static void rx_complete(struct my_ctx *ctx) { dma_sync_single_for_cpu(ctx-dev, ctx-rx_dma, RX_BUF_SIZE, DMA_FROM_DEVICE); dma_rmb(); process_packet(ctx-rx_buf, ctx-rx_len); arm_rx(ctx); }注意dma_sync_single_for_device和dma_sync_single_for_cpu只是Cache维护手段并不会像map/unmap那样建立或释放IOMMU映射。如果你的设备没有IOMMU或者驱动不需要反复映射这个方法很适合。这套API最大价值是让“刷Cache”这件事变得标准化。但它不是万能钥匙CPU访问期和DMA访问期必须严格交替互不重叠。一旦有一个CPU线程在DMA传输过程中插入访问崩溃和脏数据只是一瞬间的事。3. 第二招手动Cache维护加内存屏障自己掌控全局Linux API固然方便但有些场景绕不开手动操作老版本内核、裸机驱动、或者需要极细致控制Cache维护时机的地方。另外面试或者排查问题时懂底层Cache指令也能省很多力气。这一招的核心是两件事Cache Clean/Invalidate操作以及配套的内存屏障。很多只刷Cache不加屏障的驱动也能跑但属于“运气好”换个CPU、换个总线问题立刻现形。3.1 底层Cache操作接口与调用位置在ARMv7/ARMv8裸机环境下Cache维护通常通过协处理器指令或者CPI段中的Cache操作by MVA完成。例如Clean Cache LineDCCMVAC把脏Cache行回写内存。Invalidate Cache LineDCIMVAC丢弃Cache中的副本。Clean and InvalidateDCCIMVAC。在Linux内核里不直接依赖具体指令而是使用封装好的底层接口比如dmac_map_area、dmac_unmap_area。但更推荐的做法还是用dma_sync_single_*因为它在不同架构间有可移植性。这里讲手动方式是为了让你理解API背后发生了什么。映射到实际操作位置发送方向在DMA启动前对缓冲区做Cleanclean_cache_mva(buffer, len); dma_wmb(); writel(CTRL_START, dev-regs TX_CTRL);接收方向在DMA完成中断里对缓冲区做Invalidatedma_rmb(); invalidate_cache_mva(buffer, len); dma_rmb(); process_packet(buffer, len);很多人会把Clean和Invalidate的顺序搞反。发送前该做的是Clean而不是Invalidate接收完成后该做的是Invalidate同时对旧的脏行要先Clean再Invalidate否则脏行数据可能还没来得及回写就被扬了。3.2 为什么还必须配内存屏障内存屏障管的是“访问顺序”它和Cache维护是两回事。我见过太多人把内存屏障当成刷Cache的替代品加了dma_rmb却不做invalidate数据照样错然后百思不得其解。屏障解决的是乱序问题处理器、编译器、总线都可能对内存访问重新排序。典型例子是发送方向/* 错误示范只写数据不写屏障 */ memcpy(buffer, data, len); clean_cache_mva(buffer, len); writel(CTRL_START, dev-regs CTRL);如果writel(CTRL_START)被CPU乱序提前执行DMA控制器可能在你还没Clean完成、甚至数据还没完全写入缓冲区时就开始传输。结果就是设备读到了不完整的包或者读到了旧值。正确做法是在数据准备和启动DMA之间插入写屏障memcpy(buffer, data, len); clean_cache_mva(buffer, len); dma_wmb(); /* 保证clean_cache以及buffer写入都对DMA引擎可见 */ writel(CTRL_START, dev-regs CTRL);接收方向DMA完成中断里需要读屏障if (readl(dev-regs IRQ_STATUS) RX_DONE) { dma_rmb(); /* 确保DMA写入内存的动作已经完成且有序可见 */ invalidate_cache_mva(rx_buf, len); dma_rmb(); /* 确保invalidate之后CPU再读buffer */ process_packet(rx_buf, len); }为什么中断里还要加dma_rmb因为DMA控制器写入内存是外设侧的主设备操作CPU观察到中断时DMA的写如果还没完全抵达内存或总线上仍有排队写后续读取可能读到半新不旧的数据。屏障可以约束CPU端读操作的顺序这也是内核文档里明确建议的用法。3.3 手动Cache维护时的描述符Ring示例在裸机网络驱动里我们经常用描述符Ring管理收发。描述符本身也是DMA要访问的内存同样存在Cache一致性问题。一个经典做法是描述符数组放在非Cacheable内存区域这样CPU每次读写描述符都直接访问RAM不需要维护Cache。但如果你偏要把描述符放在Cacheable内存里就得按下面这种模式处理/* 写发送描述符然后启动DMA */ static void prepare_tx_desc(struct my_dev *ctx, int idx, dma_addr_t payload_dma, u32 len) { volatile struct tx_desc *desc ctx-tx_ring[idx]; desc-addr payload_dma; desc-len len; desc-ctrl DESC_VALID | DESC_LAST; /* 确保描述符内容先写入内存 */ clean_cache_mva((void *)desc, sizeof(*desc)); dma_wmb(); /* 更新生产者索引让DMA引擎开始搬运 */ writel((idx 1) % RING_SIZE, ctx-regs TX_PRODUCER); }接收完成同理static void handle_rx_desc(struct my_dev *ctx, int idx) { volatile struct rx_desc *desc ctx-rx_ring[idx]; u32 len; dma_rmb(); invalidate_cache_mva((void *)desc, sizeof(*desc)); dma_rmb(); len desc-actual_len; if (desc-ctrl DESC_DONE) { invalidate_cache_mva(ctx-rx_payload[idx], len); process_packet(ctx-rx_payload[idx], len); } }这样搞确实繁琐所以即使裸机驱动很多老手的做法依然是描述符放Non-Cacheable区Payload缓存用Cacheable区只在Payload上做Cache操作。只有非常追求性能、Payload也不大时才把环形缓冲整体放到手动管理里反复折腾。4. 第三招直接分配一致性DMA缓冲区从根上解决如果每次收发都要刷Cache、加屏障总还是太累了。有没有一种办法让CPU和DMA操作同一块内存时硬件或软件映射机制自动保证双方看到的内容一致有。这就是一致性DMA映射。Linux下对应接口是dma_alloc_coherent和dma_free_coherent。用这个API分配出来的缓冲区CPU和设备不用再做Cache维护直接读直接写两边看到的结果都是同步的。4.1 dma_alloc_coherent 到底干了什么它的核心思路是把分配给DMA控制器使用的物理内存区域在CPU页表里映射为“不具备一般Cache缓存属性”的内存。通常是不允许Write-Back Cache缓存或者直接映射为保持“一致性”的硬件域。这样CPU访问该缓冲区时数据不只是停留在Cache而是直接落到内存DMA访问时自然也能拿到最新内容。在某些支持硬件总线一致性的平台上dma_alloc_coherent可能分配的仍是Cacheable内存但硬件监听机制保证了DMA和CPU共同看到同一份数据。无论哪种实现对驱动代码来说效果一样不需要手动Clean/Invalidate。典型用法struct desc_ring { void *virt_base; dma_addr_t dma_base; u32 n_desc; }; static int alloc_desc_ring(struct my_ctx *ctx, struct desc_ring *ring) { ring-virt_base dma_alloc_coherent(ctx-dev, ring-n_desc * sizeof(struct desc), ring-dma_base, GFP_KERNEL); if (!ring-virt_base) return -ENOMEM; memset(ring-virt_base, 0, ring-n_desc * sizeof(struct desc)); return 0; } static void free_desc_ring(struct my_ctx *ctx, struct desc_ring *ring) { dma_free_coherent(ctx-dev, ring-n_desc * sizeof(struct desc), ring-virt_base, ring-dma_base); }分配成功后virt_base是CPU使用的虚拟地址dma_base是DMA引擎使用的物理地址。CPU操作virt_baseDMA操作dma_base共享同一块物理内存。这类内存最适合放DMA描述符、环形队列索引、硬件状态字以及体积不大但访问频繁的控制信息块。下面这段描述符Ring的消费端就用得干干净净static bool rx_ring_has_packet(struct my_ctx *ctx) { struct rx_desc *desc ctx-rx_ring_desc[idx]; dma_rmb(); return (desc-ctrl DESC_DONE) ! 0; }这里还需要dma_rmb吗需要。一致性内存解决的是Cache一致性问题但没解决总线上的读写排序问题。DMA引擎写完描述符后CPU并不保证马上就能看到中间还有总线缓冲、写缓冲。用一次dma_rmb确保读到的是DMA写入后的最新状态。这是很多人的知识盲区我见过有人把一致性内存当成完全隔离结果还是读到旧描述符状态。4.2 哪些场景适合用一致性内存哪些并不适合一致性内存并不总是优于普通内存。它的主要代价是CPU访问性能。如果CPU每次读写都直接绕过Cache冲到内存那么对于大数据量Payload的解析和填充速度会明显下降。所以实际驱动里最常见的组合是描述符Ring、状态字、生产者/消费者索引这类“控制面”数据放一致性内存。真正的网络包Payload、大文件缓冲区这类“数据面”用普通Cacheable内存通过dma_map_single在收发瞬间做Cache维护。举个例子某网络驱动里环形描述符共256项每项64字节用dma_alloc_coherent分配每个包的数据缓冲区单独申请用dma_map_single做映射。这样描述符的读写频率非常高放到一致性内存能省下每包都Clean/Invalidate描述符的开销而Payload动辄几KBCPU解析时访存压力大放Cacheable内存再手动管理Cache收益更明显。有一个容易踩的坑dma_alloc_coherent分配大块连续内存并不总是成功。它拿的是物理连续页面还要满足一致性内存池或CMA分配器的限制。大块分配失败时驱动回退方案要么是改用dma_map_single配合分散页要么用sg表。所以要分配几MB甚至几十MB的视频帧缓冲别指望一致性内存在所有平台都能轻松给你。5. 实操避坑指南缓存行对齐与调试验证很多Cache相关的问题不是方向搞错而是对齐问题。Cache维护的基本单位是Cache Line不是字节。x86常见64字节ARM Cortex-A系列常见64字节老一些的ARM可能是32字节也有个别平台是128字节。如果你的DMA缓冲区和其他变量共用了一个Cache Line那麻烦就大了。5.1 缓存行对齐——最容易翻车的隐藏规则假设分配了一个缓冲区buf从0x1000到0x1010只有16字节而紧跟着地址0x1010到0x1020里放了一个相邻变量flag。Cache Line大小64字节也就是说0x1000到0x1040属于同一行。CPU之前访问过flag这个Line被缓存起来了。DMA往buf写入新数据然后CPU对buf做invalidate操作。invalidate是以整个Cache Line为单位的0x1000到0x1040这一行会被直接丢弃。如果flag的数据还没来得及回写或者CPU后面再从内存读flag内存里flag可能是旧值也可能因为脏行丢失而状态错乱。更恶心的是这种问题只在特定地址布局、特定时序下出现看起来完全随机非常难复现。解决办法DMA缓冲区首地址对齐到Cache Line大小。缓冲区长度向上对齐到Cache Line整数倍。不要让DMA缓冲区和普通变量共享同一Cache Line区域。如果DMA只需要操作一段很小的数据你依然要给整个Cache Line“买全单”该操作整行就操作整行。Linux内核里DMA映射API要求buffer地址按ARCH_DMA_MINALIGN对齐。自定义驱动里可以结合平台信息用dma_get_cache_line_size取到Cache Line大小然后对缓冲做对齐分配。裸机环境直接用编译期常量ALIGN即可。一个简单分配代码#define CACHE_LINE_SIZE 64 static unsigned char dma_buf[2 * CACHE_LINE_SIZE]; static unsigned char *aligned_buf; aligned_buf (unsigned char *)ALIGN((unsigned long)dma_buf, CACHE_LINE_SIZE);这里的冗余分配就是为了保证即使数组起始不对齐也能找到一段从Cache Line边界开始的空间。5.2 打开内核DMA调试开关让问题自己浮出来Linux提供了一套严格检测DMA映射使用错误的机制内核配置项叫CONFIG_DMA_API_DBUG编进内核后通过启动参数或sysfs观察。打开之后它会记录每次dma_map_*和dma_unmap_*的调用检测map/unmap次数不匹配在映射未释放时反复map同一地址使用未映射的地址访问设备越界访问使用了错误的direction排查Cache问题时我第一件事就是先开它。它能排除掉“API调用本身写错”的可能避免在错误的方向上浪费大量时间。debug信息通常在/sys/kernel/debug/dma-api目录下。内核也会在检测到问题时打印详细堆栈。在设备驱动模型里还可以通过dma_debug_driver驱动名只跟踪特定设备的DMA操作。这个过滤在大型系统里很有用否则log会被大量无关DMA操作淹没。5.3 常见症状速查表现象大概率原因排查方向发送数据偶尔变成旧内容重启后第一次正常DMA启动前Cache没Clean或者map后又写buffer检查DMA启动前的clean操作检查访问时序接收数据永远是上一次的内容DMA完成后没有InvalidateCPU命中旧Cache行确认接收路径invalidate是否执行接收数据偶尔部分正确部分乱Cache Line部分被invalidate殃及相邻变量检查缓冲对齐、DMA与变量是否共享Cache Line加了内存屏障还是出错屏障不是刷CacheCache问题没解决先确保Clean/Invalidate正确再加屏障平台宣称非一致性但怎么刷都还是旧数据可能是设备树标了dma-coherent或IOMMU配置导致查设备树和IOMMU配置换了CPU或总线频率后错误复现频率变化乱序窗口被放大缺少内存屏障在DMA启动/完成路径补全屏障最直接的验证方法是把DMA缓冲区临时映射为Non-Cacheable或Strongly-Ordered如果问题消失基本坐实是Cache一致性问题。不过要注意这只是排查手段正式方案仍应回到上面三招里选型。6. 写在最后我的几点体会这类问题真正难的地方不是“刷Cache”这个动作而是你不知道什么时候该刷、哪个方向刷、刷多大范围。我个人的经验是每接手一个新平台的DMA驱动第一件事就是看SoC手册里DMA Controller和CPU Cache之间有没有硬件一致机制确认之后再决定用哪一招。另外强烈建议在项目早期做一次最简单的DMA Cache一致性自测分配一块Cacheable内存CPU先写入一个特征字节然后启动DMA去读再看DMA写回的特征字节CPU能否立即读到。这个测试不到半天就能写好但能省下后面几个月的排错时间尤其是硬件和驱动同时Debug的阶段。如果非要总结一条最实用的心法那就是CPU和DMA是两套完全独立的内存视图你的驱动代码就是它们之间的翻译官。翻译出错数据自然错乱。把方向、时机、对齐这三件事刻在脑子里Cache问题再诡异也能一步步拆解到源头。