Contents

V4l2loopback实现笔记(五):VB2 缓冲区管理

本模块演示 videobuf2 (VB2) 框架的使用,这是 Linux 内核中处理视频流的标准方法。

实现的功能

  • VB2 队列初始化 (vb2_queue_init)
  • 缓冲区操作回调 (vb2_ops)
  • 缓冲区请求和查询 (REQBUFS, QUERYBUF)
  • 缓冲区入队和出队 (QBUF, DQBUF)
  • 流控制 (STREAMON, STREAMOFF)
  • 多种 I/O 模式支持 (MMAP, USERPTR, READ/WRITE)

学习要点

  1. 理解 VB2 框架的设计目标和优势
  2. 理解 vb2_ops 各回调的调用时机
  3. 理解缓冲区状态机
  4. 理解为什么 stop_streaming 必须返回所有缓冲区
  5. 理解 VB2 与自定义缓冲区管理的权衡

代码解析

结构体定义

1. struct vb2fw_format

struct vb2fw_format {
    u32 fourcc;           /* V4L2 FourCC 代码 */
    u32 depth;            /* 每像素位数 */
    const char *name;     /* 格式名称 */
};

static const struct vb2fw_format vb2fw_formats[] = {
    {
        .fourcc = V4L2_PIX_FMT_YUYV,
        .depth  = 16,
        .name   = "YUYV 4:2:2",
    },
    {
        .fourcc = V4L2_PIX_FMT_UYVY,
        .depth  = 16,
        .name   = "UYVY 4:2:2",
    },
    {
        .fourcc = V4L2_PIX_FMT_RGB24,
        .depth  = 24,
        .name   = "RGB24",
    },
    {
        .fourcc = V4L2_PIX_FMT_BGR24,
        .depth  = 24,
        .name   = "BGR24",
    },
    {
        .fourcc = V4L2_PIX_FMT_YUV420,
        .depth  = 12,
        .name   = "YUV 4:2:0",
    },
    {
        .fourcc = V4L2_PIX_FMT_NV12,
        .depth  = 12,
        .name   = "NV12",
    },
};

将 V4L2 标准的 fourcc 码(如 V4L2_PIX_FMT_YUYV)与驱动计算所需的参数(如 depth 像素深度)关联起来。当用户调用 S_FMT 设置分辨率时,你需要计算 sizeimage = width * height * (depth / 8)。这个结构体让你能通过简单的循环找到当前格式对应的计算权重。

2. struct vb2fw_buffer 单个缓冲区

struct vb2fw_buffer {
    struct vb2_v4l2_buffer vb;    /* 必须是第一个成员 */
    struct list_head list;         /* 用于队列管理 */
};

这是 vb2 框架要求的典型模式:将内核标准结构包装在私有结构中。

  • struct vb2_v4l2_buffer vb:这是 VB2 框架定义的缓冲区结构, 是 videobuf2-core 用来管理缓冲区状态(QUEUED, DONE 等)、时间戳、帧序号的载体,必须作为第一个成员,这样内核在处理 vb2_buffer 指针时,通过偏移量计算就能轻松找回私有包装结构,以确保内存布局正确。

  • struct list_head list:用于将缓冲区链接到驱动的私有缓冲区列表中,当缓冲区进入驱动(buf_queue)但尚未被硬件/逻辑处理时,把这个缓冲区挂载到 vb2fw_device 的 buf_list 链表中排队,方便管理和遍历。

3. struct vb2fw_device 设备私有数据

这是整个驱动的核心上下文,它维护了设备从启动到关闭的所有状态。

struct vb2fw_device {
    /* V4L2 核心结构 */
    struct v4l2_device v4l2_dev;
    struct video_device vdev;

    /* videobuf2 队列 */
    struct vb2_queue vb2_queue;

    /* 同步原语 */
    struct mutex lock;            /* 保护设备状态 */
    spinlock_t qlock;             /* 保护缓冲区队列 */

    /* 当前格式 */
    struct v4l2_pix_format pix_format;
    const struct vb2fw_format *current_fmt;

    /* 缓冲区管理 */
    struct list_head buf_list;    /* 已入队的缓冲区列表 */
    unsigned int buf_count;       /* 当前队列中的缓冲区数量 */
    unsigned int sequence;        /* 帧序号计数器 */

    /* 流状态 */
    bool streaming;
};
  • struct v4l2_device v4l2_devstruct video_device vdev:这是 V4L2 框架的核心结构,分别表示 V4L2 设备和视频设备节点。v4l2_dev是整个 V4L2 实例的引用计数和管理根节点,vdev代表 /dev/videoX 设备文件。它把 fops(如 open, read)和 ioctl_ops(如 S_FMT)绑定到系统。

  • struct vb2_queue vb2_queue:这是 VB2 框架的核心数据结构,管理缓冲区队列。它包含了缓冲区的分配、状态管理、I/O 模式(MMAP/USERPTR)以及指向回调函数(vb2_ops)的指针等信息。通过初始化这个结构体,驱动可以利用 VB2 提供的标准化缓冲区管理功能。

  • struct mutex lock: 保护设备状态的互斥锁,防止多个线程同时修改设备状态,确保线程安全。

  • spinlock_t qlock:保护缓冲区队列的自旋锁,适用于在中断上下文中使用,防止缓冲区队列被并发修改。

  • 互斥锁和自旋锁的区别是:互斥锁适用于可能会阻塞的上下文,而自旋锁适用于不能阻塞的上下文(如中断处理程序)。在驱动中,通常使用互斥锁保护较长时间的操作,而使用自旋锁保护短时间的临界区。

  • struct v4l2_pix_format pix_format:存储当前的视频格式信息,包括分辨率、像素格式等。

  • const struct vb2fw_format *current_fmt:指向当前使用的格式定义,方便在计算缓冲区大小时使用。

  • struct list_head buf_list:维护已入队但未处理的缓冲区列表,方便驱动管理和调度缓冲区。其中list_head是 Linux 内核提供的通用双向循环链表实现,用于高效地管理动态数据结构。

    struct list_head {
        struct list_head *next, *prev;
    };
  • unsigned int buf_count:记录当前队列中的缓冲区数量,方便统计和管理。

  • unsigned int sequence:帧序号计数器,用于为每个缓冲区分配唯一的序号,便于跟踪和调试。

  • bool streaming:标志当前设备是否处于流媒体传输状态,控制缓冲区的处理逻辑。

4. 关键数据结构关联

V4L2 驱动的内存管理与数据结构分为两类:

  • 通用框架层 (Kernel generic):vb2_queue 和 vb2_buffer。这是 Linux 内核定义的标准结构,所有 V4L2 驱动都一样。

  • 驱动私有层 (Driver private):vb2fw_device 和 vb2fw_buffer。这是驱动程序自定义的结构,用于存储特定设备的状态和缓冲区管理信息。

  1. vb2fw_buffervb2_buffer

vb2fw_buffer 包含了 vb2_buffer

+------------------------------------------+
struct vb2fw_buffer (驱动私有的大结构体)
+------------------------------------------+ <--- 起始地址 (buf)
| struct vb2_v4l2_buffer vb (必须在第一位)  |
|   +----------------------------------+   |
|   | struct vb2_buffer vb2_buf (核心)  |   | <--- 内核只认这个指针 (vb)
|   +----------------------------------+   |
| ... (flags, sequence, timestamp)     |
+------------------------------------------+
| struct list_head list (链表钩子)          | <--- 驱动用来排队的钩子
+------------------------------------------+
  1. vb2fw_devicevb2_queue

vb2fw_device是设备的私有数据结构,持有一个vb2_queue实例,一个视频设备通常有一个队列(用于 Capture 或 Output)。这个队列是设备结构体的一个成员变量。

  1. vb2_queuevb2_ops

vb2_queue 包含一个指向 vb2_ops 结构体的指针,这个结构体定义了一组回调函数,驱动程序必须实现这些函数以处理缓冲区的分配、初始化、准备、入队、启动和停止流等操作。

  1. vb2_buffervb2_ops

vb2_buffer 结构体表示单个缓冲区实例,当缓冲区状态发生变化(如准备好、入队等)时,VB2 框架会调用 vb2_ops 中对应的回调函数来处理这些状态变化。

  1. vb2_queuevb2fw_device

vb2_queue 通过 vb2_get_drv_priv 函数可以获取到包含它的 vb2fw_device 结构体指针,从而访问设备的私有数据和状态。

  1. vb2_queuevb2_buffer
struct vb2_queue {
    // ...
    struct vb2_buffer **bufs; /* 指向缓冲区指针数组的指针 */
    unsigned int num_buffers; /* 当前分配的缓冲区数量 */
    // ...
};
  • vb2_queue 管理着所有分配出来的 vb2_buffer,当你调用 REQBUFS(count=4) 时,vb2_queue 会在内核中创建 4 个 vb2_buffer(实际上是 4 个 vb2fw_buffer)。vb2_queue 内部有一个指针数组(bufs),存放着指向这 4 个缓冲区的指针。

  • vb2_buffer内部有一个指向所属 vb2_queue 的指针(vb->vb2_queue),这样每个缓冲区都知道自己属于哪个队列。vb->index 则表示这个缓冲区在队列中的索引位置(0 到 count-1)。

  • vb2_queue 里面存的是 struct vb2_buffer 的指针,但是分配和管理的是 struct vb2fw_buffer 的内存块。

    q->buf_struct_size = sizeof(struct vb2fw_buffer);
  • 回溯路径:vb2_buffer可以通过vb->vb2_queue找到所属的vb2_queue,而vb2_queue可以通过vb2_get_drv_priv(vq)找到包含它的vb2fw_device

VB2 操作回调函数 (vb2_ops)

VB2 框架通过一组回调函数与驱动交互,这些函数定义在 struct vb2_ops 结构体中。驱动需要实现这些回调,以便 VB2 框架能够正确管理缓冲区。

static const struct vb2_ops vb2fw_qops = {
    .queue_setup     = vb2fw_queue_setup,
    .buf_init        = vb2fw_buf_init,
    .buf_prepare     = vb2fw_buf_prepare,
    .buf_queue       = vb2fw_buf_queue,
    .start_streaming = vb2fw_start_streaming,
    .stop_streaming  = vb2fw_stop_streaming,
    .wait_prepare    = vb2_ops_wait_prepare,
    .wait_finish     = vb2_ops_wait_finish,
};

1. queue_setup 设置缓冲区队列

static int vb2fw_queue_setup(struct vb2_queue *vq,
                             unsigned int *nbuffers,
                             unsigned int *nplanes,
                             unsigned int sizes[],
                             struct device *alloc_devs[])
{
    printk(KERN_INFO "DEBUG: queue_setup called, type=%d, count=%d\n", vq->type, *nbuffers);
    struct vb2fw_device *dev = vb2_get_drv_priv(vq);
    unsigned int size;

    printk("queue_setup: nbuffers=%u\n", *nbuffers);

    /* 计算每个缓冲区需要的大小 */
    size = dev->pix_format.sizeimage;
    if (size == 0) {
        printk(KERN_ERR VB2FW_MODULE_NAME ": Invalid image size\n");
        return -EINVAL;
    }

    /* 确保缓冲区数量在合理范围内 */
    if (*nbuffers < VB2FW_MIN_BUFFERS)
        *nbuffers = VB2FW_MIN_BUFFERS;
    if (*nbuffers > VB2FW_MAX_BUFFERS)
        *nbuffers = VB2FW_MAX_BUFFERS;

    /*
     * 如果 *nplanes 非零,说明这是 REQBUFS 的重新协商
     * 需要检查请求的大小是否足够
     */
    if (*nplanes) {
        if (sizes[0] < size)
            return -EINVAL;
    } else {
        *nplanes = 1;
        sizes[0] = size;
    }

    printk("queue_setup: allocated %u buffers of size %u\n",
            *nbuffers, sizes[0]);

    return 0;
}

参数详解

  • struct vb2_queue *vq:指向当前缓冲区队列的指针,包含了队列的类型、状态等信息。此处的vq是在驱动初始化时赋值给视频设备的vb2_queue指针。
  • unsigned int *nbuffers:指向请求的缓冲区数量的指针,驱动可以修改这个值以调整实际分配的缓冲区数量。
  • unsigned int *nplanes:指向每个缓冲区的平面数量的指针,驱动可以设置为 1(单平面)或更多(多平面)。
  • unsigned int sizes[]:数组,包含每个平面的大小,驱动需要确保这些大小足够存储图像数据。
  • struct device *alloc_devs[]:用于缓冲区分配的设备数组,通常不需要修改。

功能说明

  • 计算每个缓冲区所需的大小(sizeimage)。
  • 检查并调整请求的缓冲区数量,确保在预定义的最小和最大范围内。
  • 填写平面数量和大小,确保缓冲区能够存储图像数据。

2. buf_init 初始化缓冲区

static int vb2fw_buf_init(struct vb2_buffer *vb)
{
    struct vb2fw_buffer *buf = container_of(vb, struct vb2fw_buffer, vb.vb2_buf);

    printk("buf_init: buffer %u\n", vb->index);

    /* 初始化列表节点 */
    INIT_LIST_HEAD(&buf->list);

    return 0;
}

参数详解

  • struct vb2_buffer *vb:指向当前缓冲区的指针,包含了缓冲区的状态、索引等信息。 功能说明
  • 使用 container_of 宏将通用的 vb2_buffer 指针转换为驱动私有的 vb2fw_buffer 指针。
  • 初始化缓冲区的列表节点,以便后续将其添加到驱动的缓冲区列表中。
    static inline void INIT_LIST_HEAD(struct list_head *list)
    {
        list->next = list;
        list->prev = list;
    }

3. buf_prepare 准备缓冲区

static int vb2fw_buf_prepare(struct vb2_buffer *vb)
{
    struct vb2fw_device *dev = vb2_get_drv_priv(vb->vb2_queue);
    unsigned long size;

    printk("buf_prepare: buffer %u\n", vb->index);

    size = dev->pix_format.sizeimage;

    /* 检查缓冲区大小是否足够 */
    if (vb2_plane_size(vb, 0) < size) {
        printk(KERN_ERR VB2FW_MODULE_NAME
               ": buffer too small (%lu < %lu)\n",
               vb2_plane_size(vb, 0), size);
        return -EINVAL;
    }

    /* 设置有效数据长度 */
    vb2_set_plane_payload(vb, 0, size);

    return 0;
}

主要功能

  • 检查内存缓冲区的大小是否足够存储图像数据。vb2_plane_size(vb, 0) 获取第一个平面的大小,是在queue_setup阶段,内核通过mmapuserptr分配的实际缓冲区大小。

  • 设置有效负载长度,告诉内核这个缓冲区实际存储了多少数据。vb2_set_plane_payload(vb, 0, size) 设置第一个平面的有效数据长度为计算得到的图像大小。

4. buf_queue 入队缓冲区

static void vb2fw_buf_queue(struct vb2_buffer *vb)
{
    struct vb2fw_device *dev = vb2_get_drv_priv(vb->vb2_queue);
    struct vb2fw_buffer *buf = container_of(vb, struct vb2fw_buffer, vb.vb2_buf);
    unsigned long flags;

    printk("buf_queue: buffer %u\n", vb->index);

    /* 将缓冲区添加到内部列表 */
    spin_lock_irqsave(&dev->qlock, flags);
    list_add_tail(&buf->list, &dev->buf_list);
    dev->buf_count++;
    spin_unlock_irqrestore(&dev->qlock, flags);
}

该函数响应用户空间发起的 VIDIOC_QBUF ioctl 调用。在 buf_prepare 校验通过后,vb2 核心层调用此函数,将这就绪状态的缓冲区(Ready Buffer)移交给驱动程序,以便后续进行 DMA 传输或数据填充。

功能说明:

  • 实现缓冲区所有权转移 (Ownership Transfer) 的核心回调接口。它标志着一个视频缓冲区从 VB2 核心层的管理域正式进入驱动程序的调度域。

  • list_add_tail 将当前缓冲区的 list_head 节点挂载到设备维护的双向链表 (buf_list) 的尾部。这确立了先进先出 (FIFO) 的处理策略,保证视频帧的时序一致性。

5. start_streaming 启动流传输

static int vb2fw_start_streaming(struct vb2_queue *vq, unsigned int count)
{
    struct vb2fw_device *dev = vb2_get_drv_priv(vq);

    printk("start_streaming: count=%u\n", count);

    dev->sequence = 0;
    dev->streaming = true;

    return 0;
}

该函数负责将驱动程序从 空闲状态 (Idle State) 切换至 流传输状态 (Streaming State)。它是数据传输流水线的“总开关”。在此函数返回成功 (0) 之前,驱动程序不得向硬件提交任何缓冲区;返回成功后,硬件或模拟逻辑应立即开始消耗缓冲区。

  • 序列号重置:V4L2 协议要求每一帧数据都必须携带一个单调递增的序列号。在每次流启动时,必须将计数器归零。这确保了用户空间应用程序能够通过检测序列号的不连续性来判断是否发生了丢帧 (Frame Drop)。

  • 流状态标志:将 streaming 标志设置为 true,表示设备当前处于流传输状态。这一标志在其他回调函数中用于判断是否允许缓冲区的处理和提交。

6. stop_streaming 停止流传输

static void vb2fw_stop_streaming(struct vb2_queue *vq)
{
    struct vb2fw_device *dev = vb2_get_drv_priv(vq);
    struct vb2fw_buffer *buf, *tmp;
    unsigned long flags;

    printk("stop_streaming\n");

    dev->streaming = false;

    /*
     * 重要:必须将所有缓冲区返回给 vb2 框架
     * 否则会导致内存泄漏和系统不稳定
     */
    spin_lock_irqsave(&dev->qlock, flags);
    list_for_each_entry_safe(buf, tmp, &dev->buf_list, list) {
        list_del(&buf->list);
        vb2_buffer_done(&buf->vb.vb2_buf, VB2_BUF_STATE_ERROR);
    }
    dev->buf_count = 0;
    spin_unlock_irqrestore(&dev->qlock, flags);
}

vb2fw_stop_streaming 是 vb2 框架操作结构体 (struct vb2_ops) 中的回调函数,用于响应用户空间的 VIDIOC_STREAMON (off) ioctl 调用。它负责执行流传输终止 (Stream Termination) 和资源回收 (Resource Reclamation)。

该函数强制终止视频数据流的传输。其核心职责包括两个方面:

  • 硬件层:停止 DMA 引擎,屏蔽中断,或停止驱动内部的数据生成逻辑。

  • 软件层:清理驱动程序内部队列中所有待处理 (Pending) 的缓冲区,并将它们的所有权归还给 vb2 核心层。

关键实现细节:

  • 流状态更新:将 streaming 标志设置为 false,表示设备不再处于流传输状态。这一标志在其他回调函数中用于判断是否允许缓冲区的处理和提交。

  • 缓冲区回收:使用 list_for_each_entry_safe 宏遍历驱动维护的缓冲区列表 (buf_list),对于每一个缓冲区,先将其从列表中删除 (list_del),然后调用 vb2_buffer_done 将缓冲区状态设置为错误状态 (VB2_BUF_STATE_ERROR),并将其返回给 vb2 框架。这样做确保了所有未处理的缓冲区都能被正确释放,防止内存泄漏和系统不稳定。

关键调用路径

申请缓冲区 (ioctl(fd, VIDIOC_REQBUFS, ...))

当用户应用程序(如 FFmpeg 或 v4l2-ctl)执行“申请缓冲区”操作(通常是调用 ioctl(fd, VIDIOC_REQBUFS, …))时,内核调用是一个从 V4L2 接口层 深入到 VB2 框架层,最后回调你的 驱动层 的过程。

1. 入口:V4L2 接口层 (v4l2_ioctl_ops)

用户态发起 VIDIOC_REQBUFS,内核首先查找 vb2fw_ioctl_ops 表。接口为.vidioc_reqbufs = vb2_ioctl_reqbufs

  • 定义:这是一个内核提供的通用辅助函数(Helper Function),不需要自己写。
  • 位置:drivers/media/common/videobuf2/videobuf2-v4l2.c
  • 作用:它是连接 V4L2 子系统和 VB2 框架的桥梁。从用户空间接收 struct v4l2_requestbuffers 结构体(包含用户想要的缓冲区数量 count 和内存类型 memory),找到设备对应的 vb2_queue,获取队列锁 (mutex lock),然后调用VB2 核心层的 vb2_core_reqbufs 函数

2. 协商:VB2 回调层 (vb2_ops)

VB2 核心层在准备分配内存之前,需要知道“到底要分配多大的内存”。因此,它会调用驱动提供的 queue_setup 回调函数,传入用户请求的缓冲区数量和每个缓冲区的大小数组。接口为.queue_setup = vb2fw_queue_setup

  • 定义:这是驱动程序必须实现的回调函数。
  • 位置:在你的驱动代码中定义。
  • 作用:协商缓冲区的数量和大小。驱动可以根据设备能力调整用户请求的缓冲区数量(如限制在最小和最大范围内),并计算每个缓冲区所需的大小(通常基于当前的视频格式)。最终,驱动将协商后的缓冲区数量和大小返回给 VB2 核心层。

当函数返回0后,VB2 核心层会继续执行内存分配逻辑,创建 vb2_buffer 结构体,并将它们包装在驱动的私有结构体(如 vb2fw_buffer)中。

3. 初始化:VB2 回调层 (vb2_ops)

内存分配成功后,内核创建了 nbuffers 个 struct vb2_buffer 结构体。为了实现初始化,内核会遍历这些缓冲区,调用驱动的 buf_init 回调函数。接口为.buf_init = vb2fw_buf_init

内核把刚分配好的、干干净净的 vb2_buffer 指针传给回调函数,通过 container_of 找到 vb2fw_buffer。

核心任务:调用 INIT_LIST_HEAD(&buf->list)。

注意:这里申请的nbuffers 个 struct vb2fw_buffer 在逻辑上和物理上都是相互独立的。

[ vb2_queue ]
    |
    +-> bufs[] (数组)
          |
          +-> [0] -----------------> [ Buffer 0 (独立对象) ]
          |
          +-> [1] -----------------> [ Buffer 1 (独立对象) ]
          |
          +-> [2] -----------------> [ Buffer 2 (独立对象) ]

入队缓冲区 (ioctl(fd, VIDIOC_QBUF, ...))

调用路径图如下所示:

User Space (用户空间)
   |
   +-> ioctl(fd, VIDIOC_QBUF, &buf)
         |
Kernel Space (内核空间)
   |
   +-> sys_ioctl (系统调用入口)
         |
         +-> v4l2_fops_unlocked_ioctl (VFS层 / V4L2核心)
               |
               +-> video_ioctl2 (加锁,查找 ioctl_ops )
                     |
                     +-> __video_do_ioctl
                           |
                           | // 查表 vb2fw_ioctl_ops,找到 .vidioc_qbuf
                           v
                           vb2_ioctl_qbuf (VB2 V4L2 适配层)
                                 |
                                 +-> vb2_core_qbuf (VB2 核心逻辑层)
                                       |
                                       +-> 1. call q->ops->buf_prepare()
                                       |      -> vb2fw_buf_prepare (你的驱动)
                                       |
                                       | // 状态变更为 VB2_BUF_STATE_QUEUED
                                       |
                                       +-> 2. call q->ops->buf_queue()
                                              -> vb2fw_buf_queue (你的驱动)

当用户空间应用程序调用 ioctl(fd, VIDIOC_QBUF, &buf) 时,内核会执行一系列函数调用,最终将缓冲区入队到驱动程序的管理列表中。以下是详细的调用路径解析:

1. 入口:V4L2 接口层 (v4l2_ioctl_ops)

用户态发起 VIDIOC_QBUF,内核首先查找 vb2fw_ioctl_ops 表。接口为.vidioc_qbuf = vb2_ioctl_qbuf。这是一个 VB2 提供的通用辅助函数(Helper),它负责将 V4L2 标准的 struct v4l2_buffer 转换为 VB2 内部逻辑,检查队列类型、内存模型等基础参数是否匹配,然后调用 VB2 核心层的 vb2_core_qbuf 函数

2. 核心处理:VB2 核心层 (vb2_core_qbuf)

VB2 核心层的 vb2_core_qbuf 函数是处理缓冲区入队的核心逻辑。根据用户传入的 index,找到对应的 vb2_buffer 结构体,确认该缓冲区当前是 DEQUEUED(出队/闲置)状态。只有闲置的缓冲区才能入队。最后触发两个驱动回调

3. 驱动回调:buf_prepare 准备

VB2 核心层通过 vb2fw_qops 结构体,调用自定义的 .buf_prepare。检查 vb2_plane_size(物理大小)是否满足当前的 sizeimage 要求,设置有效负载长度 vb2_set_plane_payload。

4. VB2 核心层 - 状态变更

在 buf_prepare 成功返回后,VB2 核心层会将缓冲区的状态从 DEQUEUED 变更为 QUEUED,表示该缓冲区已经准备好,可以被驱动程序处理。

5. 驱动回调:buf_queue 入队

VB2 核心层再次通过 vb2fw_qops,调用自定义的 .buf_queue。该函数将缓冲区添加到驱动维护的 buf_list 链表中,标志着缓冲区所有权从 VB2 核心层转移到驱动程序。

出队缓冲区 (ioctl(fd, VIDIOC_DQBUF, ...))

User Space (用户空间)
   |
   +-> ioctl(fd, VIDIOC_DQBUF, &buf)
         |
Kernel Space (内核空间)
   |
   +-> sys_ioctl
         |
         +-> video_ioctl2 (加锁 mutex)
               |
               +-> vb2_ioctl_dqbuf (VB2 V4L2 适配层)
                     |
                     +-> vb2_core_dqbuf (VB2 核心逻辑层)
                           |
                           +-> 1. [可选] 调用 q->ops->wait_prepare()
                           |      -> vb2_ops_wait_prepare (释放 mutex )
                           |
                           +-> 2. [阻塞等待] 检查 done_list
                           |      (如果队列空,进程在此休眠,等待驱动调用 vb2_buffer_done 唤醒)
                           |
                           +-> 3. [被唤醒] 调用 q->ops->wait_finish()
                           |      -> vb2_ops_wait_finish (重新获取 mutex )
                           |
                           +-> 4.  done_list 取出一个 Buffer
                           |
                           +-> 5. [可选] 调用 q->ops->buf_finish()
                           |      (你的 struct vb2_ops 中没有定义这个,所以跳过)
                           |
                           +-> 返回用户空间 (填充 v4l2_buffer 结构)

用户空间应用程序调用 ioctl(fd, VIDIOC_DQBUF, &buf) 时,内核会执行一系列函数调用,最终将一个已完成的缓冲区返回给用户空间。以下是详细的调用路径解析:

1. 入口:V4L2 接口层 (v4l2_ioctl_ops)

用户态发起 VIDIOC_DQBUF,内核首先查找 vb2fw_ioctl_ops 表。接口为.vidioc_dqbuf = vb2_ioctl_dqbuf。这是一个 VB2 提供的通用辅助函数(Helper),它负责将 V4L2 标准的 struct v4l2_buffer 转换为 VB2 内部逻辑,检查队列类型、内存模型等基础参数是否匹配,然后调用 VB2 核心层的 vb2_core_dqbuf 函数

2. 核心处理:VB2 核心层 (vb2_core_dqbuf)

VB2 核心层的 vb2_core_dqbuf 函数是处理缓冲区出队的核心逻辑。它会检查驱动程序维护的已完成缓冲区列表(done_list),如果列表为空,表示没有可用的缓冲区,进程会在此休眠,等待驱动程序通过 vb2_buffer_done 函数唤醒它。

3. 驱动回调层 - 锁的释放 (wait_prepare)

在进程休眠之前,VB2 必须释放video_device 的互斥锁 (dev->lock)。

4. 睡眠等待 (Sleep)

进程进入休眠状态,挂起在 VB2 的等待队列 (vq->done_wq) 上。

5. 驱动唤醒 (vb2_buffer_done)

进程被唤醒后,继续执行。回调函数为.wait_finish = vb2_ops_wait_finish。重新获取互斥锁。保证后续操作的并发安全。

FFmpeg 示例

ffmpeg -f lavfi -i testsrc=size=640x480:rate=30 -t 5 -f v4l2 /dev/video2

内核输出日志如下:

[ 5103.828253] open
[ 5103.828267] g_fmt
[ 5103.828269] g_fmt
[ 5103.828272] s_fmt: 640x480, fourcc=0x33424752
[ 5103.828276] try_fmt: 640x480, fourcc=0x33424752
[ 5103.828278] s_fmt: set to 640x480, size=921600
[ 5103.830538] DEBUG: queue_setup called, type=2, count=3
[ 5103.830545] queue_setup: nbuffers=3
[ 5103.830547] queue_setup: allocated 3 buffers of size 921600
[ 5103.830750] buf_init: buffer 0
[ 5103.830938] buf_init: buffer 1
[ 5103.831144] buf_init: buffer 2
[ 5103.831306] buf_prepare: buffer 0
[ 5103.831632] buf_prepare: buffer 1
[ 5103.831638] buf_queue: buffer 0
[ 5103.831640] buf_queue: buffer 1
[ 5103.831641] start_streaming: count=2
[ 5103.832261] buf_prepare: buffer 2
[ 5103.832263] buf_queue: buffer 2

这段日志非常完美地展示了一个 V4L2 输出设备(OUTPUT Device) 从“打开”到“准备就绪”再到“启动传输”的标准流程。

这条日志告诉你:FFmpeg 已经成功地把图像数据塞进了你的驱动,并且按下了启动按钮。


第一阶段:握手与协商 (Negotiation)

FFmpeg 刚开始运行,正在和你的驱动“对暗号”,确认格式。

  1. [ 5103.828253] open
  • 动作:FFmpeg 打开 /dev/video2 文件。
  • 意义:连接建立。
  1. **[ 5103.828267] g_fmt / s_fmt / try_fmt**
  • 动作

  • g_fmt (Get Format):FFmpeg 问:“你默认是什么格式?”

  • s_fmt (Set Format):FFmpeg 说:“我要发送 640x480RGB24 (fourcc=0x33424752) 数据给你。”

  • s_fmt: ... size=921600:驱动确认:“好的,一帧图像的大小是 921,600 字节 ()。”

  • 意义:双方达成一致,准备传输 RGB24 图像。


第二阶段:建仓与分配 (Allocation)

格式谈好了,现在要申请内存来存放图像数据。

  1. [ 5103.830538] queue_setup called, type=2, count=3
  • 动作ioctl(VIDIOC_REQBUFS)。FFmpeg 申请 3 个 缓冲区。
  • 意义:FFmpeg 想要“三缓冲”机制(一个填,一个发,一个备用)。
  1. [ 5103.830547] queue_setup: allocated 3 buffers of size 921600
  • 动作:内核分配了 3 块物理内存,每块 900KB。
  1. **[ 5103.830750] buf_init: buffer 0buffer 2**
  • 动作:驱动为这 3 个新诞生的缓冲区初始化了链表节点。
  • 状态:此时,3 个空盘子都在内核仓库里,都是“闲置”状态。

第三阶段:预填数据 (Pre-roll)

这是最关键的一步。在启动播放之前,FFmpeg 会先把一部分缓冲区填满,防止播放一开始就卡顿。

  1. [ 5103.831306] buf_prepare: buffer 0
  • 动作:FFmpeg 填好了第一帧画面(Buffer 0),调用 QBUF 入队。
  • 驱动检查:你的驱动检查发现大小合适,标记为有效。
  1. [ 5103.831632] buf_prepare: buffer 1
  • 动作:FFmpeg 填好了第二帧画面(Buffer 1),也调用 QBUF 入队。
  1. [ 5103.831638] buf_queue: buffer 0
  2. [ 5103.831640] buf_queue: buffer 1
  • 动作:Buffer 0 和 Buffer 1 通过了安检,被挂到了你的驱动链表 (dev->buf_list) 上。
  • 现状:现在你的驱动手里有两个填满数据的缓冲区,随时可以发送。

第四阶段:点火启动 (Ignition)

数据准备好了,FFmpeg 命令驱动开始工作。

  1. [ 5103.831641] start_streaming: count=2
  • 动作ioctl(VIDIOC_STREAMON)
  • 参数解读count=2 证实了在启动的这一瞬间,你的传送带上确实积压了 2 个 缓冲区(Buffer 0 和 1)。
  • 关键点:此时驱动应该启动定时器或线程,开始消耗这些数据。

第五阶段:后续补充 (Streaming)

启动后,FFmpeg 继续把剩下的缓冲区也填满塞进来。

  1. [ 5103.832261] buf_prepare: buffer 2
  2. [ 5103.832263] buf_queue: buffer 2
  • 动作:FFmpeg 把手里最后一张牌(Buffer 2)也打出去了。
  • 现状:Buffer 0, 1, 2 全部都在你的驱动链表里排队。

为什么日志到这里就断了?

这正是问题的所在。

  1. 满仓:FFmpeg 申请了 3 个缓冲区,并且把 0, 1, 2 全部 QBUF 给你的驱动了。
  2. 等待:FFmpeg 现在手里没有空盘子了。它发起了 dqbuf(请求回收到期的空盘子),然后进入了睡眠等待状态。
  3. 驱动罢工
  • 你的驱动虽然收到了 3 个缓冲区,也响应了 start_streaming
  • 但是(根据你之前的代码),你没有启动任何定时器去处理这些缓冲区。
  • 没有代码去调用 vb2_buffer_done
  1. 死锁
  • FFmpeg:“你用完 Buffer 0 了吗?还给我,我要填第 4 帧数据。”
  • 驱动:(沉默… 缓冲区在链表里生灰)
  • 结果:FFmpeg 永远等不到 Buffer 0 回来,所以它卡住了,不再打印任何新日志。

结论

前面的代码逻辑(初始化、内存分配、入队)完全正确! 现在的唯一任务是:start_streaming 里启动一个周期性任务(定时器),把这些积压的缓冲区一个个取出来,并调用 vb2_buffer_done 还给 FFmpeg。 只要你加上这一步,日志就会源源不断地滚动下去了。