3. DMA缓冲区基础:DMA概念、物理连续内存、IOMMU/SMMU、DMA API、dma_alloc_coherent

3.1 DMA核心概念与物理连续内存

DMA(Direct Memory Access) 是一种允许硬件设备(如摄像头、网卡、存储控制器)绕过CPU,直接与系统内存进行数据读写的技术。在V4L2视频采集场景中,DMA是保证高帧率、低延迟的关键——摄像头传感器通过DMA直接将图像数据写入内存缓冲区,CPU仅在缓冲区准备好后进行处理,避免了逐字节拷贝的开销。

关键约束: 传统DMA控制器只能访问物理连续内存。这是因为DMA引擎使用物理地址进行传输,且其地址寄存器通常不支持页表遍历。如果缓冲区在物理上不连续,DMA传输会跨越页边界导致数据错乱或总线错误。

物理连续内存的分配在长期运行的系统中容易导致内存碎片化。Linux内核提供了多种机制来应对:

  • 页分配器(alloc_pages):分配2的幂次阶的连续物理页,适用于小尺寸缓冲区。
  • CMA(Contiguous Memory Allocator):预留一大块物理连续区域,允许在未使用时被可移动页回收,专为大块连续内存设计。
  • IOMMU/SMMU:通过硬件页表将不连续的物理内存映射为连续的I/O虚拟地址,解除物理连续性约束。
DMA物理连续内存 vs IOMMU/SMMU映射 无IOMMU:物理连续 物理页 0x1000 物理页 0x2000 物理页 0x3000 物理页 0x4000 DMA设备直接使用物理地址 有IOMMU:虚拟连续 I/O虚拟地址 0xA000 I/O虚拟地址 0xB000 I/O虚拟地址 0xC000 I/O虚拟地址 0xD000 IOMMU页表映射到不连续物理页

3.2 IOMMU/SMMU:打破物理连续性约束

IOMMU(I/O Memory Management Unit) 在x86平台称为VT-d,在ARM平台称为SMMU(System Memory Management Unit)。它位于DMA设备与系统总线之间,负责将设备发出的I/O虚拟地址(IOVA)翻译为物理地址。

特性 无IOMMU 有IOMMU/SMMU
物理连续性 必须连续 不要求连续
地址空间隔离 设备可访问全部物理内存 设备仅能访问映射的IOVA范围
大缓冲区支持 依赖CMA或预留内存 通过页表聚合碎片页
性能开销 无翻译延迟 TLB miss时增加延迟
典型场景 低端嵌入式、USB摄像头 高端SoC、PCIe设备、虚拟化
V4L2调优提示: 在支持SMMU的ARM平台上(如RK3588、Jetson Orin),Videobuf2默认使用IOMMU分配缓冲区。如果遇到DMA映射失败,请检查设备树中 iommus 属性是否配置正确,或通过 dma_mask 确认设备支持的地址宽度。
IOMMU/SMMU地址翻译流程 DMA设备 (摄像头/网卡) IOVA 0x1000_0000 IOMMU/SMMU 页表查找 物理地址 0x4000_0000 IOMMU页表示例 IOVA 0x1000_0000 → 物理页 0x4000_0000 (连续) IOVA 0x1000_1000 → 物理页 0x5000_0000 (不连续) IOVA 0x1000_2000 → 物理页 0x3000_0000 (不连续)

3.3 DMA API:dma_alloc_coherent 与流式映射

Linux内核提供了一套DMA API,用于管理设备与内存之间的DMA缓冲区。Videobuf2在分配缓冲区时,底层依赖这些API。核心函数包括:

  • dma_alloc_coherent():分配物理连续且缓存一致性(coherent)的内存,返回CPU虚拟地址和DMA总线地址。
  • dma_map_single() / dma_unmap_single():将已有的内核缓冲区映射为DMA可访问区域,适用于流式(streaming)DMA。
  • dma_alloc_attrs():带属性的分配接口,可指定DMA_ATTR_NO_KERNEL_MAPPING等标志。
dma_alloc_coherent 特点:
  • 分配的内存是物理连续的(除非IOMMU介入)。
  • 保证缓存一致性:CPU和DMA设备看到的同一份数据,无需手动刷新缓存。
  • 返回两个地址:cpu_addr(CPU虚拟地址)和 dma_handle(DMA总线地址)。
  • 适用于长期、频繁的DMA传输,如视频帧缓冲区。

典型使用模式:

#include <linux/dma-mapping.h>

struct device *dev = get_device_from_v4l2();
size_t size = SZ_4M;  // 4MB缓冲区
dma_addr_t dma_handle;
void *cpu_addr;

// 分配一致性DMA缓冲区
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;
}

// 使用缓冲区:CPU通过cpu_addr访问,DMA设备通过dma_handle访问
memset(cpu_addr, 0, size);
// ... 启动DMA传输 ...

// 释放缓冲区
dma_free_coherent(dev, size, cpu_addr, dma_handle);

3.4 dma_alloc_coherent 与 Videobuf2 的集成

在Videobuf2框架中,dma_alloc_coherent 是 vb2_dma_contig 内存分配器的核心实现。当V4L2驱动选择 V4L2_MEMORY_DMABUF 或 V4L2_MEMORY_MMAP 且底层使用DMA连续分配器时,分配流程如下:

  1. vb2_core_reqbufs() 调用驱动注册的 ops->alloc()。
  2. vb2_dma_contig_alloc() 内部调用 dma_alloc_coherent() 分配物理连续内存。
  3. 分配的 dma_handle 被记录在 vb2_dma_contig_buf 结构中。
  4. 用户空间通过 mmap 映射 cpu_addr,或通过 DMABUF 导出 dma_buf 给其他设备共享。
Videobuf2内存分配器 底层DMA API 物理连续性 适用场景
vb2_dma_contig dma_alloc_coherent 连续(或IOMMU虚拟连续) 大多数V4L2设备,如UVC、CSI摄像头
vb2_vmalloc vmalloc 不连续 模拟设备、测试、无DMA需求
vb2_dma_sg dma_map_sg 分散-聚集(SG) 支持SG DMA的高端设备
Videobuf2 dma_alloc_coherent 分配流程 用户空间 VIDIOC_REQBUFS Videobuf2核心 vb2_core_reqbufs DMA连续分配器 vb2_dma_contig_alloc dma_alloc _coherent 分配结果 cpu_addr: 0xffff800012345000 (CPU虚拟地址) dma_handle: 0x10000000 (DMA总线地址) 物理内存(连续) 页0: 0x1000_0000 页1: 0x1000_1000 页2: 0x1000_2000 页3: 0x1000_3000

3.5 调优要点与常见陷阱

常见陷阱:
  • dma_mask 未设置: 如果设备 dma_mask 未正确设置,dma_alloc_coherent 可能返回 NULL 或分配在错误的地址范围。务必在驱动 probe 中调用 dma_set_mask_and_coherent()。
  • GFP标志选择: 在原子上下文(如中断处理)中不能使用 GFP_KERNEL,应使用 GFP_ATOMIC。Videobuf2 在 alloc() 回调中通常允许 GFP_KERNEL。
  • CMA区域耗尽: 当系统内存碎片严重时,CMA可能无法分配大块连续内存。可通过 cma=size 内核参数增加CMA区域,或启用IOMMU。
  • 缓存一致性问题: 虽然 dma_alloc_coherent 保证一致性,但如果驱动错误地使用 dma_map_single 映射同一区域,可能导致缓存不一致。

性能调优建议:

  • 优先使用 dma_alloc_coherent 而非 dma_map_single + 手动缓存刷新,减少 dma_sync_single_for_cpu/device 开销。
  • 对于多缓冲区场景(如V4L2的 VIDIOC_REQBUFS 请求多个缓冲区),一次性分配大块连续内存再切分,比多次小分配效率更高。
  • 在支持IOMMU的平台上,考虑使用 dma_alloc_attrs() 配合 DMA_ATTR_NO_KERNEL_MAPPING 减少内核虚拟地址空间占用。