4. videobuf2内存模型

videobuf2(vb2)是 V4L2 框架中负责缓冲区管理的核心层,它向上对接应用层的缓冲区请求,向下对接驱动层的硬件 DMA 操作。理解 vb2 的内存模型,是编写高效、稳定摄像头驱动的关键。本章将深入剖析 vb2 支持的四种主要内存模型,并重点讲解与硬件 DMA 协同工作的两种核心模式:dma-contig 和 dma-sg。

核心概念: 内存模型决定了视频数据在物理内存中的布局方式,这直接影响了 DMA 控制器如何访问数据。选择合适的内存模型,需要在 CPU 访问效率、DMA 传输效率、内存碎片化程度和硬件约束之间做出权衡。

4.1 四种内存模型概览

vb2 框架抽象了四种标准的内存模型,每种模型对应不同的内存分配和映射策略。下表从分配器、物理连续性、适用场景等维度进行了对比。

内存模型 分配器 物理连续性 CPU 访问方式 典型硬件场景
连续内存 (Contiguous) vb2_dma_contig_memops 物理连续 直接映射 / DMA 映射 大多数嵌入式摄像头、USB 摄像头
分散内存 (Scatter/Gather) vb2_dma_sg_memops 物理不连续 通过 SG 表进行 DMA 映射 高性能 PCIe 采集卡、支持 SG 的 DMA 引擎
用户态指针 (User Pointer) vb2_vmalloc_memops / 自定义 不保证 直接访问用户空间地址 软件编解码、低延迟预览、零拷贝需求
DMA 连续 (dma-contig) dma_alloc_coherent 物理连续 一致性 DMA 映射 大多数 SoC 内置摄像头接口(CSI/ISP)
DMA 分散 (dma-sg) dma_map_sg 物理不连续 流式 DMA 映射 支持 SG 模式的 DMA 控制器、高性能设备
注意: 在 Linux 内核中,dma-contig 和 dma-sg 是实际驱动开发中最常用的两种模型。它们直接封装了 DMA API,与硬件 DMA 引擎的交互最为紧密。用户态指针模型通常用于软件处理路径,而纯连续内存模型在大多数现代 SoC 上已被 dma-contig 取代。

4.2 连续内存模型 (dma-contig)

dma-contig 模型分配的是物理上连续的内存块,并通过 dma_alloc_coherent 或 dma_alloc_attrs 接口获得。这种模型最大的优点是:DMA 引擎无需处理复杂的地址转换表,只需提供一个起始地址和传输长度即可开始传输。

4.2.1 工作原理

  1. 分配: 驱动调用 vb2_dma_contig_memops->alloc(),内部调用 dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL)。该函数返回一个 CPU 可访问的虚拟地址 vaddr,并通过 dma_handle 返回 DMA 总线地址。
  2. 映射: 由于内存是物理连续的,且 dma_alloc_coherent 已经保证了 CPU 和 DMA 视角的一致性,驱动可以直接将 dma_handle 编程到 DMA 控制器的源/目的地址寄存器中。
  3. 队列: 驱动将缓冲区加入硬件队列时,只需传递 dma_handle 和 size。

4.2.2 适用场景与限制

  • 优点: DMA 编程简单,硬件要求低,适合大多数嵌入式 SoC 内置的摄像头接口。
  • 缺点: 在系统长时间运行后,分配大块物理连续内存可能失败(内存碎片化)。分配大小受限于系统 CMA 区域大小。
驱动开发陷阱: 使用 dma-contig 时,务必检查 dma_alloc_coherent 的返回值。如果返回 NULL,表示分配失败,驱动应优雅地返回错误码 -ENOMEM,而不是直接使用空指针。

4.2.3 dma-contig 内存模型 SVG 图

dma-contig 内存模型:物理连续,单一 DMA 地址 CPU 虚拟地址空间 (vaddr) 物理连续内存块 起始物理地址: 0x4A00_0000 DMA 总线地址 (dma_handle) DMA 引擎直接访问 总线地址: 0x8C00_0000 一对一映射 特点:CPU 和 DMA 看到的是同一块物理内存,无需页表转换,延迟最低。

4.3 分散内存模型 (dma-sg)

dma-sg 模型允许缓冲区由多个物理上不连续的页面组成。DMA 引擎通过一个“分散/聚集表”(Scatter-Gather List)来访问这些分散的内存块。这种模型极大地缓解了内存碎片化问题,并允许分配非常大的缓冲区。

4.3.1 工作原理

  1. 分配: 驱动调用 vb2_dma_sg_memops->alloc(),内部分配一组页面(通常通过 alloc_pages 或从 CMA 区域获取),并构建一个 struct sg_table。
  2. 映射: 驱动调用 dma_map_sg(dev, sg_table->sgl, sg_table->nents, direction)。这个函数为每个 SG 条目填充 DMA 总线地址和长度,并返回有效的 DMA 条目数。
  3. 队列: 驱动将 sg_table 的 DMA 地址和长度列表编程到支持 SG 模式的 DMA 控制器中。DMA 控制器会自动遍历 SG 表,完成整个缓冲区的传输。

4.3.2 适用场景与限制

  • 优点: 内存分配成功率更高,支持大缓冲区,适合高性能设备。
  • 缺点: 需要 DMA 控制器硬件支持 SG 模式;驱动编程更复杂;DMA 映射和取消映射有开销。
性能关键点: dma_map_sg 可能会合并物理上连续的 SG 条目,因此驱动必须使用 dma_map_sg 返回的 nents(而非原始 nents)来编程 DMA 控制器。忽略这一点会导致 DMA 传输错误。

4.3.3 dma-sg 内存模型 SVG 图

dma-sg 内存模型:物理分散,通过 SG 表访问 Scatter-Gather 表 条目 0: 地址=0x8C00, 长度=4KB 条目 1: 地址=0x9A00, 长度=4KB 条目 2: 地址=0x7B00, 长度=4KB 条目 3: 地址=0xAD00, 长度=4KB 物理页 0 物理页 1 物理页 2 物理页 3 DMA 引擎 读取 SG 表 依次访问每个物理页 特点:物理页不连续,DMA 通过 SG 表完成整个缓冲区的传输,有效避免内存碎片。

4.4 用户态指针模型 (User Pointer)

用户态指针模型允许应用程序直接提供用户空间的内存地址作为视频缓冲区。vb2 框架通过 get_user_pages 将这些用户页锁定在内存中,并获取其物理页,然后进行 DMA 映射。

  • 优点: 实现真正的零拷贝,应用程序可以直接处理视频数据,无需额外的内核缓冲区拷贝。
  • 缺点: 内存锁定开销大;用户提供的地址必须满足 DMA 对齐要求;安全性较低(用户可能提供无效地址)。
  • 典型用例: 软件编码器、图像处理库(如 OpenCV)直接操作摄像头数据。

4.5 如何选择:dma-contig vs dma-sg

在驱动开发中,选择哪种模型通常取决于硬件能力。下表提供了一个决策参考:

硬件特性 推荐模型 原因
DMA 控制器只支持线性地址 dma-contig 硬件无法处理 SG 表,必须使用物理连续内存。
DMA 控制器支持 SG 模式 dma-sg 更灵活,内存分配成功率更高,适合大分辨率或高帧率。
设备是 SoC 内置 ISP/CSI dma-contig 通常 ISP 硬件设计为连续内存,且 SoC 有 CMA 区域保证分配。
设备是 PCIe 采集卡 dma-sg PCIe 设备通常内置 SG DMA 引擎,且系统内存充足。
需要分配超大缓冲区 (> 32MB) dma-sg 连续内存分配大块极易失败,SG 模型可以聚合多个页面。
最佳实践: 如果硬件同时支持两种模式,优先选择 dma-sg。它提供了更好的内存伸缩性和抗碎片能力。在驱动 probe 阶段,可以通过 dma_set_mask_and_coherent 设置 DMA 掩码,然后尝试分配一个小的 SG 缓冲区来探测硬件是否支持。

4.6 关键代码模式:初始化内存操作符

在 vb2 驱动的 queue_setup 回调中,需要指定使用的内存模型。这是驱动与 vb2 框架约定的第一步。

// 示例:在驱动中使用 dma-contig 模型
static int my_v4l2_queue_setup(struct vb2_queue *vq,
                                unsigned int *nbuffers,
                                unsigned int *nplanes,
                                unsigned int sizes[],
                                struct device *alloc_devs[])
{
    // 指定分配设备(用于 DMA API)
    alloc_devs[0] = my_dev->dev;
    // 指定缓冲区数量
    *nbuffers = 4;
    // 指定单平面大小(例如 1920x1080 YUYV)
    sizes[0] = 1920 * 1080 * 2;
    *nplanes = 1;
    return 0;
}

// 在初始化 vb2_queue 时设置内存操作符
static struct vb2_queue my_video_q = {
    .type           = V4L2_BUF_TYPE_VIDEO_CAPTURE,
    .io_modes       = VB2_MMAP | VB2_DMABUF,
    .buf_struct_size = sizeof(struct my_buffer),
    .ops            = &my_v4l2_ops,
    .mem_ops        = &vb2_dma_contig_memops, // <-- 选择 dma-contig
    .drv_priv       = &my_dev,
    .timestamp_flags = V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC,
};

如果选择 dma-sg 模型,只需将 .mem_ops 替换为 &vb2_dma_sg_memops。vb2 框架会自动处理后续的分配、映射和队列管理。

重要提醒: 不要混淆 vb2_dma_contig_memops 和 vb2_dma_sg_memops。一旦选定,驱动中所有与 DMA 地址相关的操作(如编程硬件寄存器)都必须遵循对应模型的地址获取方式。对于 dma-contig,使用 vb2_dma_contig_plane_dma_addr();对于 dma-sg,使用 vb2_dma_sg_plane_desc() 获取 SG 表。