第4章:设备注册与注销:video_register_device、video_unregister_device、设备节点创建
好,我们继续往下走。上一章我们聊了怎么探测到摄像头,这一章要解决的是:探测到了之后,怎么让它在系统里「活」起来?说白了,就是怎么把我们的 video_device 注册进内核,让用户空间能看到 /dev/videoX 这个节点。
我个人习惯把设备注册看作「上户口」。你硬件插上去了,驱动也 probe 成功了,但如果没有注册,系统里就查无此人。用户空间想打开摄像头?门都没有。
4.1 video_register_device:给设备上户口
这个函数是 V4L2 框架里最核心的注册接口。它的原型长这样:
int video_register_device(struct video_device *vdev,
int type, int nr);
三个参数,我来拆开讲:
- vdev:你之前初始化好的 video_device 结构体指针。注意,里面的 v4l2_dev、fops、ioctl_ops 这些字段必须已经填好了。
- type:设备类型。摄像头一般用 VFL_TYPE_VIDEO,如果是 radio 或者 vbi 设备,就用对应的类型。
- nr:你想让设备节点编号从几开始?填 -1 表示让内核自动分配。我个人建议新手先用 -1,省得跟其他设备冲突。
我在项目中遇到过一个问题:有个同事把 type 填成了 VFL_TYPE_GRABBER,结果 /dev/videoX 死活不出来,出来个 /dev/vbiX。查了半天才发现是类型搞错了。你想想看,这种低级错误排查起来真的很浪费时间。
核心要点:video_register_device 调用成功后,内核会自动在 /dev 下创建 videoX 节点,并且关联上你注册的 fops。用户空间 open("/dev/video0") 时,实际上就调到了你写的 v4l2_fops.open。
4.2 设备节点创建:内核帮你做了什么?
很多人以为设备节点是驱动自己创建的,其实不是。内核的 V4L2 核心代码在 video_register_device 内部会调用 device_create 或者类似机制,通过 devtmpfs 自动生成节点。
嗯,这里要注意:如果你的内核没有开启 devtmpfs 支持,那节点可能不会自动出现。不过现在主流的内核都默认开启了,除非你用的是特别老的版本或者自己裁剪的内核。
节点名称的规则是这样的:
- type 为 VFL_TYPE_VIDEO 时,节点名是 video0、video1……
- type 为 VFL_TYPE_VBI 时,节点名是 vbi0、vbi1……
- type 为 VFL_TYPE_RADIO 时,节点名是 radio0、radio1……
你传的 nr 参数决定了起始编号。比如 nr=2,那第一个设备就是 video2,第二个是 video3,以此类推。如果 nr 被占用了,内核会自动往后找空闲的编号。
小技巧:调试的时候,你可以用 ls -l /dev/video* 看看节点有没有生成。如果节点存在但打不开,多半是权限问题或者 fops 没写对。
4.3 video_unregister_device:优雅地注销
有注册就有注销。当摄像头被拔掉,或者驱动要卸载时,必须调用 video_unregister_device 来清理资源。
void video_unregister_device(struct video_device *vdev);
这个函数会做几件事:
- 标记设备为已注销状态,后续的 open 调用会返回 -ENODEV。
- 移除 /dev/videoX 节点。
- 唤醒所有正在等待这个设备的进程(比如正在阻塞在 poll 上的应用程序)。
- 释放内部占用的资源。
我曾经犯过一个错误:在注销之后,没有把 video_device 结构体置零或者释放。结果下次热插拔时,内核发现这个结构体还在,就报了一堆奇怪的错误。后来我养成了一个习惯:注销之后,紧接着调用 video_device_release 或者 kfree,把内存也清掉。
警告:不要在中断上下文或者持有自旋锁的时候调用 video_unregister_device。这个函数可能会睡眠,因为它要等待正在进行的 IO 操作完成。如果你在原子上下文中调用,系统会直接 panic。
4.4 完整的注册与注销流程
我一般会在 probe 函数里这样写:
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. 初始化字段
vdev->v4l2_dev = &mycam->v4l2_dev;
vdev->fops = &mycam_fops;
vdev->ioctl_ops = &mycam_ioctl_ops;
vdev->release = video_device_release;
strscpy(vdev->name, "MyCamera", sizeof(vdev->name));
// 3. 注册
ret = video_register_device(vdev, VFL_TYPE_VIDEO, -1);
if (ret) {
video_device_release(vdev);
return ret;
}
// 4. 保存指针,方便注销时使用
mycam->vdev = vdev;
return 0;
}
注销时对应的是:
static int mycam_remove(struct platform_device *pdev)
{
struct mycam_device *mycam = platform_get_drvdata(pdev);
// 先注销,再释放
video_unregister_device(mycam->vdev);
// 注意:如果 release 回调设了 video_device_release,
// 这里不需要再手动 kfree
return 0;
}
你看,流程其实很清晰。但有个细节很多人会忽略:注册和注销的顺序必须对称。如果你在 probe 里先注册了 video_device,然后又注册了其他子系统,那 remove 时一定要先注销其他子系统,最后注销 video_device。否则用户空间可能还在用你的设备,你这边已经把底层硬件关了,就会出现访问野指针的问题。
4.5 热插拔场景下的特殊处理
对于 USB 摄像头这类热插拔设备,注册和注销的时机跟平台设备略有不同。USB 驱动里,probe 是在设备插入时调用,disconnect 是在拔出时调用。所以注册写在 probe 里,注销写在 disconnect 里,这个逻辑是一样的。
但有个坑:USB 设备拔出时,如果用户空间还在打开 /dev/videoX,那 disconnect 会被阻塞,直到用户空间关闭文件描述符。这就是为什么有些摄像头拔掉之后,/dev/videoX 节点还在,要等几秒才消失。这不是 bug,是内核为了保护数据安全而设计的。
避坑指南:我曾经调试过一个摄像头,拔掉后节点一直不消失。查了半天发现是用户空间的某个进程一直没 close。后来我在 disconnect 里加了 printk,打印出当前打开设备的进程 PID,才找到元凶。所以如果你遇到节点不消失的问题,先看看有没有进程占着文件描述符不放。
4.6 总结一下
这一章的内容其实就三件事:
- 注册:video_register_device 把设备挂进内核,生成 /dev/videoX。
- 注销:video_unregister_device 移除节点,清理资源。
- 节点创建:内核自动完成,你只需要关心 type 和 nr 参数。
下一章我们会深入 ioctl 的处理流程,看看用户空间调用 VIDIOC_* 时,内核是怎么一步步走到你的驱动代码里的。到时候你会发现,注册只是第一步,真正的重头戏还在后面。