第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);

这个函数会做几件事:

  1. 标记设备为已注销状态,后续的 open 调用会返回 -ENODEV。
  2. 移除 /dev/videoX 节点。
  3. 唤醒所有正在等待这个设备的进程(比如正在阻塞在 poll 上的应用程序)。
  4. 释放内部占用的资源。

我曾经犯过一个错误:在注销之后,没有把 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_* 时,内核是怎么一步步走到你的驱动代码里的。到时候你会发现,注册只是第一步,真正的重头戏还在后面。