3、V4L2核心数据结构:struct video_device、struct v4l2_device、struct vb2_queue详解

好,咱们直接进入正题。V4L2框架里这三个结构体,说白了就是Camera驱动的三根柱子。你把它仨搞明白了,整个驱动框架的骨架也就搭起来了。我刚开始啃V4L2源码的时候,就是被这三个结构体绕晕过好几次,后来慢慢理清了它们各自的分工,才觉得豁然开朗。

3.1 struct video_device:用户态看到的那个“设备文件”

这个结构体,对应的是你在 /dev/video0 里看到的那个节点。用户程序打开它、调用ioctl,最终都会落到这个结构体关联的操作函数上。

我个人习惯,把它理解成“前台接待员”。用户看到的就是它,所有的交互都通过它来中转。

struct video_device {
    const struct v4l2_file_operations *fops;
    struct device dev;
    struct v4l2_device *v4l2_dev;
    // ... 省略其他成员
};

这里最关键的,就是 fops 指针。它指向一个 v4l2_file_operations 结构体,里面定义了 open、close、ioctl、mmap 这些回调函数。你想想看,用户程序调用 open("/dev/video0"),内核最终就会调用你注册的这个 fops->open

核心要点:每个video_device实例,代表一个独立的视频设备节点。如果你的硬件有多个摄像头,那就需要注册多个video_device。

我在项目中遇到过一个问题:注册video_device时忘了设置 v4l2_dev 字段,结果导致设备节点创建成功了,但上层应用一打开就崩溃。排查了半天才发现是父设备指针没挂上。嗯,这里要注意,v4l2_dev 必须指向一个已经初始化好的 v4l2_device 实例。

3.2 struct v4l2_device:驱动层的“总管家”

这个结构体,是V4L2驱动实例的顶层抽象。它管理着所有子设备(比如sensor、isp、flash),也管理着所有video_device节点。

说白了,它就是整个Camera驱动的“总管家”。

struct v4l2_device {
    struct device *dev;
    struct list_head subdevs;
    struct list_head video_devices;
    spinlock_t lock;
    // ... 省略其他成员
};

你看,它维护了两个链表:subdevs 链表挂载所有子设备,video_devices 链表挂载所有video_device。这样设计的好处是,驱动内部可以方便地遍历所有子设备,或者找到某个video_device对应的父设备。

我的经验:初始化v4l2_device时,一定要把 dev 指针指向你的平台设备(platform_device)。否则,sysfs里的设备层次结构会乱掉,调试起来很头疼。

我曾经踩过一个坑:在热插拔场景下,如果先销毁了v4l2_device,再销毁video_device,内核会因为访问野指针而panic。正确的顺序应该是:先注销所有video_device,再注销v4l2_device。这个顺序,你写代码的时候一定要记牢。

3.3 struct vb2_queue:视频缓冲区的“调度中心”

这个结构体,是V4L2框架里最复杂、也最核心的部分。它负责管理视频数据的缓冲区队列,包括分配、入队、出队、同步等操作。

你可以把它想象成一个“缓冲区调度中心”。用户程序通过ioctl把缓冲区交给你,你填好数据再还回去,中间的所有调度逻辑都由vb2_queue来管理。

struct vb2_queue {
    enum v4l2_buf_type type;
    unsigned int io_modes;
    const struct vb2_ops *ops;
    struct mutex lock;
    // ... 省略大量成员
};

这里有几个关键字段:

  • type:缓冲区类型,比如 V4L2_BUF_TYPE_VIDEO_CAPTURE 表示采集,V4L2_BUF_TYPE_VIDEO_OUTPUT 表示输出。
  • io_modes:支持的I/O模式,比如 VB2_MMAPVB2_USERPTRVB2_DMABUF
  • ops:回调函数集,这是你驱动需要实现的核心接口。

避坑指南:我曾经在实现热插拔时,忘记在设备拔出时调用 vb2_queue_release(),结果导致内存泄漏。后来加上了,问题解决。记住,vb2_queue的生命周期必须和硬件设备严格绑定。

来看看 vb2_ops 里需要实现哪些回调:

回调函数 作用 我的建议
queue_setup 分配缓冲区时调用,确定缓冲区数量和大小 这里要根据当前分辨率动态计算大小
buf_prepare 缓冲区入队前调用,做准备工作 比如检查缓冲区大小是否匹配
buf_queue 缓冲区正式入队,等待硬件处理 这里要把缓冲区挂到硬件的DMA链表上
start_streaming 启动视频流 开启硬件时钟、DMA传输
stop_streaming 停止视频流 关闭硬件、清理未完成的缓冲区

你想想看,为什么需要这些回调?因为V4L2框架不知道你的硬件怎么工作,它只负责调度。具体的硬件操作,比如怎么把缓冲区地址告诉DMA控制器,怎么启动传输,这些都得你驱动来实现。

特别注意:在热插拔场景下,stop_streaming 回调可能会在中断上下文中被调用。所以这个函数里不能做睡眠操作,比如不能调用 kmalloc(GFP_KERNEL)。我当初就因为这个原因,在调试热插拔时遇到了内核调度警告,后来改用 GFP_ATOMIC 才解决。

3.4 三个结构体的协作关系

这三个结构体不是孤立的,它们之间有明确的层级关系:

  1. v4l2_device 是顶层,管理整个驱动实例。
  2. video_device 是中层,代表用户看到的设备节点。
  3. vb2_queue 是底层,负责实际的数据流管理。

一个典型的初始化流程是这样的:

// 1. 先初始化v4l2_device
v4l2_device_register(platform_dev, &v4l2_dev);

// 2. 再初始化vb2_queue
vb2_queue_init(&queue, &v4l2_dev, ...);

// 3. 最后注册video_device,并关联vb2_queue
video_device.v4l2_dev = &v4l2_dev;
video_set_drvdata(&video_device, &queue);
video_register_device(&video_device, VFL_TYPE_VIDEO, -1);

这个顺序不能乱。为什么?因为video_device注册时需要知道它属于哪个v4l2_device,而vb2_queue初始化时需要知道它属于哪个v4l2_device。你想想看,如果顺序反了,video_device注册时v4l2_device还没初始化好,那不就出问题了?

我的小技巧:在调试热插拔时,我习惯在 struct v4l2_device 里加一个自定义的私有数据指针,指向包含所有子设备信息的结构体。这样在回调函数里,通过 v4l2_dev 就能拿到所有上下文,不用到处传指针。

好了,这三个结构体就讲到这里。它们之间的关系,说白了就是:v4l2_device是家,video_device是门,vb2_queue是水管。家管着门和水管,门是用户进出的通道,水管负责把数据流进来流出去。你把这个比喻记住了,整个框架的脉络也就清晰了。