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图来展示。你看完就明白了。
从上图你能看到:
- 一个
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是叶。根深才能叶茂,这个道理在驱动开发里也一样适用。