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 一致性缓冲区生命周期
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 映射则提供了最大的灵活性,但要求开发者精确控制同步时机。在实际项目中,应根据数据传输频率、缓冲区大小和硬件能力选择合适的机制,并始终使用内核提供的调试工具验证正确性。