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。

核心要点: V4L2 的 DMA-BUF 接口遵循“显式导入/导出”模型。应用程序必须明确告知 V4L2 驱动使用 DMA-BUF 模式,并通过文件描述符(FD)传递缓冲区所有权。

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 文件描述符
注意事项: 导出的 DMA-BUF 文件描述符在 close() 后,底层缓冲区引用计数减一。如果 V4L2 驱动仍然持有该缓冲区的内部引用,缓冲区内存不会立即释放。应用程序必须确保在关闭 FD 前,所有导入方已完成访问。

4.3 VIDIOC_EXPBUF:导出 IOCTL 调用流程

VIDIOC_EXPBUF 是 V4L2 设备节点上执行的 IOCTL 命令,用于将 V4L2 内部缓冲区转换为 DMA-BUF 文件描述符。调用该 IOCTL 前,缓冲区必须已经通过 VIDIOC_REQBUFS 分配,并且通常处于 已分配但未入队 或 已出队(DQBUF) 状态。

典型调用步骤:

  1. 请求缓冲区: 使用 VIDIOC_REQBUFS 分配 N 个缓冲区,内存类型设为 V4L2_MEMORY_MMAP。
  2. 查询缓冲区: 使用 VIDIOC_QUERYBUF 获取每个缓冲区的物理信息(可选,用于 mmap)。
  3. 填充导出结构体: 设置 struct v4l2_exportbuffer 的 index、type、plane 字段。
  4. 调用 IOCTL: 执行 ioctl(fd, VIDIOC_EXPBUF, &expbuf)。
  5. 获取 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 导入路径。

V4L2 DMA-BUF 导入/导出架构图 V4L2 视频设备 VIDIOC_EXPBUF struct v4l2_exportbuffer index / type / plane → fd DMA-BUF 文件描述符 expbuf.fd DMA-BUF 共享内存 物理内存页 / 散列表 引用计数 + 同步机制 文件描述符传递所有权 导出 V4L2 视频设备 V4L2_MEMORY_DMABUF struct v4l2_buffer memory = DMABUF / fd 外部 DMA-BUF FD 来自 DRM / 其他 V4L2 导入 关键流程说明 1. 导出路径:V4L2 设备通过 VIDIOC_EXPBUF 将内部缓冲区转换为 DMA-BUF FD。 2. 导入路径:应用程序将外部 DMA-BUF FD 填入 v4l2_buffer.m.fd,并设置 memory = DMABUF。 3. 共享核心:DMA-BUF 作为内核对象,通过文件描述符在进程和设备间传递所有权。 4. 生命周期:导出方和导入方各自持有 FD,close() 操作递减引用计数,最后一个 close() 释放内存。

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 驱动收到 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 对象。