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

好,咱们直接进入正题。V4L2框架里,有三个结构体你绕不开——video_device、v4l2_device和v4l2_fh。说白了,它们就是整个视频驱动世界的「户口本」、「居委会」和「用户凭证」。

我刚开始啃V4L2源码时,也被这三者的关系绕晕过。后来在调试一个摄像头驱动时,发现v4l2_fh没初始化,导致应用层打开设备直接崩溃……嗯,从那以后我就老老实实把这三个结构体吃透了。

2.1 struct video_device —— 视频设备的「户口本」

每个视频设备节点(比如/dev/video0),背后都对应一个struct video_device。它负责向用户空间暴露设备能力,说白了就是告诉应用程序:我能干啥。

核心字段一览:

struct video_device {
    const struct v4l2_file_operations *fops;  // 文件操作函数集
    struct v4l2_device *v4l2_dev;             // 指向父设备
    struct device *dev;                       // 内核设备结构体
    struct cdev *cdev;                        // 字符设备
    struct v4l2_ctrl_handler *ctrl_handler;   // 控制句柄
    unsigned long flags;                      // 设备标志位
    int index;                                // 设备节点编号
    // ... 还有不少,但上面这些最常用
};

你想想看,应用层调用open("/dev/video0")时,内核怎么知道该调用哪个函数?就是靠fops这个指针。我习惯在驱动入口处先分配一个video_device,然后填充fops,再注册到内核。

个人经验:注册video_device前,一定要先设置v4l2_dev字段。我曾经漏掉这一步,结果v4l2_device_register返回成功,但应用层打开设备时一直报ENODEV。排查了半天,才发现是父设备指针没挂上。

2.2 struct v4l2_device —— 设备的「居委会」

v4l2_device是更上层的抽象。一个物理设备可能包含多个子设备(比如摄像头模组里有sensor、ISP、闪光灯),这些子设备都挂在同一个v4l2_device下。它负责协调管理,有点像居委会——管着整个小区(设备)的杂事。

struct v4l2_device {
    struct device *dev;                    // 物理设备
    struct list_head subdevs;              // 子设备链表
    spinlock_t lock;                       // 自旋锁
    char name[V4L2_DEVICE_NAME_SIZE];      // 设备名称
    // 调试用
    struct v4l2_ctrl_handler *ctrl_handler;
};

为什么需要这个结构体?我举个例子。你在一个嵌入式平台上接了三个摄像头,每个摄像头都有自己的video_device。但它们的电源管理、时钟同步、I2C总线都是共享的。v4l2_device就是用来管理这些共享资源的。

避坑指南:我曾经在项目里把v4l2_device和video_device的生命周期搞反了——先注销了v4l2_device,再注销video_device。结果内核直接Oops,因为video_device还在引用已经释放的父设备。记住:先注销子设备,再注销父设备。

2.3 struct v4l2_fh —— 用户的「通行证」

当应用程序打开一个video_device时,内核会为这次打开创建一个v4l2_fh实例。它记录了当前文件句柄的状态:订阅了哪些事件、当前优先级、正在使用的缓冲区队列等。

struct v4l2_fh {
    struct list_head list;              // 挂接到video_device的fh_list
    struct video_device *vdev;          // 所属的video_device
    struct v4l2_ctrl_handler *ctrl_handler; // 控制句柄(可独立)
    enum v4l2_priority priority;        // 设备访问优先级
    wait_queue_head_t wait;             // 事件等待队列
    // 事件订阅列表
    struct list_head subscribed;        
    struct list_head available;         
};

说白了,v4l2_fh就是用户和内核之间的「会话」。每个打开的设备节点都有一个独立的fh,互不干扰。你想想看,如果两个进程同时打开同一个摄像头,一个要设置曝光时间,另一个要设置白平衡——没有fh来隔离,不乱套才怪。

关键点:v4l2_fh不是必须的。如果你的驱动不需要事件订阅、优先级管理这些功能,完全可以不初始化它。但说实话,我建议你还是加上——因为很多用户空间工具(比如v4l2-ctl)会依赖这些特性。

2.4 三者之间的关系 —— 一张图说清楚

这三个结构体的关系,我用一张SVG图来展示。你看完就明白了。

struct v4l2_device (居委会) struct video_device /dev/video0 fops: v4l2_file_operations v4l2_dev → 指向居委会 fh_list → 用户会话列表 struct video_device /dev/video1 fops: v4l2_file_operations v4l2_dev → 指向居委会 fh_list → 用户会话列表 struct v4l2_fh vdev → /dev/video0 priority: V4L2_PRIORITY_DEFAULT struct v4l2_fh vdev → /dev/video0 priority: V4L2_PRIORITY_RECORD struct v4l2_fh vdev → /dev/video1 priority: V4L2_PRIORITY_INTERACTIVE video_device (设备节点) v4l2_fh (用户会话) v4l2_device (父设备)

从上图你能看到:

  • 一个v4l2_device可以挂多个video_device —— 就像居委会管着好几栋楼
  • 一个video_device可以有多个v4l2_fh —— 多个进程可以同时打开同一个设备节点
  • 每个v4l2_fh只属于一个video_device —— 用户的会话是独立的

2.5 实际驱动中的初始化流程

我习惯的初始化顺序是这样的,你参考一下:

// 1. 先分配并注册 v4l2_device
struct v4l2_device *v4l2_dev;
v4l2_dev = kzalloc(sizeof(*v4l2_dev), GFP_KERNEL);
v4l2_device_register(parent_dev, v4l2_dev);

// 2. 再分配 video_device,并关联 v4l2_device
struct video_device *vdev;
vdev = video_device_alloc();
vdev->v4l2_dev = v4l2_dev;  // 挂到居委会
vdev->fops = &my_fops;
vdev->release = video_device_release;

// 3. 注册 video_device
video_register_device(vdev, VFL_TYPE_VIDEO, -1);

// 4. 在 open 回调中初始化 v4l2_fh
static int my_open(struct file *file) {
    struct video_device *vdev = video_devdata(file);
    struct v4l2_fh *fh = kzalloc(sizeof(*fh), GFP_KERNEL);
    v4l2_fh_init(fh, vdev);   // 初始化会话
    v4l2_fh_add(fh);          // 添加到 video_device 的 fh_list
    file->private_data = fh;  // 关联到文件句柄
    return 0;
}

小技巧:在release回调里,别忘了调用v4l2_fh_del和v4l2_fh_exit。我见过不少驱动只分配不释放,导致内存泄漏。虽然嵌入式设备重启后内存会回收,但长期运行的设备会慢慢耗尽内存。

2.6 避坑总结

常见问题 原因 解决办法
应用层打开设备返回ENODEV video_device的v4l2_dev未设置 注册前检查vdev->v4l2_dev是否有效
多个进程同时操作设备崩溃 未使用v4l2_fh隔离状态 在open中创建v4l2_fh并关联
卸载驱动时内核Oops 先注销了v4l2_device 先注销所有video_device,再注销v4l2_device
事件订阅不生效 v4l2_fh未初始化事件列表 调用v4l2_fh_init时会自动初始化

嗯,这三个结构体就讲到这里。说白了,v4l2_device是根,video_device是干,v4l2_fh是叶。根深才能叶茂,这个道理在驱动开发里也一样适用。


可可精品资料库