2. V4L2 核心数据结构:struct video_device、struct v4l2_device、struct vb2_queue

在深入 mmap 与 poll 实现之前,必须理解 V4L2 框架中三个最核心的数据结构。它们构成了从设备注册、视频节点管理到缓冲区队列流转的完整骨架。掌握这些结构体的职责与关联,是编写正确驱动与应用程序的基础。

2.1 数据结构全景关系

下图展示了三个核心结构体在 V4L2 框架中的层次关系与协作方式:

V4L2 核心数据结构关系图 struct v4l2_device 顶层设备抽象 管理子设备与全局状态 subdevs 链表头 struct video_device 字符设备节点 (/dev/videoX) 实现 file_operations v4l2_dev 指针 struct vb2_queue 缓冲区队列引擎 内存管理与流控制 ops 回调 parent queue 用户空间:通过 /dev/videoX 调用 open/ioctl/mmap/poll 内核空间:V4L2 框架 + 设备驱动 关键协作流程: 1. v4l2_device 注册到内核,作为顶层容器 2. video_device 创建字符设备,关联 v4l2_device 3. vb2_queue 挂载到 video_device,处理缓冲区申请/入队/出队 4. 用户通过 ioctl 操作 video_device,最终由 vb2_queue 执行内存映射

2.2 struct v4l2_device — 顶层设备容器

struct v4l2_device 是 V4L2 框架中最顶层的抽象结构,代表一个完整的视频设备实例。它作为容器管理所有子设备(subdev)和全局状态信息。

字段 类型 说明
dev struct device * 指向内核通用设备结构,用于电源管理、sysfs 等
subdevs struct list_head 子设备链表头,管理所有挂载的 subdev
ctrl_handler struct v4l2_ctrl_handler * 控制处理器,管理亮度、对比度等控制项
name char[32] 设备名称,用于调试和标识
notify void (*notify)() 子设备通知回调函数

驱动开发要点:在 probe 函数中,驱动必须首先调用 v4l2_device_register() 注册顶层设备。该函数接受父 struct device * 和 struct v4l2_device * 作为参数。所有后续的 video_device 和 subdev 都依附于此结构。

2.3 struct video_device — 字符设备节点

struct video_device 是用户空间直接交互的接口,对应 /dev/videoX 字符设备节点。它封装了 file_operations 的实现,并将用户请求路由到具体的驱动回调。

字段 类型 说明
fops const struct v4l2_file_operations * V4L2 文件操作函数表(open/release/ioctl/mmap/poll)
v4l2_dev struct v4l2_device * 指向所属的顶层 v4l2_device
queue struct vb2_queue * 指向缓冲区队列,用于流 I/O
minor int 次设备号,用于标识设备节点
device_caps u32 设备能力标志(如 V4L2_CAP_VIDEO_CAPTURE)
ioctl_ops const struct v4l2_ioctl_ops * ioctl 操作函数表,处理 VIDIOC_* 命令

关键理解:用户空间的 open("/dev/video0") 最终调用的是 video_device.fops->open。而 ioctl(fd, VIDIOC_REQBUFS, ...) 则通过 video_device.ioctl_ops 路由到 vb2_queue 的缓冲区管理函数。这种分层设计使得驱动开发者只需实现底层硬件操作,V4L2 框架自动完成标准接口的封装。

2.4 struct vb2_queue — 缓冲区队列引擎

struct vb2_queue 是 V4L2 帧采集的核心引擎,负责缓冲区生命周期管理、内存分配、DMA 映射以及流控制。mmap 映射和 poll 机制都直接依赖于 vb2_queue 的正确配置。

字段 类型 说明
type enum v4l2_buf_type 缓冲区类型(V4L2_BUF_TYPE_VIDEO_CAPTURE 等)
io_modes u32 支持的 I/O 模式位掩码(VB2_MMAP | VB2_USERPTR | VB2_DMABUF)
ops const struct vb2_ops * 驱动回调函数表(queue_setup/buf_prepare/buf_queue/start_streaming/stop_streaming)
mem_ops const struct vb2_mem_ops * 内存操作函数表(alloc/put/get_dmabuf/attach_dmabuf 等)
buf_struct_size unsigned int 每个缓冲区私有数据的大小(驱动可扩展)
min_buffers_needed unsigned int 启动流所需的最小缓冲区数量
timestamp_flags u32 时间戳标志(V4L2_BUF_FLAG_TIMESTAMP_*)

2.5 vb2_ops 回调函数详解

struct vb2_ops 是驱动与 vb2_queue 之间的契约。驱动必须实现这些回调,vb2_queue 在合适的时机调用它们:

回调函数 调用时机 驱动职责
queue_setup REQBUFS 或 CREATE_BUFS 时 返回缓冲区数量、大小和格式信息
buf_prepare QBUF 时,缓冲区入队前 验证缓冲区参数,准备 DMA 映射
buf_queue QBUF 时,缓冲区入队后 将缓冲区加入硬件处理队列
start_streaming STREAMON 时 启动硬件采集,至少 min_buffers_needed 个缓冲区已入队
stop_streaming STREAMOFF 或出错时 停止硬件采集,取消未完成的缓冲区
buf_finish DQBUF 后或清理时 清理缓冲区资源(可选)

常见陷阱:许多初学者在 queue_setup 中返回固定的缓冲区数量,但未考虑 min_buffers_needed 的约束。如果 min_buffers_needed = 4,而用户只请求 3 个缓冲区,start_streaming 将不会被调用。驱动应检查 reqbufs.count 并确保满足最小需求。

2.6 数据结构初始化流程

在驱动 probe 函数中,三个核心结构的初始化顺序至关重要:

  1. 注册 v4l2_device:调用 v4l2_device_register(parent_dev, &v4l2_dev)
  2. 初始化 vb2_queue:填充 struct vb2_queue 字段,调用 vb2_queue_init(&v4l2_dev, &vb2_q)
  3. 注册 video_device:设置 video_device.v4l2_dev 和 video_device.queue,调用 video_register_device(&video_dev, VFL_TYPE_VIDEO, -1)
// 典型驱动 probe 中的初始化代码片段
static int mydriver_probe(struct platform_device *pdev)
{
    struct mydriver_dev *dev;
    int ret;

    dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
    if (!dev)
        return -ENOMEM;

    // 1. 注册 v4l2_device
    ret = v4l2_device_register(&pdev->dev, &dev->v4l2_dev);
    if (ret < 0)
        return ret;

    // 2. 初始化 vb2_queue
    dev->vb2_q.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    dev->vb2_q.io_modes = VB2_MMAP | VB2_USERPTR;
    dev->vb2_q.ops = &mydriver_vb2_ops;
    dev->vb2_q.mem_ops = &vb2_dma_contig_memops;
    dev->vb2_q.drv_priv = dev;
    dev->vb2_q.min_buffers_needed = 2;
    dev->vb2_q.timestamp_flags = V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC;
    ret = vb2_queue_init(&dev->vb2_q);
    if (ret < 0)
        goto err_v4l2;

    // 3. 注册 video_device
    dev->video_dev.v4l2_dev = &dev->v4l2_dev;
    dev->video_dev.queue = &dev->vb2_q;
    dev->video_dev.fops = &mydriver_fops;
    dev->video_dev.ioctl_ops = &mydriver_ioctl_ops;
    dev->video_dev.device_caps = V4L2_CAP_VIDEO_CAPTURE |
                                  V4L2_CAP_STREAMING;
    ret = video_register_device(&dev->video_dev,
                                VFL_TYPE_VIDEO, -1);
    if (ret < 0)
        goto err_queue;

    platform_set_drvdata(pdev, dev);
    return 0;

err_queue:
    vb2_queue_release(&dev->vb2_q);
err_v4l2:
    v4l2_device_unregister(&dev->v4l2_dev);
    return ret;
}

2.7 数据结构关系总结

下表总结了三个核心结构在 mmap 与 poll 机制中的角色:

结构体 mmap 中的角色 poll 中的角色
v4l2_device 提供设备上下文,管理内存分配器 提供中断处理上下文
video_device 实现 v4l2_fops->mmap,调用 vb2_mmap 实现 v4l2_fops->poll,调用 vb2_poll
vb2_queue 管理 DMA 缓冲区,建立用户空间映射 维护缓冲区状态,触发 POLLIN/POLLOUT 事件

核心结论:用户空间的 mmap 调用最终由 vb2_queue 的 mem_ops->mmap 完成物理内存到用户空间的映射。而 poll 调用则由 vb2_queue 的缓冲区状态机驱动:当有缓冲区完成采集(DQBUF 可用)时,触发 POLLIN 事件唤醒等待的进程。下一章将深入分析 mmap 映射的具体实现细节。