4. Linux DMA API:dma_alloc_coherent、dma_map_single、dma_map_sg、API使用场景与注意事项

在嵌入式Linux系统中,DMA(Direct Memory Access)是提升外设吞吐量的核心技术。Linux内核提供了一套标准化的DMA API,用于管理设备与内存之间的数据传输。本章将深入剖析三个最核心的DMA API:dma_alloc_coherent、dma_map_single 和 dma_map_sg,并结合Raw格式输出与Buffer共享的实际场景,讲解其使用方式与避坑要点。

核心概念: DMA API 的核心目标是解决“Cache一致性”问题。CPU通过Cache加速内存访问,而DMA设备直接访问物理内存。如果Cache中的数据与物理内存不一致,就会导致数据错误。Linux DMA API通过一致性映射(Coherent)或流式映射(Streaming)来管理这种一致性。

4.1 dma_alloc_coherent:一致性DMA缓冲区

dma_alloc_coherent 用于分配一块在CPU和DMA设备之间保持缓存一致性的内存区域。该函数返回两个地址:一个给CPU使用的虚拟地址(vaddr),一个给DMA设备使用的物理地址(dma_handle)。

函数原型与参数

#include <linux/dma-mapping.h>

void *dma_alloc_coherent(struct device *dev, size_t size,
                         dma_addr_t *dma_handle, gfp_t flag);
参数 说明 典型值
dev 请求DMA的设备指针 从platform_device或pci_dev获取
size 分配的字节数(必须为2的幂次对齐) PAGE_SIZE 或其倍数
dma_handle 输出参数,返回DMA总线地址 传递给DMA控制器寄存器
flag 内存分配标志 GFP_KERNEL(进程上下文)或 GFP_ATOMIC(中断上下文)

使用场景

  • Raw格式视频帧缓冲区: 摄像头传感器通过DMA直接将Raw数据写入此缓冲区,CPU可直接读取处理,无需手动刷新Cache。
  • 音频DMA缓冲区: I2S控制器与内存之间的音频数据流,要求低延迟且无Cache一致性问题。
  • 控制类数据交换: 如USB控制传输、SPI命令缓冲区,数据量小但要求实时一致。
注意事项:
  • 分配的内存是连续的物理内存,大块分配(> 4MB)可能失败,建议使用 dma_alloc_noncoherent 或 CMA(Contiguous Memory Allocator)。
  • 该API内部会禁用该段内存的Cache(或使用特殊内存属性),因此CPU访问速度可能略慢于普通内存。
  • 必须配对使用 dma_free_coherent 释放,否则造成内存泄漏。
dma_alloc_coherent 内存映射关系 CPU 虚拟地址空间 vaddr (虚拟地址) DMA 设备地址空间 dma_handle (总线地址) 物理内存 (一致映射区域) Cache 被禁用或硬件自动同步 CPU 和 DMA 设备通过同一块物理内存直接交换数据,无需软件维护一致性

4.2 dma_map_single:流式DMA映射

dma_map_single 用于将一段已存在的内核缓冲区(如kmalloc分配的、栈上的或全局数组)映射给DMA设备使用。它不分配新内存,而是建立DMA地址与物理页的映射关系,并处理Cache同步。

函数原型与参数

dma_addr_t dma_map_single(struct device *dev, void *cpu_addr,
                          size_t size, enum dma_data_direction dir);
参数 说明 典型值
dev 设备指针 同 dma_alloc_coherent
cpu_addr CPU虚拟地址(必须为内核逻辑地址或直接映射地址) kmalloc 返回的地址
size 映射的字节数 缓冲区实际数据长度
dir 数据传输方向 见下方方向枚举

数据传输方向枚举

  • DMA_TO_DEVICE: CPU写入数据,DMA读取(如发送网络包)。
  • DMA_FROM_DEVICE: DMA写入数据,CPU读取(如接收网络包)。
  • DMA_BIDIRECTIONAL: 双向传输,性能开销最大,尽量少用。
  • DMA_NONE: 仅用于调试或初始化。

使用场景

  • 网络驱动: 将skb的数据缓冲区映射给DMA引擎发送或接收。
  • 块设备驱动: 将bio请求的页映射给存储控制器。
  • Raw格式输出优化: 将用户态通过mmap映射的缓冲区,在内核态使用 dma_map_single 映射给显示控制器或ISP。
最佳实践: 流式映射适用于“临时借用”内存给DMA使用。每次DMA传输完成后,必须调用 dma_unmap_single 解除映射,并调用 dma_sync_single_for_cpu 确保CPU看到最新数据。
dma_map_single 流式映射流程 步骤1: 分配缓冲区 kmalloc / 栈 / 全局 步骤2: dma_map_single 获取 dma_addr 步骤3: 启动DMA 写 dma_addr 到设备 步骤4: 传输完成 DMA 中断 dma_sync_single_for_cpu / dma_unmap_single 确保Cache一致性,释放DMA映射 流式映射不分配新内存,只建立映射关系。每次DMA传输后必须解映射,否则数据可能不一致。 方向参数决定Cache刷新的策略:TO_DEVICE 刷出Cache,FROM_DEVICE 无效化Cache。

4.3 dma_map_sg:分散/聚集DMA映射

dma_map_sg 用于将一组物理上不连续的缓冲区(Scatter-Gather列表)映射为DMA可访问的地址列表。它返回映射成功的段数,并填充每个sg_entry的DMA地址和长度。

函数原型与参数

int dma_map_sg(struct device *dev, struct scatterlist *sg,
               int nents, enum dma_data_direction dir);
参数 说明 典型值
dev 设备指针 同前
sg scatterlist 数组首地址 由上层分配并填充
nents sg 数组中的条目数 物理段数量
dir 数据传输方向 同 dma_map_single

使用场景

  • 存储驱动: NVMe、SD/MMC 驱动处理不连续的块请求。
  • 视频处理: 将多个YUV平面(Y、U、V分别在不同物理页)映射给硬件编码器。
  • Raw格式Buffer共享: 当Raw帧由多个物理不连续的chunk组成时,使用SG列表一次性映射,减少DMA设置开销。

API使用流程

  1. 分配并初始化 struct scatterlist 数组。
  2. 调用 sg_set_page 或 sg_set_buf 填充每个段。
  3. 调用 dma_map_sg 获取DMA映射。
  4. 遍历映射后的sg列表,使用 sg_dma_address 和 sg_dma_len 获取DMA地址和长度。
  5. DMA传输完成后,调用 dma_unmap_sg 解除映射。
性能优势: 相比多次调用 dma_map_single,dma_map_sg 只需一次IOMMU页表更新或Cache同步操作,显著降低映射开销。在Raw格式高帧率输出场景中,这是关键优化点。
dma_map_sg 分散/聚集映射结构 物理内存(不连续) Page 0: Raw Data Chunk 0 Page 1: Raw Data Chunk 1 Page 2: Raw Data Chunk 2 Page 3: Raw Data Chunk 3 Scatter-Gather 列表 sg[0]: dma_addr=0x1000, len=4096 指向 Page 0 sg[1]: dma_addr=0x2000, len=4096 指向 Page 1 sg[2]: dma_addr=0x3000, len=4096 指向 Page 2 sg[3]: dma_addr=0x4000, len=4096 指向 Page 3 DMA 设备视角 DMA 引擎 读取 SG 列表 自动遍历 每个段 连续传输 无需CPU干预 dma_map_sg 将多个物理不连续页映射为DMA可访问的连续地址列表。 硬件DMA引擎自动遍历SG列表,实现“分散读、聚集写”或“分散写、聚集读”。

4.4 API对比与选择指南

特性 dma_alloc_coherent dma_map_single dma_map_sg
内存分配 分配新内存 映射已有内存 映射已有内存(多段)
Cache一致性 硬件保证(禁用Cache或硬件同步) 软件管理(需sync操作) 软件管理(需sync操作)
物理连续性 连续 连续(单段) 不连续(多段)
适用数据量 中等(建议 < 4MB) 小到中等 大(可超过4MB)
典型延迟 分配时高,使用时低 映射时低,需sync 映射时略高,传输效率高
Raw格式场景 固定帧缓冲区 单帧临时映射 多平面/多chunk帧

4.5 常见错误与调试技巧

常见错误清单:
  • 忘记解映射: 每次 dma_map_single 或 dma_map_sg 后,必须在DMA传输完成且CPU访问前调用对应的 dma_unmap_*,否则导致内存损坏。
  • 方向错误: 使用 DMA_FROM_DEVICE 但CPU在映射后写入数据,会导致数据被Cache刷出覆盖。
  • 越界访问: DMA设备写入长度超过映射的size,破坏相邻内存。
  • 在中断上下文使用 GFP_KERNEL: dma_alloc_coherent 在中断中必须使用 GFP_ATOMIC。
  • 忽略 dma_set_mask: 未设置设备的DMA掩码(如64位地址支持),导致分配失败或地址截断。

调试技巧

  • 启用内核DMA调试:CONFIG_DMA_API_DEBUG=y,可检测未配对映射、双重释放等错误。
  • 使用 dma_debug_show_all 查看当前所有活跃的DMA映射。
  • 在QEMU或FPGA验证平台上,使用 trace_printk 跟踪dma_addr和传输长度。
  • 对于Raw格式输出,建议在驱动中增加 dma_sync_single_for_cpu 后的数据校验(如CRC或模式检查)。
性能优化要点:
  • 优先使用 dma_map_sg 替代多次 dma_map_single,减少IOMMU页表更新次数。
  • 对于高频小数据包,考虑使用 dma_pool 预分配一致性内存池。
  • 在Raw格式多Buffer乒乓操作中,使用 dma_alloc_coherent 预分配多个帧缓冲区,避免运行时分配。
  • 利用 dma_get_required_mask 查询设备实际支持的地址范围,避免不必要的bounce buffer。
DMA API 选择决策树 需要DMA传输? 需要分配新内存? 是 dma_alloc_coherent 固定缓冲区 / 帧缓冲 / 控制数据 否 映射已有内存 kmalloc / 用户态缓冲区 ← 上一章 📖 返回目录 下一章 →