4. vb2 内存分配器接口:vb2_mem_ops 结构与内存类型枚举
Videobuf2 框架的核心设计理念之一是将缓冲区内存管理与视频流处理逻辑解耦。这一解耦通过 vb2_mem_ops 结构体实现,它为上层提供了统一的内存操作接口,而底层则根据不同的内存类型(如连续物理内存、分散/聚集内存、用户态指针、DMA-BUF)提供具体的实现。
核心概念:
vb2_mem_ops 是 V4L2 驱动开发者与内存分配后端之间的“契约”。驱动只需调用这些回调,无需关心内存是来自 CMA、ION 还是用户空间。
4.1 内存类型枚举:v4l2_memory
V4L2 通过 v4l2_memory 枚举定义了四种标准的内存模型,驱动和应用程序通过 VIDIOC_REQBUFS 和 VIDIOC_QUERYBUF 交互时使用该枚举。
| 枚举值 | 宏定义 | 含义 | 典型场景 |
|---|---|---|---|
V4L2_MEMORY_MMAP |
1 | 内核分配物理内存,通过 mmap 映射到用户空间 |
嵌入式摄像头、USB 摄像头(默认模式) |
V4L2_MEMORY_USERPTR |
2 | 用户空间预先分配内存,通过指针传递给内核 | 用户态专用内存池、零拷贝场景(受限) |
V4L2_MEMORY_OVERLAY |
3 | 直接硬件覆盖(已废弃,仅用于旧驱动) | 传统模拟采集卡 |
V4L2_MEMORY_DMABUF |
4 | 通过 DMA-BUF 文件描述符共享内存 | GPU/VPU 流水线、跨设备零拷贝 |
调优要点: 在嵌入式系统中,
V4L2_MEMORY_MMAP 通常配合 CMA 分配器使用,能保证大块连续物理内存;V4L2_MEMORY_DMABUF 是高性能异构计算的首选,但需要硬件支持。
4.2 vb2_mem_ops 结构体解析
vb2_mem_ops 定义在 include/media/videobuf2-memops.h 中,是每个内存分配器后端必须实现的回调集合。以下是核心回调函数及其职责:
struct vb2_mem_ops {
// 分配内存
void *(*alloc)(struct device *dev, unsigned long attrs,
unsigned long size, enum dma_data_direction dma_dir,
gfp_t gfp_flags);
// 释放内存
void (*put)(void *buf_priv);
// 获取用户空间指针对应的 DMA 地址
int (*get_userptr)(struct device *dev, unsigned long vaddr,
unsigned long size, enum dma_data_direction dma_dir,
struct vb2_plane *planes);
// 释放用户指针映射
void (*put_userptr)(void *buf_priv);
// 附加 DMA-BUF 文件描述符
int (*attach_dmabuf)(struct device *dev, struct dma_buf *dbuf,
unsigned long size, enum dma_data_direction dma_dir,
struct vb2_plane *planes);
// 分离 DMA-BUF
void (*detach_dmabuf)(void *buf_priv);
// 映射 DMA-BUF 到设备地址空间
int (*map_dmabuf)(void *buf_priv);
// 取消映射 DMA-BUF
void (*unmap_dmabuf)(void *buf_priv);
// 获取 DMA 地址和长度(用于 MMAP 模式)
int (*get_dma_addr)(struct device *dev, void *buf_priv,
unsigned long *dma_addr, unsigned long *size);
// 初始化分配器(可选)
int (*init)(struct vb2_queue *q);
// 清理分配器(可选)
void (*cleanup)(struct vb2_queue *q);
};
4.3 核心回调详解
4.3.1 alloc / put:内存的分配与释放
- alloc:分配
size字节的 DMA 缓冲区,返回一个私有句柄buf_priv。参数attrs携带 DMA 属性(如DMA_ATTR_NO_KERNEL_MAPPING),dma_dir指明数据方向(DMA_FROM_DEVICE/DMA_TO_DEVICE)。 - put:释放由
alloc分配的缓冲区。调用后buf_priv不再有效。
4.3.2 get_userptr / put_userptr:用户指针模式
- get_userptr:接收用户空间虚拟地址
vaddr,将其物理页锁定并映射到设备 DMA 地址空间,填充vb2_plane结构。 - put_userptr:解除锁定和映射,释放 DMA 地址。
性能陷阱:
get_userptr 涉及页表遍历和页锁定,频繁调用会导致显著延迟。建议在流启动前一次性完成所有缓冲区的注册。
4.3.3 attach_dmabuf / detach_dmabuf / map_dmabuf / unmap_dmabuf:DMA-BUF 模式
- attach_dmabuf:将外部传入的
struct dma_buf *附加到当前设备,验证大小和方向,返回私有句柄。 - map_dmabuf:将附加的 DMA-BUF 映射到设备 I/O 地址空间,返回映射后的地址。
- unmap_dmabuf:解除设备映射。
- detach_dmabuf:分离 DMA-BUF,释放附加时分配的资源。
4.4 内存分配器架构图
下图展示了 vb2_mem_ops 如何将 V4L2 核心与三种主流内存后端连接起来:
4.5 内存类型与分配器后端对应关系
不同的 v4l2_memory 枚举值会触发内核选择不同的 vb2_mem_ops 实现。Linux 内核内置了三个标准后端:
| 内存类型 | 后端分配器 | 内核模块 | 物理连续性 | 跨设备共享 |
|---|---|---|---|---|
V4L2_MEMORY_MMAP |
CMA 分配器 | videobuf2-dma-contig |
连续 | 不支持 |
V4L2_MEMORY_MMAP |
SG 分配器 | videobuf2-dma-sg |
非连续 | 不支持 |
V4L2_MEMORY_USERPTR |
SG 分配器 | videobuf2-dma-sg |
非连续 | 不支持 |
V4L2_MEMORY_DMABUF |
DMA-BUF 分配器 | videobuf2-dma-contig 或 videobuf2-dma-sg |
取决于源 | 支持 |
调优决策: 对于需要硬件 Scatter-Gather 的 SoC,应选择 SG 分配器;对于需要硬件连续地址的摄像头控制器,必须使用 CMA 分配器。DMA-BUF 模式不关心底层连续性,只关心文件描述符的有效性。
4.6 回调调用流程(以 MMAP 模式为例)
- REQBUFS 阶段: 驱动调用
vb2_core_reqbufs,根据内存类型选择后端,然后调用ops->alloc为每个缓冲区分配内存。 - QUERYBUF 阶段: 调用
ops->get_dma_addr获取 DMA 地址和大小,填充v4l2_buffer结构。 - QBUF / DQBUF 阶段: 硬件通过 DMA 地址直接访问缓冲区,无需调用内存操作回调。
- STREAMOFF / REQBUFS(0) 阶段: 调用
ops->put释放所有缓冲区内存。
4.7 自定义分配器注册
驱动开发者可以通过 vb2_queue 的 mem_ops 字段注册自定义分配器:
struct vb2_queue q;
q.mem_ops = &my_custom_mem_ops; // 替换默认分配器
q.memory = V4L2_MEMORY_MMAP; // 指定内存类型
vb2_queue_init(&q);
高级调优: 对于具有专用 SRAM 或硬件内存管理单元(IOMMU)的平台,实现自定义
vb2_mem_ops 可以绕过标准 CMA/SG 分配器,直接管理专用内存池,减少碎片并提高分配确定性。