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 → CPU):DMA把新数据写进物理内存,但CPU之前可能读过这块缓冲区,Cache里保留了旧副本。CPU去读缓冲区,命中的是Cache里的旧数据,DMA刚写入的新内容被无视了。
用个粗糙但好懂的类比:CPU是秘书,手边有一堆“桌面草稿”(Cache),正式档案存放在“文件柜”(内存)。DMA是另一个部门,只认文件柜里的版本。秘书改了一版文件,只放在桌上没有归档,DMA来取的时候拿到的当然还是旧版;反过来,DMA把新版本放进文件柜,秘书却还盯着桌上的旧草稿,自然也对不上。
1.2 先确认你的SoC到底要不要处理Cache一致性
很多人一上来就常规性地刷Cache,其实第一步应该是查硬件的Cache一致性机制。
部分现代SoC,比如带CCI/CMN总线的ARM高性能平台,或者x86架构配合IOMMU、PCIe硬件Snoop,DMA访问已经做到了硬件级一致。也就是说,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按方向自动刷Cache
Linux内核的DMA API其实是把最标准的做法写进了规范里。只要在合适的时机、按正确的方向调用dma_map_single和dma_unmap_single,Cache维护这个脏活就交给内核去办了。
这也是我在驱动里优先推荐的方法。因为它不要求你自己动手写任何刷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前让数据真正落到RAM |
| DMA_FROM_DEVICE | 需要处理Cache中的旧数据,防止脏写回覆盖DMA内容 | 需要Invalidate Cache,CPU再读时才会取内存中的新数据 | 接收完成后必须让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 Line:
DCCMVAC,把脏Cache行回写内存。 - Invalidate Cache Line:
DCIMVAC,丢弃Cache中的副本。 - Clean and Invalidate:
DCCIMVAC。
在Linux内核里,不直接依赖具体指令,而是使用封装好的底层接口,比如dmac_map_area、dmac_unmap_area。但更推荐的做法还是用dma_sync_single_*,因为它在不同架构间有可移植性。这里讲手动方式,是为了让你理解API背后发生了什么。
映射到实际操作位置:
发送方向,在DMA启动前对缓冲区做Clean:
clean_cache_mva(buffer, len); dma_wmb(); writel(CTRL_START, dev->regs + TX_CTRL);接收方向,在DMA完成中断里对缓冲区做Invalidate:
dma_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_base,DMA操作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动辄几KB,CPU解析时访存压力大,放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完成后没有Invalidate,CPU命中旧Cache行 | 确认接收路径invalidate是否执行 |
| 接收数据偶尔部分正确部分乱 | Cache Line部分被invalidate,殃及相邻变量 | 检查缓冲对齐、DMA与变量是否共享Cache Line |
| 加了内存屏障还是出错 | 屏障不是刷Cache,Cache问题没解决 | 先确保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问题再诡异,也能一步步拆解到源头。