4. V4L2设备注册与注销
好,咱们进入第四章。前面几章我们把 V4L2 的框架结构、数据结构理了一遍,算是搭好了认知的骨架。这一章,咱们要让这个骨架真正「活」起来——说白了,就是怎么把摄像头设备注册到内核里,让用户空间能通过 /dev/videoX 找到它;以及怎么在驱动卸载时,干净地把它注销掉。
我刚开始写摄像头驱动时,觉得注册不就是调个函数嘛,有什么难的?结果第一次调试,设备节点死活不出来,dmesg 里也没报错。后来才发现,是注册时机和数据结构初始化顺序搞反了。嗯,这种坑,咱们今天一并填上。
4.1 注册前的准备工作
调用 video_register_device 之前,有几件事必须做扎实。我个人习惯,把这一步叫做「三件套」:
- 分配并初始化 video_device 结构体
- 填充 v4l2_file_operations 和 v4l2_ioctl_ops
- 设置 release 回调函数
你想想看,内核里那么多设备,它怎么知道你的摄像头支持哪些操作?全靠这些回调函数告诉它。少填一个,用户空间调用对应 ioctl 时就会返回 -ENOTTY,而且连个警告都没有——我排查过这种问题,真的很头疼。
核心原则:video_device 中的 vfl_dir 字段必须正确设置。V4L2 设备分三种方向:VFL_DIR_RX(捕获,摄像头)、VFL_DIR_TX(输出,显示)、VFL_DIR_M2M(内存到内存,编解码)。设错了,应用层打开设备就会失败。
4.2 video_register_device 深度解析
函数原型长这样:
int video_register_device(struct video_device *vdev,
int type,
int nr,
int warn_unnamed_null,
struct module *owner);
参数看着多,其实核心就三个:
| 参数 | 含义 | 我的建议 |
|---|---|---|
vdev |
已经初始化好的 video_device 指针 | 确保所有必要字段已赋值 |
type |
设备类型,如 VFL_TYPE_VIDEO |
摄像头用这个,别搞混 |
nr |
设备编号,-1 表示让内核自动分配 | 我建议传 -1,省心 |
这里有个细节:nr 传 -1 时,内核会从 0 开始找第一个可用的编号。如果你有多个同类型设备,自动分配能避免编号冲突。我曾经在一个项目里手动指定了编号,结果设备热插拔后编号乱了,应用层一脸懵。从那以后,我全用自动分配。
小技巧:注册成功后,video_device 的 num 字段会被赋值为实际分配的设备号。你可以通过 vdev->num 拿到它,用于打印日志或生成 sysfs 属性。
4.3 实战:完整的注册流程
咱们直接看代码。这是一个简化的摄像头驱动注册片段:
static int mycam_probe(struct platform_device *pdev)
{
struct video_device *vdev;
int ret;
// 1. 分配 video_device
vdev = video_device_alloc();
if (!vdev)
return -ENOMEM;
// 2. 设置名称(会在 /sys/class/video4linux/ 下看到)
strscpy(vdev->name, "MyCamera", sizeof(vdev->name));
// 3. 绑定文件操作
vdev->fops = &mycam_fops;
vdev->ioctl_ops = &mycam_ioctl_ops;
// 4. 设置释放回调(重要!)
vdev->release = mycam_release;
// 5. 设置设备类型
vdev->vfl_dir = VFL_DIR_RX;
vdev->device_caps = V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING;
// 6. 注册设备
ret = video_register_device(vdev, VFL_TYPE_VIDEO, -1);
if (ret) {
video_device_release(vdev);
return ret;
}
// 7. 保存到私有数据,方便后续使用
platform_set_drvdata(pdev, vdev);
return 0;
}
注意第 4 步的 release 回调。这个函数会在最后一个用户关闭设备文件描述符时被调用,用于释放 video_device 占用的内存。如果你忘了设置它,内核会在 v4l2_device_unregister 时打印一个警告,而且内存泄漏是跑不掉的。
避坑指南:我曾经在 release 回调里只调了 kfree,忘了调 video_device_release。结果每次 rmmod 后重新 insmod,都会分配一个新的 video_device,旧的那个永远释放不掉。用 kmemleak 一查,全是我的锅。
4.4 设备注销:video_unregister_device
注销比注册简单,但坑也不少。核心函数就一个:
void video_unregister_device(struct video_device *vdev);
调用这个函数后,内核会:
- 立即从系统中移除
/dev/videoX设备节点 - 标记设备为已注销状态
- 等待所有正在进行的文件操作完成
- 当最后一个引用计数归零时,调用你设置的
release回调
这里有个关键点:注销是同步的,但释放是异步的。也就是说,video_unregister_device 返回后,设备节点已经没了,但 video_device 结构体可能还活着——因为有用户空间程序正拿着文件描述符在做操作。
对应的注销代码:
static int mycam_remove(struct platform_device *pdev)
{
struct video_device *vdev = platform_get_drvdata(pdev);
// 先注销,再释放其他资源
video_unregister_device(vdev);
// 注意:这里不要调 video_device_release!
// release 回调会在引用计数归零时自动调用
return 0;
}
重要:不要在 remove 函数里手动调用 video_device_release。如果你这么做了,而恰好有用户空间程序还在用这个设备,内核会访问已经释放的内存——恭喜你,你复现了一个经典的 use-after-free bug。
4.5 注册与注销的完整生命周期
为了让你更直观地理解整个过程,我画了一张流程图:
4.6 常见错误与排查方法
我把这些年遇到的注册/注销相关的问题整理了一下,你对照着看,能省不少调试时间:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 注册返回 0,但没生成设备节点 | udev 规则问题,或 v4l2_device_register 没调 |
检查 /sys/class/video4linux/ 下有没有对应目录 |
| open 设备返回 -ENODEV | 设备已注销,或 vfl_dir 设置错误 |
dmesg 看有没有 V4L2 相关警告 |
| ioctl 返回 -ENOTTY | ioctl_ops 没填完整 |
检查 vidioc_querycap 等必须回调是否实现 |
| rmmod 时内核崩溃 | 注销顺序不对,或 release 回调里访问了已释放的资源 | 加 might_sleep 和引用计数调试 |
| 设备节点编号不连续 | 之前有设备未正常注销 | 检查 /dev/video* 列表,确认驱动是否泄漏了设备 |
调试利器:注册成功后,去 /sys/class/video4linux/videoX/ 目录下看看。里面的 name 文件就是你设置的设备名,index 是内核分配的序号。如果这些文件不存在,说明注册根本没成功。
4.7 关于设备编号的补充说明
最后聊一个容易被忽略的点:设备编号的分配策略。内核提供了三种方式:
- 传 -1:自动分配,从 0 开始找第一个空闲编号
- 传具体数字:固定编号,如果被占用则注册失败
- 传
V4L2_VIDDEV_NR_FORCE宏:强制使用指定编号,即使被占用也覆盖(不推荐)
我个人强烈建议用自动分配。固定编号在单设备场景下没问题,但一旦涉及多个摄像头或者热插拔,很容易出乱子。你想想看,用户插了两个摄像头,一个固定为 video0,另一个自动分配为 video1。结果第一个拔了再插,video0 被占着,新设备只能拿 video2——应用层如果写死了用 video0,那就尴尬了。