3. Linux内核DMA子系统

在Linux内核中,DMA子系统承担着“让外设直接访问内存而无需CPU逐字节搬运”的职责。理解DMA性能瓶颈与缓存一致性维护,必须先厘清三个层次:DMAEngine框架负责抽象DMA控制器能力,DMA映射接口负责建立设备可见的总线地址,一致性映射与流式映射则决定缓存一致性由谁维护、何时维护。三者关系可用下图概括。

Linux DMA子系统三层结构 DMAEngine 框架(dmaengine.h / dmaengine.c) dma_device 控制器能力描述 dma_chan 通道申请与释放 dma_desc 传输描述符 dma_cookie 完成状态跟踪 调用映射接口 DMA 映射接口(dma-mapping.h) 一致性映射 dma_alloc_coherent() 长期有效,CPU/设备同时可见 流式映射 dma_map_single/sg/page() 按传输方向临时建立 同步与撤销 dma_sync_* / dma_unmap_* 维护缓存一致性 产生总线地址 硬件与内存 DMA控制器 发起总线读写 IOMMU / SMMU 地址转换与隔离 系统内存 DDR / Cache 外设 FIFO 关键:DMAEngine 只负责调度传输,缓存一致性由映射接口与架构代码共同保证

一、DMAEngine框架:把DMA控制器抽象成统一对象

不同SoC的DMA控制器寄存器、通道数、传输能力差异巨大。DMAEngine框架的目标是让驱动开发者用统一API发起传输,而不必关心底层是PL330、DW-DMAC还是EDMA。其核心对象包括:

  • dma_device:描述一个DMA控制器,包含通道数量、支持的方向、最大传输长度、能力标志等。
  • dma_chan:一条可独立工作的DMA通道,驱动通过 dma_request_chan() 申请。
  • dma_desc:传输描述符,描述源地址、目的地址、长度、传输模式。
  • dma_cookie:传输完成状态跟踪,用于异步等待和回调。

典型使用流程如下:

struct dma_chan *chan;
struct dma_device *dev;
struct dma_async_tx_descriptor *tx;
dma_cookie_t cookie;

chan = dma_request_chan(&pdev->dev, "rx");
dev = chan->device;
tx = dev->device_prep_dma_memcpy(chan, dst, src, len, DMA_CTRL_ACK);
tx->callback = my_dma_callback;
tx->callback_param = my_data;
cookie = tx->tx_submit(tx);
dma_async_issue_pending(chan);

这里的关键是:DMAEngine只负责“搬”,不负责“缓存一致性”。如果驱动直接把CPU虚拟地址交给DMA控制器,设备访问的物理地址与CPU缓存中的副本可能不一致,因此必须经过DMA映射接口。

二、DMA映射接口:建立设备可见的总线地址

DMA映射接口解决三个问题:地址转换、缓存一致性、访问权限。它把CPU虚拟地址转换为DMA控制器可用的总线地址(dma_addr_t),并根据映射类型决定是否需要软件维护缓存。

映射类型 典型API 生命周期 缓存一致性维护 适用场景
一致性映射 dma_alloc_coherent() 长期,通常驱动生命周期 硬件保证,CPU与设备看到同一份数据 描述符环、状态寄存器、长期共享缓冲区
流式映射 dma_map_single() / dma_map_sg() 单次传输,用完即撤销 软件按方向执行cache flush/invalidate 网络包、块设备数据、音频采样
流式映射(页面) dma_map_page() 单次传输 同流式映射 页级缓冲区、scatterlist构建

三、一致性映射与流式映射的本质区别

两者最核心的差异在于缓存一致性由谁维护、维护多久。

一致性映射在分配时就保证CPU和设备看到一致的内存视图。在多数ARM/ARM64平台上,它通过将内存标记为Non-cacheable或Write-Combine,或者由硬件一致性互连来保证。驱动拿到的是虚拟地址和总线地址两个值,可以长期使用,不需要每次传输前后调用sync。代价是访问速度可能低于普通cacheable内存,且占用连续的DMA区域。

流式映射则假设内存仍然是cacheable的,CPU访问很快。但设备读写的是物理内存,CPU缓存中可能有旧副本。因此驱动必须在设备读取前把CPU写的数据flush到内存,在设备写入后把CPU缓存invalidate掉,避免读到旧数据。这个动作由 dma_map_* 和 dma_sync_* 根据DMA方向自动完成。

方向决定同步动作:

  • DMA_TO_DEVICE:CPU写、设备读,映射时flush CPU缓存。
  • DMA_FROM_DEVICE:设备写、CPU读,映射时invalidate CPU缓存。
  • DMA_BIDIRECTIONAL:双向,flush + invalidate,开销最大。

四、案例:网卡收包路径中的映射选择

以典型网卡驱动为例,收包环形缓冲区中的描述符环通常使用一致性映射,因为网卡和CPU会频繁读写描述符状态,且生命周期长。而实际数据包缓冲区使用流式映射:

/* 描述符环:一致性映射 */
desc = dma_alloc_coherent(&pdev->dev,
                          ring_size * sizeof(*desc),
                          &desc_dma, GFP_KERNEL);

/* 数据缓冲区:流式映射 */
dma_addr = dma_map_single(&pdev->dev, skb->data,
                          skb->len, DMA_FROM_DEVICE);
/* 网卡将数据写入 dma_addr 指向的内存 */
...
dma_unmap_single(&pdev->dev, dma_addr,
                 skb->len, DMA_FROM_DEVICE);
/* 此时CPU可安全读取skb->data */

如果错误地把数据缓冲区做成一致性映射,虽然省去了sync,但会失去cache加速,收包吞吐可能下降;反之,如果把描述符环做成流式映射却忘记sync,网卡可能读到过期的描述符状态,导致丢包或DMA异常。

常见误区:认为“一致性映射就是性能更好的映射”。实际上,一致性映射牺牲了cache性能换取编程简单;流式映射保留了cache性能,但要求驱动严格配对map/unmap和sync。选择依据是访问频率、生命周期和方向,而不是“哪个更高级”。

五、复盘:DMA映射检查清单

  1. 长期共享、频繁双向访问的结构(描述符、状态字)优先用 dma_alloc_coherent()。
  2. 单次大批量数据传输优先用 dma_map_single() 或 dma_map_sg(),保留cache收益。
  3. 每次 dma_map_* 必须有对应的 dma_unmap_*,否则会造成IOMMU映射泄漏或cache状态混乱。
  4. 方向参数必须真实反映数据流向,否则sync动作相反,导致数据损坏。
  5. 在带IOMMU的平台上,映射接口还会建立IOVA到PA的转换,错误映射可能触发IOMMU fault。
  6. 调试时可用 dma-debug 检查映射泄漏、重复映射和方向错误。
# 开启DMA-API调试
echo 1 > /sys/kernel/debug/dma-api/dump
dmesg | grep -i "DMA-API"

掌握DMAEngine、映射接口以及一致性/流式映射的边界,是后续分析DMA性能瓶颈和缓存一致性维护的基础。只有映射类型选对、sync时机正确,DMA才能在保证数据正确的前提下接近理论带宽。