4. videobuf2核心:理解vb2缓冲队列的设计哲学,queue与buf的管理机制

V4L2 驱动里最绕不开的,就是 videobuf2(简称 vb2)这套缓冲框架。很多初学者一上来就被 vb2_queue、vb2_buffer、各种 ops 回调搞得晕头转向。说实话,我早年刚接触 V4L2 时也踩过不少坑——缓冲区申请失败、DMA 地址不对、队列状态混乱,折腾了好几个通宵。

这一节,我就把 vb2 的核心设计思路拆开来讲。你理解了两个东西,整个框架就通了:queue 怎么管,buf 怎么流转。

4.1 为什么需要 videobuf2?

先问一个问题:为什么内核不直接让驱动自己管理缓冲区?

原因很简单——太乱了。不同的硬件平台(DMA 连续、分散、IOMMU)、不同的内存类型(物理连续、vmalloc、dma_alloc)、不同的用户态交互方式(mmap、DMABUF、用户指针),如果每个驱动都自己写一套,代码会爆炸。

vb2 做的就是:把缓冲区的分配、管理、流转、回收抽象成一套通用框架。驱动只需要实现几个回调,剩下的交给 vb2。

核心思想: vb2 把缓冲区视为"状态机",每个 buffer 在 VIDEOBUF2_QUEUED → VIDEOBUF2_DONE → VIDEOBUF2_DEQUEUED 之间流转。queue 负责维护这个状态机的秩序。

4.2 两个核心数据结构

要理解 vb2,先记住两个结构体:

结构体 作用 我关注的重点
struct vb2_queue 管理整个缓冲队列的"总管" 内存类型、操作回调、队列状态、驱动私有数据
struct vb2_buffer 单个缓冲区的"代言人" 状态、序号、plane 信息、时间戳、私有数据

我个人习惯把 vb2_queue 想象成一个"调度中心",vb2_buffer 是"任务单元"。调度中心决定哪个任务单元该干什么。

4.3 vb2_queue:队列的骨架

驱动初始化时,第一件事就是填充 struct vb2_queue。来看看关键字段:

struct vb2_queue {
    unsigned int            type;        // 队列类型:V4L2_BUF_TYPE_VIDEO_CAPTURE 等
    unsigned int            io_modes;    // 支持的 IO 模式:VB2_MMAP | VB2_DMABUF | VB2_USERPTR
    unsigned int            buf_struct_size; // 驱动自定义 buffer 结构体大小

    const struct vb2_ops    *ops;        // 操作回调(核心!)
    const struct vb2_mem_ops *mem_ops;   // 内存操作回调

    void                    *drv_priv;   // 驱动私有数据,通常指向你的设备结构体

    // 队列状态
    unsigned int            streaming:1; // 是否正在 streaming
    unsigned int            start_streaming_called:1;

    // 缓冲区列表
    struct list_head        queued_list; // 已入队待处理的 buffer
    unsigned int            queued_count;

    struct list_head        done_list;   // 已完成等待出队的 buffer
    unsigned int            done_count;

    // 等待队列
    wait_queue_head_t       done_wq;     // 用户态 poll/select 等待队列
};

嗯,这里要注意:buf_struct_size 这个字段很多人会忽略。它告诉 vb2 框架,你的驱动自定义 buffer 结构体有多大。vb2 在分配 buffer 时,会额外分配这块内存,并挂到 vb2_buffer.priv 上。

我的经验: 在驱动中定义自己的 buffer 结构体时,一定要把 struct vb2_buffer 放在第一个成员。这样你就可以用 container_of 从 vb2_buffer 拿到你的私有数据。很多驱动都是这么做的,比如:

struct my_buffer {
    struct vb2_buffer vb;      // 必须第一个
    struct list_head list;
    dma_addr_t dma_addr;
    // ... 其他私有字段
};

4.4 vb2_ops:驱动与框架的契约

这是 vb2 最核心的部分。驱动通过实现这些回调,告诉 vb2 框架"我的硬件该怎么操作"。

回调函数 调用时机 驱动该做什么
queue_setup 用户调用 REQBUFS 时 告诉框架需要多少 buffer、每个 plane 的大小
buf_prepare buffer 入队前 准备 DMA 映射、检查参数、填充 plane 信息
buf_queue buffer 正式入队后 把 buffer 加入硬件描述符链表,启动 DMA
start_streaming streamon 时 启动硬件传输,开始采集
stop_streaming streamoff 时 停止硬件,取消所有未完成的 buffer
buf_finish buffer 被释放前 清理 DMA 映射、释放资源

我曾经在一个项目中,buf_queue 回调里忘记把 buffer 加入硬件链表,结果用户态一直等不到数据。调试了整整一天才发现——硬件根本没拿到 buffer 的 DMA 地址。

4.5 缓冲区的生命周期:状态机

每个 vb2_buffer 都有一个状态字段 state,它决定了 buffer 当前在干什么。状态流转如下:

DEQUEUED ──(QBUF)──→ QUEUED ──(硬件完成)──→ DONE ──(DQBUG)──→ DEQUEUED

就这么简单?嗯,实际上内部还有 PREPARING、PREPARED 等中间状态,但核心就是这三个。

  • DEQUEUED:buffer 在用户态手里,驱动不能碰。
  • QUEUED:用户通过 QBUF 把 buffer 还给驱动,驱动可以往里面填数据。
  • DONE:硬件采集完成,驱动把 buffer 标记为完成,等待用户 DQBUF 取走。

避坑指南: 我曾经在 stop_streaming 回调里,没有把 queued_list 和 done_list 中的 buffer 全部回收到 DEQUEUED 状态。结果下次 streamon 时,vb2 发现还有 buffer 处于 QUEUED 状态,直接返回 -EBUSY。正确的做法是:遍历所有 buffer,调用 vb2_buffer_done(vb, VB2_BUF_STATE_ERROR) 强制回收。

4.6 核心流程:从 REQBUFS 到 DQBUF

我把整个流程画成了一张图,你一看就明白:

vb2 缓冲队列核心流程 用户态应用 vb2 核心框架 驱动 + 硬件 ① REQBUFS queue_setup() 分配内存 ② QBUF buf_prepare() DMA 映射 ③ STREAMON start_streaming() 启动 DMA ④ 硬件完成 buf_finish() ⑤ DQBUF buffer 状态流转 DEQUEUED → QUEUED → DONE → DEQUEUED(循环)

这张图里,我特意把用户态、vb2 框架、驱动三层分开了。你看,用户态只跟 vb2 框架交互(通过 ioctl),vb2 框架再调用驱动的回调。驱动不直接跟用户态打交道——这就是抽象的好处。

4.7 队列管理的关键机制

有几个机制,我觉得值得单独拿出来讲:

4.7.1 入队与出队的原子性

vb2 内部用自旋锁 vb2_queue.lock 保护队列操作。驱动在 buf_queue 回调里可以安全地操作硬件链表,不用担心并发问题。但要注意:不要在持有锁的情况下调用 vb2 框架的函数,否则可能死锁。

4.7.2 done_list 与唤醒机制

当硬件完成一个 buffer 时,驱动调用 vb2_buffer_done()。这个函数会把 buffer 从 queued_list 移到 done_list,然后唤醒等待在 done_wq 上的用户态进程。用户态的 DQBUF 和 poll 都是基于这个等待队列实现的。

4.7.3 最小 buffer 数量

vb2 要求至少 min_buffers_needed 个 buffer 入队后,才能调用 start_streaming。这个值由驱动在 queue_setup 中设置。我一般设 2 或 3——太少容易丢帧,太多浪费内存。

我的建议: 如果你的硬件支持乒乓操作(ping-pong),至少设 2 个 buffer。一个用于硬件采集,一个用于用户态处理,这样能最大化吞吐量。我曾经在一个 USB 摄像头驱动里只设了 1 个 buffer,结果帧率直接砍半——因为硬件必须等用户把 buffer 还回来才能继续采集。

4.8 避坑总结

最后,我把这些年遇到的 vb2 相关坑整理一下:

  • 忘记调用 vb2_buffer_done():硬件中断来了,驱动处理完数据后一定要调用这个函数,否则 buffer 永远卡在 QUEUED 状态,用户态等不到数据。
  • 在 start_streaming 中返回错误时未清理 buffer:如果 start_streaming 失败,必须把已经入队的 buffer 全部通过 vb2_buffer_done(vb, VB2_BUF_STATE_QUEUED) 归还,否则下次 streamon 会失败。
  • stop_streaming 中未取消所有 buffer:硬件停止后,遍历所有 buffer,调用 vb2_buffer_done(vb, VB2_BUF_STATE_ERROR) 强制完成。
  • buf_prepare 中 DMA 映射失败未处理:返回负错误码,vb2 会帮你把 buffer 回滚到 DEQUEUED 状态。