3. DMA 缓冲区分配与释放:连续内存分配、一致性 DMA 缓冲区、流式 DMA 映射

DMA 缓冲区是外设与系统内存之间进行高速数据交换的物理载体。与普通内存分配不同,DMA 缓冲区必须满足物理连续性、对齐约束以及缓存一致性等特殊要求。本章深入剖析 Linux 内核中三种核心 DMA 缓冲区管理机制:连续内存分配、一致性 DMA 缓冲区和流式 DMA 映射,帮助驱动开发者掌握从分配、使用到释放的完整生命周期。

3.1 连续内存分配:物理连续性的基石

大多数 DMA 控制器要求缓冲区在物理地址空间中是连续的,因为硬件无法处理分散的物理页。Linux 内核提供多种连续内存分配接口,适用于不同场景。

3.1.1 分配接口对比

接口 最大分配大小 对齐要求 适用场景
kmalloc() 通常 < 4 MB 自然对齐 小缓冲区、驱动初始化阶段
__get_free_pages() 2^order 页 页对齐 中等大小、对延迟敏感
dma_alloc_coherent() 无严格上限 DMA 掩码对齐 一致性 DMA 缓冲区
dma_alloc_noncoherent() 无严格上限 DMA 掩码对齐 需要手动同步的流式映射
经验法则: 对于小于一个页面的缓冲区,优先使用 kmalloc();对于中等大小(1~4 MB),使用 __get_free_pages();对于大缓冲区或需要 DMA 一致性保证的场景,始终使用 dma_alloc_coherent()。

3.1.2 连续内存分配流程

/* 示例:使用 dma_alloc_coherent 分配连续缓冲区 */
#include <linux/dma-mapping.h>

struct device *dev = &pdev->dev;
dma_addr_t dma_handle;
size_t size = SZ_1M;  /* 1 MB 连续缓冲区 */
void *cpu_addr;

cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
if (!cpu_addr) {
    dev_err(dev, "Failed to allocate coherent DMA buffer\n");
    return -ENOMEM;
}

/* 使用缓冲区... */

/* 释放缓冲区 */
dma_free_coherent(dev, size, cpu_addr, dma_handle);
关键约束: dma_alloc_coherent() 返回的 CPU 虚拟地址与 DMA 总线地址之间的映射是固定的,驱动不能假设两者之间存在简单的偏移关系。必须同时保存 cpu_addr 和 dma_handle 两个值。

3.2 一致性 DMA 缓冲区:缓存同步的零成本方案

一致性 DMA 缓冲区在分配时即保证 CPU 和 DMA 控制器看到的缓存视图一致。内核通过禁用缓存行(non-cacheable)或使用硬件一致性协议(如 ARM 的 ACE/CHI)来实现。

3.2.1 一致性缓冲区特性

  • 无需手动同步: CPU 和 DMA 设备可以同时访问缓冲区,无需调用 dma_sync_single_for_cpu() 或 dma_sync_single_for_device()。
  • 分配开销较大: 内核可能需要在启动时预留大块连续内存(CMA),或使用 IOMMU 映射。
  • 适用场景: 控制寄存器、状态描述符环、小数据包频繁交互。

3.2.2 一致性缓冲区生命周期

一致性 DMA 缓冲区生命周期 分配 CPU 访问 DMA 访问 并发访问 释放 关键特性:一致性缓冲区在分配时即保证缓存一致性,CPU 和 DMA 设备无需额外同步操作。

3.3 流式 DMA 映射:灵活性与性能的平衡

流式 DMA 映射允许驱动使用任意已分配的缓冲区(如栈、堆、文件系统缓存)进行 DMA 传输,但需要显式管理缓存一致性。这种机制在减少内存拷贝的同时,要求开发者精确控制同步时机。

3.3.1 流式映射的三种模式

方向 宏定义 典型场景
设备到 CPU DMA_FROM_DEVICE 网络接收、音频采集
CPU 到设备 DMA_TO_DEVICE 网络发送、显示帧缓冲
双向 DMA_BIDIRECTIONAL 存储设备、FPGA 数据交换

3.3.2 流式映射完整流程

/* 流式 DMA 映射示例:网络驱动发送路径 */
struct device *dev = &pdev->dev;
struct sk_buff *skb = /* 从网络栈获取 */;
dma_addr_t dma_addr;

/* 1. 映射缓冲区到 DMA 地址空间 */
dma_addr = dma_map_single(dev, skb->data, skb->len, DMA_TO_DEVICE);
if (dma_mapping_error(dev, dma_addr)) {
    dev_err(dev, "DMA mapping failed\n");
    goto err;
}

/* 2. 通知设备开始 DMA 传输 */
writel(dma_addr, hw->regs + TX_DMA_ADDR);
writel(skb->len, hw->regs + TX_DMA_LEN);
writel(1, hw->regs + TX_START);

/* 3. 等待传输完成(中断或轮询)... */

/* 4. 同步缓存(可选,对于 TO_DEVICE 通常不需要) */
dma_sync_single_for_cpu(dev, dma_addr, skb->len, DMA_TO_DEVICE);

/* 5. 解除映射 */
dma_unmap_single(dev, dma_addr, skb->len, DMA_TO_DEVICE);
性能关键点: 流式映射的 dma_map_single() 和 dma_unmap_single() 是开销较高的操作,因为它们可能涉及 IOMMU 页表更新和缓存刷写。对于高频小数据包场景,考虑在初始化时预映射缓冲区,避免运行时反复映射/解映射。

3.4 三种机制的对比与选择

维度 连续内存分配 一致性 DMA 缓冲区 流式 DMA 映射
物理连续性 必须连续 必须连续 不要求(IOMMU 可聚合)
缓存一致性 不保证 硬件保证 软件管理
分配/映射开销 低 中 高(每次映射)
灵活性 低 中 高
典型用途 驱动内部小数据 描述符环、控制寄存器 网络数据包、文件 I/O

3.5 常见陷阱与调试技巧

  • 忘记保存 dma_handle: 释放一致性缓冲区时必须同时提供 CPU 地址和 DMA 地址,丢失任何一个都会导致内存泄漏。
  • 流式映射方向错误: 使用 DMA_FROM_DEVICE 时,CPU 在读取前必须调用 dma_sync_single_for_cpu(),否则可能读到脏缓存数据。
  • IOMMU 回退: 当系统不支持连续内存时,dma_alloc_coherent() 可能通过 IOMMU 映射分散物理页,驱动应通过 dma_get_required_mask() 确认设备地址能力。
  • 调试工具: 使用 CONFIG_DMA_API_DEBUG 内核选项可以检测常见的 DMA API 误用,如双重映射、释放未映射的地址等。
调试命令: 在运行时检查 DMA 映射状态:
# cat /sys/kernel/debug/dma-api/dump
# echo 1 > /sys/kernel/debug/dma-api/error_count

3.6 本章小结

DMA 缓冲区管理是驱动开发中最容易出错但性能影响最大的环节之一。连续内存分配提供了物理连续性的基础;一致性 DMA 缓冲区通过硬件保证缓存一致性,简化了驱动逻辑;流式 DMA 映射则提供了最大的灵活性,但要求开发者精确控制同步时机。在实际项目中,应根据数据传输频率、缓冲区大小和硬件能力选择合适的机制,并始终使用内核提供的调试工具验证正确性。