3. DMA 缓冲区分配概述:一致性映射与流式映射

在 Linux 内核驱动中,DMA 缓冲区分配是设备与内存之间高效数据交换的基础。根据缓存一致性维护策略的不同,DMA 映射分为两种核心类型:一致性 DMA 映射(Consistent DMA Mapping)和流式 DMA 映射(Streaming DMA Mapping)。理解两者的区别、适用场景以及生命周期管理,是正确编写 DMA 驱动的关键。

核心概念: DMA 映射的本质是建立 CPU 虚拟地址、物理地址与设备总线地址之间的转换关系,并确保 CPU 与设备在访问同一块内存时不会因缓存(Cache)导致数据不一致。

3.1 一致性 DMA 映射 vs 流式 DMA 映射

下表从多个维度对比了两种映射方式的本质差异:

对比维度 一致性 DMA 映射 流式 DMA 映射
缓存策略 禁止 CPU Cache 对该区域进行缓存(或硬件自动维护一致性) 依赖软件在每次传输前后手动同步 Cache
分配开销 较高,通常需要分配连续物理内存并修改页表属性 较低,可直接映射已有的内核缓冲区(如 sk_buff、kmalloc 内存)
使用场景 DMA 描述符环、控制寄存器块、长时间共享的状态结构体 网络数据包、块设备 I/O 请求、音频采样数据等批量传输
API 函数 dma_alloc_coherent() / dma_free_coherent() dma_map_single() / dma_unmap_single() 及 SG 版本
生命周期 从分配开始到驱动卸载,长期有效 仅在单次 DMA 传输期间有效,传输完成后应立即解映射
对 CPU 访问的影响 CPU 读写直接访问物理内存,无 Cache 延迟,但性能较低 CPU 访问可享受 Cache 加速,但传输前后需要显式同步
经验法则: 如果设备需要频繁、随机地访问同一块内存(如更新描述符状态),使用一致性映射;如果数据是流式传输、一次性处理(如网络收发包),使用流式映射以获得更好的 CPU 缓存性能。

3.2 两种映射方式的架构对比

下面的 SVG 图展示了两种映射方式在系统架构中的位置与数据路径差异:

一致性 DMA 映射 vs 流式 DMA 映射 架构对比 一致性 DMA 映射 CPU Cache 旁路 (禁止缓存) 一致性 DMA 缓冲区 物理连续 | 无 Cache 别名问题 DMA 设备 直接访问,无需同步 流式 DMA 映射 CPU Cache 启用 (高性能访问) 流式 DMA 缓冲区 可以是任意内核内存 DMA 设备 需要显式同步 dma_sync_single_*() 刷出/无效化 Cache

3.3 分配时机与生命周期管理

两种映射方式的生命周期差异巨大,错误的生命周期管理是导致内核崩溃或数据损坏的常见原因。

3.3.1 一致性 DMA 映射的生命周期

  • 分配时机: 通常在驱动 probe() 函数中分配,或在设备第一次打开时分配。
  • 持续期: 从分配开始,贯穿设备运行全过程,直到驱动卸载或设备移除。
  • 释放时机: 在 remove() 函数或设备关闭的最后阶段释放。
/* 一致性 DMA 映射典型生命周期示例 */
static int my_driver_probe(struct platform_device *pdev)
{
    struct my_dev *dev = platform_get_drvdata(pdev);
    dma_addr_t dma_handle;

    /* 分配:在 probe 中完成,生命周期与设备绑定 */
    dev->coherent_buf = dma_alloc_coherent(&pdev->dev,
                                           BUF_SIZE,
                                           &dma_handle,
                                           GFP_KERNEL);
    if (!dev->coherent_buf)
        return -ENOMEM;

    dev->coherent_dma = dma_handle;
    /* ... 注册设备、初始化描述符环 ... */
    return 0;
}

static int my_driver_remove(struct platform_device *pdev)
{
    struct my_dev *dev = platform_get_drvdata(pdev);

    /* 释放:在 remove 中完成,确保所有 DMA 操作已停止 */
    dma_free_coherent(&pdev->dev, BUF_SIZE,
                      dev->coherent_buf, dev->coherent_dma);
    return 0;
}
关键约束: 一致性 DMA 缓冲区在分配后,CPU 访问该区域将绕过 Cache。这意味着 CPU 读写速度会下降(约等于直接访问内存的速度)。因此,一致性映射只适用于访问频率较低的控制结构,不适用于大数据量传输。

3.3.2 流式 DMA 映射的生命周期

  • 分配时机: 在每次 DMA 传输发起前动态映射。
  • 持续期: 仅覆盖单次 DMA 传输的完整过程(从映射到传输完成)。
  • 释放时机: 在 DMA 传输完成且 CPU 需要再次访问数据之前,立即解映射。
/* 流式 DMA 映射典型生命周期示例 */
static void my_driver_send(struct my_dev *dev, void *buffer, size_t size)
{
    dma_addr_t dma_addr;

    /* 1. 映射:在传输前创建流式映射 */
    dma_addr = dma_map_single(dev->device, buffer, size, DMA_TO_DEVICE);
    if (dma_mapping_error(dev->device, dma_addr))
        return; /* 映射失败处理 */

    /* 2. 将 dma_addr 写入设备描述符,启动 DMA 传输 */
    writel(dma_addr, dev->regs + DMA_SRC_ADDR);
    writel(size, dev->regs + DMA_SIZE);
    writel(1, dev->regs + DMA_START);

    /* 3. 等待 DMA 传输完成(中断或轮询) */
    wait_for_completion(&dev->dma_done);

    /* 4. 解映射:在 CPU 访问 buffer 之前必须完成 */
    dma_unmap_single(dev->device, dma_addr, size, DMA_TO_DEVICE);

    /* 5. 现在 CPU 可以安全地读取 buffer 中的数据 */
    process_buffer(buffer, size);
}

3.4 映射方向与同步语义

流式 DMA 映射需要明确指定数据传输方向,这直接决定了 Cache 同步操作的类型:

方向枚举 含义 Cache 同步操作
DMA_TO_DEVICE 数据从内存传输到设备(如网卡发送) 映射时刷出(Flush)Cache,确保设备看到最新数据
DMA_FROM_DEVICE 数据从设备传输到内存(如网卡接收) 解映射时无效化(Invalidate)Cache,确保 CPU 读取的是设备写入的数据
DMA_BIDIRECTIONAL 双向传输(较少使用,性能开销大) 映射时刷出,解映射时无效化,或使用更昂贵的同步策略
性能提示: 尽量使用 DMA_TO_DEVICE 或 DMA_FROM_DEVICE 而非 DMA_BIDIRECTIONAL。双向映射会导致每次传输都执行完整的 Cache 刷出+无效化操作,显著降低性能。

3.5 选择决策流程

下面的 SVG 流程图帮助开发者在实际项目中快速做出正确的映射选择:

DMA 映射类型选择决策流程 开始:需要 DMA 缓冲区 缓冲区是否长期 被设备和 CPU 共享? 是 一致性 DMA 映射 否 数据是否 批量流式传输? 是 流式 DMA 映射 否 重新评估设计 选择 DMA 方向 DMA_TO_DEVICE (内存→设备) DMA_FROM_DEVICE (设备→内存) DMA_BIDIRECTIONAL (尽量避免) 提示: 流式映射应优先使用单向方向(TO/FROM),仅在控制/状态复用场景下使用 BIDIRECTIONAL。

3.6 常见错误与避坑指南

高危错误: 在流式 DMA 传输完成后,未调用 dma_unmap_single() 或 dma_sync_single_for_cpu() 就直接访问缓冲区。这会导致 CPU 读到过时的 Cache 数据,造成数据损坏或静默错误。
  • 错误 1: 对一致性映射缓冲区使用 dma_sync_* 系列函数——这是不必要的,且可能引发性能惩罚。
  • 错误 2: 在流式映射有效期间,CPU 写入缓冲区后未同步就启动 DMA——设备可能读到旧数据。
  • 错误 3: 在中断上下文中分配一致性内存——dma_alloc_coherent() 可能睡眠,只能在进程上下文调用。
  • 错误 4: 忘记检查 dma_mapping_error()——映射可能因 IOMMU 或内存压力失败。
最佳实践总结:
  1. 控制结构用一致性映射,数据流用流式映射。
  2. 流式映射的生命周期严格限定在“映射→传输→解映射”之间。
  3. 始终指定正确的 DMA 方向,避免使用 BIDIRECTIONAL。
  4. 在 probe() 中分配一致性缓冲区,在 remove() 中释放。
  5. 每次流式映射后检查错误,每次传输完成后及时解映射。