4. Linux内核DMA API:dma_alloc_coherent、dma_map_single、dma_map_sg、DMA方向与一致性
各位同学,今天我们聊一个硬核话题——Linux内核里的DMA API。
做PCIE驱动,说白了就是跟数据搬运打交道。CPU搬数据太慢,那就让硬件自己搬。但硬件搬数据有个麻烦:它不认识虚拟地址,只认物理地址。而且,它还要求内存是连续的、没有缓存污染的。这就引出了我们今天的主角——DMA API。
4.1 一致性DMA缓冲区:dma_alloc_coherent
先说说 dma_alloc_coherent。这个函数我用的最多,尤其是在做数据采集卡驱动的时候。
它的作用很简单:分配一块内存,这块内存CPU和DMA控制器都能访问,而且不需要考虑缓存一致性问题。为什么?因为它在分配时就把页表里的缓存属性关掉了(或者干脆用非缓存内存)。
函数原型长这样:
dma_addr_t dma_handle;
void *cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
返回两个地址:cpu_addr 是CPU看到的虚拟地址,dma_handle 是DMA设备看到的物理地址(总线地址)。
核心要点: 一致性映射 = 分配时就把缓存问题解决了。CPU和设备看到的永远是一致的数据。
我个人习惯在设备初始化时就分配好这块内存,然后一直用到驱动卸载。比如PCIE的BAR空间映射、大块数据缓冲区,用这个最省心。
小提示: 分配的大小要谨慎。我见过有人一次分配几十MB,结果系统直接OOM。建议按需分配,一般4KB到几MB就够了。
4.2 流式DMA映射:dma_map_single
接下来是 dma_map_single。这个函数跟上面那个不一样——它不分配内存,而是把已有的内存映射给DMA使用。
什么意思呢?你想想看,有时候数据已经在某个缓冲区里了(比如网络包、文件缓存),你不想再拷贝一份。那就直接用 dma_map_single 告诉DMA控制器:这块内存的物理地址在这里,你去搬吧。
dma_addr_t dma_handle = dma_map_single(dev, cpu_addr, size, direction);
这里有个关键参数——direction。它告诉内核数据往哪个方向流动。内核会根据方向做不同的缓存处理:
| DMA方向 | 含义 | 缓存处理 |
|---|---|---|
| DMA_TO_DEVICE | CPU -> 设备 | 刷出缓存(写回内存) |
| DMA_FROM_DEVICE | 设备 -> CPU | 无效化缓存(让CPU重新读内存) |
| DMA_BIDIRECTIONAL | 双向 | 刷出 + 无效化 |
| DMA_NONE | 未定义 | 调试用,别乱用 |
警告: 方向搞错了,数据就乱了。我曾经在调试一个FPGA加速卡时,把DMA_FROM_DEVICE写成了DMA_TO_DEVICE,结果CPU读到的全是旧数据,折腾了两天才发现。
用完记得调用 dma_unmap_single 释放映射。不然DMA控制器可能还占着那块内存,别的驱动就用不了了。
4.3 分散/聚集映射:dma_map_sg
这个函数厉害了。它解决了一个实际问题:物理内存不连续怎么办?
你想想看,用户态程序分配的内存,在物理上可能是东一块西一块的。但很多DMA控制器只支持连续的物理地址。这时候 dma_map_sg 就派上用场了。
它接受一个 scatterlist 数组,里面描述了多个不连续的内存块。然后它把这些块映射成DMA能理解的格式:
struct scatterlist sg[2];
sg_init_table(sg, 2);
sg_set_buf(&sg[0], buf1, len1);
sg_set_buf(&sg[1], buf2, len2);
int nents = dma_map_sg(dev, sg, 2, direction);
返回的 nents 是实际映射的段数。有些DMA控制器支持硬件SG(分散/聚集),那 nents 就等于你传入的段数。如果不支持,内核可能会帮你合并成更少的段。
实战经验: 我在做NVMe驱动时,经常用dma_map_sg处理大块IO请求。一个4MB的读请求,可能对应几十个物理不连续的页。用SG映射,一次DMA操作就能搞定,效率高很多。
遍历映射后的SG列表,用 sg_dma_address 和 sg_dma_len 获取DMA地址和长度:
for_each_sg(sg, s, nents, i) {
dma_addr_t addr = sg_dma_address(s);
unsigned int len = sg_dma_len(s);
// 把addr和len填入DMA描述符
}
4.4 DMA方向与一致性模型
最后聊聊一致性问题。说白了就是:CPU和DMA控制器看到的同一块内存,数据是不是一样的?
有两种模型:
- 一致性模型(Coherent):用
dma_alloc_coherent分配的内存。CPU和设备随时都能看到最新数据,不需要手动同步。代价是分配和访问速度稍慢(因为禁用了缓存)。 - 流式模型(Streaming):用
dma_map_single或dma_map_sg映射的内存。数据一致性由你手动管理——映射时内核帮你做缓存同步,取消映射时再做一次。
怎么选?我的建议是:
- 控制寄存器、状态标志、小数据量 → 用一致性映射
- 大数据块、频繁传输 → 用流式映射(性能更好)
避坑指南: 我曾经在流式映射的缓冲区上,CPU写数据后忘了调用 dma_sync_single_for_device,结果DMA读到的全是缓存里的旧数据。记住:流式映射下,CPU和设备不能同时访问同一块内存。要么CPU写完通知设备,要么设备写完通知CPU。
同步函数也很简单:
// CPU写完后,通知设备可以读了
dma_sync_single_for_device(dev, dma_handle, size, DMA_TO_DEVICE);
// 设备写完后,通知CPU可以读了
dma_sync_single_for_cpu(dev, dma_handle, size, DMA_FROM_DEVICE);
嗯,到这里DMA API的核心内容就讲完了。总结一下:
dma_alloc_coherent:分配一致性内存,简单但稍慢dma_map_single:映射已有内存,灵活但需手动同步dma_map_sg:处理不连续内存,适合大块IO- 方向参数决定缓存处理方式,千万别搞反
下一章我们实战一下,写一个PCIE驱动的DMA传输示例。到时候你会看到这些API是怎么配合使用的。