做Linux下的驱动开发,绕不开DMA。只要涉及数据搬运,特别是大数据量、高吞吐的场景,比如网络收包、存储读写、串口/SPI高速传输,DMA几乎都是标配。但DMA这块的知识点非常零散,接口多、概念绕,还牵扯到cache一致性这类底层细节,很多新手上来就被dma_alloc_coherent、dma_map_single这些API搞晕。这个系列我打算把Linux DMA开发从头到尾捋一遍,第一篇先啃最核心的骨架:DMA是什么、内核里DMA映射机制怎么运作、dmaengine框架怎么用,以及调试DMA问题时最该从哪下手。
写这篇内容之前,我先说下适合谁来读。如果你是做嵌入式Linux驱动开发的,或者正被设备树里的dma属性、驱动里的DMA中断、数据偶尔乱码这些事折磨,这篇内容应该能帮你省不少时间。如果你只是天天写应用层代码,想了解DMA到底是怎么回事,也能看,前提是至少知道物理地址、虚拟地址、内核驱动模块是怎么回事。
1. 从一次数据搬运说起:Linux DMA的核心问题和设计思路
1.1 DMA表面上解决的是CPU占用问题,实际上牵扯出一整套地址转换机制
DMA的字面意思就是直接内存访问。没有DMA的时候,CPU要搬运一块数据,得自己去读源地址、写入目的地址,循环一遍。以串口为例,假设外设每秒产生几万个字节,CPU如果每次都用中断去处理单个字节,那CPU基本什么都干不了了,全在响应中断、搬数据。
有了DMA,事情就变成:CPU把源地址、目的地址、搬运长度告诉DMA控制器,然后DMA控制器自己去搬,搬完了提个中断告诉CPU“我干完了”。这个过程里CPU只负责配置DMA和接收完成中断。在大数据量场景,节省的CPU资源非常可观。
问题在于:Linux下,驱动里拿到的地址不是咱们平时在应用层理解的那个地址。内核里一个缓冲区可能有三种地址属性:
| 地址类型 | 说明 | 典型例子 |
|---|---|---|
| 虚拟地址 | CPU运行时指令里用到的地址 | kmalloc返回的地址 |
| 物理地址 | CPU地址总线上的地址 | 用于MMU关闭时的裸机访问 |
| 总线地址 | DMA控制器看到的地址 | 设备寄存器里填写的地址 |
在没有IOMMU的简单平台上,总线地址几乎等价于物理地址。但一旦有IOMMU、有总线桥、或者有复杂的地址映射关系,总线地址和物理地址可以是完全不同的值。设备寄存器里填写的DMA地址,是设备视角的地址,必须通过内核DMA API来获取,而不能直接把物理地址填进去。
这就是Linux DMA开发的第一个核心认知:驱动里不能随便拿物理地址去配置DMA,必须调用内核提供的DMA映射API。这个API系列会帮你处理缓存一致性、IOMMU映射、总线地址转换,而不仅仅是“拿到一个地址”这么简单。
1.2 缓存一致性:DMA开发里最容易出诡异Bug的根源
在ARM、x86这类带高性能CPU的平台上,CPU和DMA控制器看到的内存不是一致的。CPU读写内存,通常会经过cache,读的时候如果cache命中,根本不会访问真正的物理内存。DMA控制器则直接访问物理内存,不经过cache。
举个例子。你在驱动里申请了一块缓冲区,往里面写了一串数据,然后启动DMA发送。如果CPU写入的数据还停留在cache里,没有回写到物理内存,DMA去搬运的时候,搬走的可能是旧数据。反过来,DMA把外设数据放进了内存,CPU去读的时候,如果cache里还留着旧数据,CPU读到的就是垃圾数据。
这个问题处理不好,表现就是数据偶尔错、偶尔对,完全随机,特别难排查。所以Linux内核提供了一致性DMA映射和流式DMA映射两套机制来处理cache问题。我下面会花大篇幅讲这两套机制,因为它们是Linux DMA开发的地基。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 两个DMA映射API家族:一致性映射和流式映射
2.1 一致性DMA映射:dma_alloc_coherent
一致性DMA映射,又叫coherent mapping,核心特点是:在映射的生命周期内,CPU和设备看到的数据始终一致。实现机制上,内核会分配一段内存,要么关闭这段内存的cache属性,要么在硬件层面维护cache一致性(比如某些SoC支持硬件自动同步),保证CPU写进去的数据DMA控制器能立刻看到,DMA控制器写进去的数据CPU能立刻读到。
常用API是:
c复制dma_addr_t dma_handle;
void *cpu_addr;
cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
if (!cpu_addr) {
/* 分配失败处理 */
}
这个函数返回两个地址:cpu_addr是CPU侧的虚拟地址,驱动代码里用它来访问缓冲区;dma_handle是设备侧的总线地址,填写到设备寄存器里。这两个地址在函数返回后就已经是合法的映射关系,不需要再做额外的转换。
使用结束后释放:
c复制dma_free_coherent(dev, size, cpu_addr, dma_handle);
使用场景:适合那种CPU和设备会频繁读写同一块内存、且对一致性要求极高的场景,典型的有DMA描述符环(descriptor ring)、网络驱动的RX ring、以及一些共享状态结构体。它的缺点是分配时开销相对较大,而且可能占用较大的连续物理内存,不适合做超大缓冲区的动态分配。
2.2 流式DMA映射:dma_map_single / dma_unmap_single
流式DMA映射,又叫streaming mapping,和一致性映射最大的区别是:它不保证整个生命周期内cache始终一致,而是通过一次性的同步操作来维护一致性。用起来更像是一个“临时借用”的过程:驱动把缓冲区的映射建立好,DMA传输期间CPU不要乱碰这块内存,传输完成后调用sync接口把数据同步过来。
典型流程:
c复制dma_addr_t dma_addr;
dma_addr = dma_map_single(dev, cpu_addr, size, DMA_TO_DEVICE);
if (dma_mapping_error(dev, dma_addr)) {
/* 映射失败处理 */
}
/* 启动DMA传输... */
dma_unmap_single(dev, dma_addr, size, DMA_TO_DEVICE);
dma_map_single接收CPU虚拟地址,完成地址转换和必要的cache操作,然后返回设备侧的总线地址。DMA_TO_DEVICE表示数据方向是CPU到设备,还有几个方向取值,我下面会细说。
使用场景:典型的是一次性数据传输,比如每接收一笔数据、每发送一个包都动态建立映射
