4. V4L2设备注册与注销:video_register_device、video_unregister_device、设备节点创建

好,咱们继续往下走。上一章我们把V4L2的设备结构体搭好了,这一章要解决一个核心问题:怎么让这个设备在系统里“活”起来?说白了,就是怎么注册它,怎么让用户空间能看到它,以及怎么优雅地把它卸掉。

我个人习惯把设备注册比作“上户口”。你结构体定义得再好,不往内核的名单里登记,系统就不认你。而设备节点,就是用户空间访问这个户口的“门牌号”。

4.1 核心API:video_register_device

这个函数,是咱们驱动注册的入口。它的原型长这样:

int video_register_device(struct video_device *vdev, 
                          int type, 
                          int nr);

三个参数,我一个个说清楚。

  • vdev:你上一章辛辛苦苦初始化好的video_device结构体指针。注意,它里面的v4l2_dev、queue、fops这些成员必须已经赋值好了,不然注册会失败。
  • type:设备类型。对于Camera来说,绝大多数情况是VFL_TYPE_VIDEO。这决定了你的设备节点是/dev/videoX还是/dev/vbiX之类的。
  • nr:设备编号。你可以指定一个固定的数字,比如传0,那节点就是/dev/video0。如果传-1,内核会自动分配一个空闲的编号。

嗯,这里要注意:千万别传一个已经被占用的编号。我在项目中遇到过,有人图省事直接写死nr = 0,结果板子上已经有别的摄像头占了video0,注册直接返回-EBUSY。后来我改成传-1,让内核去管,世界清净了。

我的建议: 除非你有明确的固定节点需求(比如某些ISP硬件要求),否则一律传-1。让内核动态分配,省心省力。

4.2 注册背后的那些事

你调用video_register_device时,内核到底干了啥?我简单梳理一下:

  1. 检查vdev的合法性,比如fops不能为空,v4l2_dev不能为空。
  2. 根据type和nr,在/dev下创建对应的设备节点。比如type是VFL_TYPE_VIDEO,nr是2,那节点就是/dev/video2
  3. 把这个设备挂到V4L2的设备链表里,这样用户空间通过open("/dev/video2")就能找到你的驱动。
  4. 初始化一些内核内部的锁和等待队列。

你想想看,这一步其实挺重的。所以注册失败时,一定要检查返回值。常见的错误码有:

错误码 含义 常见原因
-ENOMEM 内存不足 内核堆空间不够,或者你分配的video_device太大
-EBUSY 设备号冲突 你指定的nr已经被占用了
-EINVAL 参数无效 type传错了,或者vdev里有空指针
我曾经踩过的坑: 注册成功后,千万别急着在注册函数后面直接操作硬件。因为此时设备节点刚创建,用户空间的udev可能还没处理完权限问题。最好等probe函数完全返回,再让应用层去打开设备。

4.3 设备节点创建的细节

设备节点,就是/dev/videoX这个文件。它的创建过程,其实是内核的device_create机制在背后工作。V4L2框架封装了一层,让你不用直接跟class_create打交道。

但有个细节很多人会忽略:设备节点的权限和所属组。默认情况下,/dev/videoX的权限是0600,属主是root。这意味着普通用户打不开你的摄像头。

我一般会在驱动里加一个udev规则,或者在内核配置里打开CONFIG_VIDEO_FIXED_MINOR_RANGES,然后手动调整权限。不过更常见的做法是,在应用层通过chmod或者udev规则来解决。

举个例子,如果你的系统用udev,可以写个规则文件:

SUBSYSTEM=="video4linux", GROUP="video", MODE="0660"

这样所有/dev/videoX节点都属于video组,权限是0660。把用户加到video组里,就能用了。

4.4 注销:video_unregister_device

有注册就有注销。这个函数相对简单:

void video_unregister_device(struct video_device *vdev);

它做的事情和注册相反:

  • 从V4L2设备链表中移除该设备。
  • 删除/dev/videoX节点。
  • 唤醒所有正在等待这个设备的进程(比如正在阻塞的poll)。
  • 标记设备为“已注销”状态。

这里有个关键点:注销后,video_device结构体并不会被释放。它只是从系统里“下线”了。如果你用的是动态分配的video_device,记得在注销后调用video_device_release来释放内存。

重要: 注销函数应该在驱动的remove回调里调用。而且,要确保在注销之前,所有已经打开的fd都被关闭了。否则用户空间拿着一个已经注销的fd做操作,会得到-ENODEV

4.5 一个完整的注册与注销示例

说了这么多,不如直接看代码。这是我常用的模板:

static int mycam_probe(struct platform_device *pdev)
{
    struct video_device *vdev;
    int ret;

    // 分配video_device
    vdev = video_device_alloc();
    if (!vdev)
        return -ENOMEM;

    // 初始化...(省略fops、queue等设置)

    // 注册
    ret = video_register_device(vdev, VFL_TYPE_VIDEO, -1);
    if (ret) {
        video_device_release(vdev);
        return ret;
    }

    platform_set_drvdata(pdev, vdev);
    return 0;
}

static int mycam_remove(struct platform_device *pdev)
{
    struct video_device *vdev = platform_get_drvdata(pdev);

    // 注销
    video_unregister_device(vdev);
    // 释放内存
    video_device_release(vdev);

    return 0;
}

你看,结构很清晰。probe里分配、初始化、注册;remove里注销、释放。顺序不能乱。

我个人习惯在注册成功后,打印一条日志:

dev_info(&pdev->dev, "Registered %s\n", video_device_node_name(vdev));

这样调试时一眼就能看到设备节点是哪个,方便。

4.6 避坑指南

最后,分享几个我实际项目中遇到的坑:

  • 重复注册:别在probe里调用两次video_register_device。内核会报错,而且第二次会失败。
  • 注销顺序:如果驱动里有中断处理程序或者workqueue,一定要在注销设备之前把它们停掉。否则注销后中断来了,访问已经释放的video_device,直接内核panic。
  • 设备节点残留:如果驱动异常卸载,/dev/videoX节点可能没被清理。重启系统或者手动rmmodinsmod一般能解决。但最好在驱动里做好错误处理。

嗯,这一章的内容就这些。设备注册与注销,说白了就是一对“创建”和“销毁”的操作。但细节决定成败,尤其是资源释放的顺序,搞不好就是内核崩溃。下一章我们聊聊更刺激的——buffer管理,那才是Camera驱动的核心战场。