4. V4L2 DMA-BUF 接口:V4L2_MEMORY_DMABUF、VIDIOC_EXPBUF、struct v4l2_exportbuffer
V4L2 在 Linux 3.x 时代正式引入了对 DMA-BUF 的原生支持,使得 V4L2 设备能够直接导入由 DRM 或其他子系统导出的 DMA-BUF,也能将自己的缓冲区导出为 DMA-BUF 供其他驱动使用。本章聚焦于 V4L2 侧与 DMA-BUF 交互的三个核心要素:内存类型标识 V4L2_MEMORY_DMABUF、导出 IOCTL VIDIOC_EXPBUF 以及描述导出操作的数据结构 struct v4l2_exportbuffer。
4.1 V4L2_MEMORY_DMABUF:声明使用 DMA-BUF 内存模型
在 V4L2 框架中,enum v4l2_memory 定义了应用程序与驱动之间交换缓冲区数据的内存模型。当应用程序计划使用外部 DMA-BUF 作为视频缓冲区的存储后端时,必须在调用 VIDIOC_REQBUFS 和 VIDIOC_QBUF 时,将 struct v4l2_requestbuffers 或 struct v4l2_buffer 中的 memory 字段设置为 V4L2_MEMORY_DMABUF。
| 内存模型枚举值 | 数值 | 说明 |
|---|---|---|
V4L2_MEMORY_MMAP |
1 | 传统 mmap 映射,驱动分配物理连续或非连续内存 |
V4L2_MEMORY_USERPTR |
2 | 用户空间指针,应用程序提供虚拟地址 |
V4L2_MEMORY_OVERLAY |
3 | 视频覆盖模式,用于硬件叠加层 |
V4L2_MEMORY_DMABUF |
4 | DMA-BUF 文件描述符模式,用于跨设备共享 |
V4L2_MEMORY_DMABUF 时,VIDIOC_REQBUFS 中的 count 字段通常设置为 0,因为缓冲区由外部提供,V4L2 驱动无需自行分配内存。但某些驱动要求非零值以初始化内部队列深度,请查阅具体驱动文档。
4.2 struct v4l2_exportbuffer:导出缓冲区的描述符
当 V4L2 设备内部已经分配了缓冲区(例如通过 V4L2_MEMORY_MMAP 模式),应用程序可以通过 VIDIOC_EXPBUF 将该缓冲区导出为一个 DMA-BUF 文件描述符。导出操作的核心数据结构是 struct v4l2_exportbuffer,定义在 <linux/videodev2.h> 中。
struct v4l2_exportbuffer {
__u32 index; /* 缓冲区索引,从 0 开始 */
__u32 type; /* 缓冲区类型,如 V4L2_BUF_TYPE_VIDEO_CAPTURE */
__u32 plane; /* 多平面模式下指定平面索引,单平面设为 0 */
__u32 flags; /* 保留位,当前必须设为 0 */
__s32 fd; /* 返回的 DMA-BUF 文件描述符 */
__u32 reserved[11]; /* 保留字段,填充 0 */
};
| 字段 | 方向 | 描述 |
|---|---|---|
index |
输入 | 要导出的缓冲区在队列中的索引,与 VIDIOC_QUERYBUF 返回的 index 一致 |
type |
输入 | 缓冲区类型,例如 V4L2_BUF_TYPE_VIDEO_CAPTURE 或 V4L2_BUF_TYPE_VIDEO_OUTPUT |
plane |
输入 | 对于多平面格式(如 NV12),指定导出第几个平面;单平面格式填 0 |
flags |
输入 | 扩展标志,当前内核版本必须设为 0 |
fd |
输出 | 调用成功后,内核在此返回一个有效的 DMA-BUF 文件描述符 |
close() 后,底层缓冲区引用计数减一。如果 V4L2 驱动仍然持有该缓冲区的内部引用,缓冲区内存不会立即释放。应用程序必须确保在关闭 FD 前,所有导入方已完成访问。
4.3 VIDIOC_EXPBUF:导出 IOCTL 调用流程
VIDIOC_EXPBUF 是 V4L2 设备节点上执行的 IOCTL 命令,用于将 V4L2 内部缓冲区转换为 DMA-BUF 文件描述符。调用该 IOCTL 前,缓冲区必须已经通过 VIDIOC_REQBUFS 分配,并且通常处于 已分配但未入队 或 已出队(DQBUF) 状态。
典型调用步骤:
- 请求缓冲区: 使用
VIDIOC_REQBUFS分配 N 个缓冲区,内存类型设为V4L2_MEMORY_MMAP。 - 查询缓冲区: 使用
VIDIOC_QUERYBUF获取每个缓冲区的物理信息(可选,用于 mmap)。 - 填充导出结构体: 设置
struct v4l2_exportbuffer的index、type、plane字段。 - 调用 IOCTL: 执行
ioctl(fd, VIDIOC_EXPBUF, &expbuf)。 - 获取 FD: 成功后,从
expbuf.fd读取 DMA-BUF 文件描述符。
int export_v4l2_buffer(int v4l2_fd, int index, int plane) {
struct v4l2_exportbuffer expbuf = {0};
expbuf.index = index;
expbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
expbuf.plane = plane;
if (ioctl(v4l2_fd, VIDIOC_EXPBUF, &expbuf) == -1) {
perror("VIDIOC_EXPBUF failed");
return -1;
}
printf("Exported DMA-BUF fd: %d\n", expbuf.fd);
return expbuf.fd; // 返回文件描述符,调用者负责 close()
}
4.4 V4L2 DMA-BUF 导入与导出架构
下图展示了 V4L2 设备与外部组件(如 DRM GPU)通过 DMA-BUF 进行双向交互的完整架构。左侧为 V4L2 导出路径,右侧为 V4L2 导入路径。
4.5 导入模式:在 QBUF 中传递 DMA-BUF FD
当 V4L2 设备作为 DMA-BUF 的消费者(导入方)时,应用程序需要在 VIDIOC_QBUF 中指定每个缓冲区的 DMA-BUF 文件描述符。对于单平面格式,直接设置 struct v4l2_buffer 的 m.fd 字段;对于多平面格式,则设置 struct v4l2_plane 的 m.fd 字段。
// 单平面导入示例
struct v4l2_buffer buf = {0};
buf.index = 0;
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_DMABUF;
buf.m.fd = dma_buf_fd_from_drm; // 从 DRM 导入的 FD
if (ioctl(v4l2_fd, VIDIOC_QBUF, &buf) == -1) {
perror("VIDIOC_QBUF with DMABUF failed");
}
// 多平面导入示例(NV12 格式,2 planes)
struct v4l2_buffer buf = {0};
struct v4l2_plane planes[2];
buf.index = 0;
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
buf.memory = V4L2_MEMORY_DMABUF;
buf.m.planes = planes;
buf.length = 2;
planes[0].m.fd = dma_buf_fd_plane_y; // Y 平面 FD
planes[1].m.fd = dma_buf_fd_plane_uv; // UV 平面 FD
if (ioctl(v4l2_fd, VIDIOC_QBUF, &buf) == -1) {
perror("VIDIOC_QBUF multi-plane DMABUF failed");
}
V4L2_MEMORY_DMABUF 的 QBUF 请求时,它会调用 dma_buf_get() 增加引用计数,并通过 dma_buf_attach() 和 dma_buf_map_attachment() 获取设备可访问的 S/G 表。驱动在 DQBUF 返回缓冲区后,应调用 dma_buf_put() 释放引用。
4.6 接口对比:导出 vs 导入
| 操作方向 | IOCTL / 字段 | 数据结构 | 内存模型 | 典型场景 |
|---|---|---|---|---|
| 导出(V4L2 → 外部) | VIDIOC_EXPBUF |
struct v4l2_exportbuffer |
源缓冲区通常为 V4L2_MEMORY_MMAP |
摄像头采集 → GPU 渲染 |
| 导入(外部 → V4L2) | VIDIOC_QBUF |
struct v4l2_buffer / struct v4l2_plane |
V4L2_MEMORY_DMABUF |
GPU 生成帧 → 编码器输入 |
4.7 常见错误与调试
- EINVAL(无效参数): 检查
type是否与设备实际支持的缓冲区类型匹配;检查plane是否超出格式的平面数。 - EPERM(操作不允许): 某些 V4L2 驱动不支持导出操作,或设备节点缺少必要的文件权限。
- ENOMEM(内存不足): 导出时内核无法分配新的 DMA-BUF 对象或关联的 sg_table。
- EBUSY(设备忙): 缓冲区当前正在硬件队列中处理,无法导出。确保在 DQBUF 后或 REQBUFS 后立即导出。
strace -e ioctl 捕获 VIDIOC_EXPBUF 的调用参数和返回值。通过 ls -l /proc/<pid>/fd/ 查看导出的 DMA-BUF 文件描述符指向的 anon_inode:dma-buf 对象。