3. Linux内核DMA子系统
在Linux内核中,DMA子系统承担着“让外设直接访问内存而无需CPU逐字节搬运”的职责。理解DMA性能瓶颈与缓存一致性维护,必须先厘清三个层次:DMAEngine框架负责抽象DMA控制器能力,DMA映射接口负责建立设备可见的总线地址,一致性映射与流式映射则决定缓存一致性由谁维护、何时维护。三者关系可用下图概括。
一、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映射检查清单
- 长期共享、频繁双向访问的结构(描述符、状态字)优先用
dma_alloc_coherent()。 - 单次大批量数据传输优先用
dma_map_single()或dma_map_sg(),保留cache收益。 - 每次
dma_map_*必须有对应的dma_unmap_*,否则会造成IOMMU映射泄漏或cache状态混乱。 - 方向参数必须真实反映数据流向,否则sync动作相反,导致数据损坏。
- 在带IOMMU的平台上,映射接口还会建立IOVA到PA的转换,错误映射可能触发IOMMU fault。
- 调试时可用
dma-debug检查映射泄漏、重复映射和方向错误。
# 开启DMA-API调试
echo 1 > /sys/kernel/debug/dma-api/dump
dmesg | grep -i "DMA-API"
掌握DMAEngine、映射接口以及一致性/流式映射的边界,是后续分析DMA性能瓶颈和缓存一致性维护的基础。只有映射类型选对、sync时机正确,DMA才能在保证数据正确的前提下接近理论带宽。