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 图展示了两种映射方式在系统架构中的位置与数据路径差异:
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 流程图帮助开发者在实际项目中快速做出正确的映射选择:
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 或内存压力失败。
最佳实践总结:
- 控制结构用一致性映射,数据流用流式映射。
- 流式映射的生命周期严格限定在“映射→传输→解映射”之间。
- 始终指定正确的 DMA 方向,避免使用 BIDIRECTIONAL。
- 在
probe()中分配一致性缓冲区,在remove()中释放。 - 每次流式映射后检查错误,每次传输完成后及时解映射。