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虚拟地址,解除物理连续性约束。
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 确认设备支持的地址宽度。
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连续分配器时,分配流程如下:
- vb2_core_reqbufs() 调用驱动注册的
ops->alloc()。 - vb2_dma_contig_alloc() 内部调用
dma_alloc_coherent()分配物理连续内存。 - 分配的
dma_handle被记录在vb2_dma_contig_buf结构中。 - 用户空间通过
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的高端设备 |
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减少内核虚拟地址空间占用。