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_addresssg_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控制器看到的同一块内存,数据是不是一样的?

有两种模型:

  1. 一致性模型(Coherent):用 dma_alloc_coherent 分配的内存。CPU和设备随时都能看到最新数据,不需要手动同步。代价是分配和访问速度稍慢(因为禁用了缓存)。
  2. 流式模型(Streaming):用 dma_map_singledma_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是怎么配合使用的。