3. V4L2设备注册与注销:video_register_device、video_unregister_device的调用流程与注意事项

好,咱们今天聊聊V4L2设备注册和注销。说实话,这块儿看起来就是两个函数调用,但坑是真不少。我早期做摄像头驱动的时候,就因为在注册时机上栽过跟头——设备节点创建了,但硬件还没准备好,用户空间一打开就崩。嗯,咱们今天就把这两个函数彻底讲透。

3.1 video_device结构体:你得先把它填对

注册之前,你得先有一个struct video_device。这个结构体是V4L2框架的核心,说白了就是用户空间看到的那个/dev/videoX节点背后的数据结构。

struct video_device {
    const struct v4l2_file_operations *fops;
    struct v4l2_ioctl_ops *ioctl_ops;
    struct device dev;
    struct v4l2_device *v4l2_dev;
    unsigned long flags;
    int minor;
    int index;
    // ... 还有不少字段
};

我个人习惯,分配这个结构体用video_device_alloc(),而不是直接kzalloc。为什么?因为video_device_alloc会帮你初始化引用计数和锁,省得你后面手动搞。当然,如果你用devm_系列接口,也可以直接用devm_kzalloc,但记得手动调用video_device_release回调。

关键字段设置:

  • v4l2_dev:指向父V4L2设备,必须设置,否则注册会失败
  • fops:文件操作集,至少要实现open、release、ioctl
  • ioctl_ops:V4L2 ioctl操作集,这是核心
  • flags:常用V4L2_FL_REGISTERED,表示设备已注册
  • minor:一般设为-1,让内核自动分配

3.2 video_register_device:调用流程与时机

函数原型长这样:

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

三个参数:

  • vdev:你填好的video_device指针
  • type:设备类型,比如VFL_TYPE_VIDEO、VFL_TYPE_VBI、VFL_TYPE_SUBDEV
  • nr:设备编号,传-1让内核自动分配,传0-63可以指定

调用流程大致如下:

  1. 检查vdev和v4l2_dev是否有效
  2. 分配minor号(如果nr为-1)
  3. 创建/dev/videoX设备节点
  4. 初始化cdev和device结构
  5. 设置V4L2_FL_REGISTERED标志
  6. 调用v4l2_device_register_subdev_nodes(如果有子设备)

这里有个我踩过的坑:注册时机。你想想看,video_register_device一调用,用户空间就能打开这个设备了。如果你的硬件还没初始化完成,比如MIPI CSI的PHY还没起来,那用户空间一调用open或者ioctl,你的驱动可能就崩了。

我曾经犯过的错:在probe函数里先注册了video_device,再去初始化硬件。结果用户空间的应用程序在udev事件触发后立刻打开了设备,而此时我的MIPI DPHY还没准备好。从那以后,我都是先把硬件初始化完,最后一步才调用video_register_device。

3.3 注销流程:video_unregister_device

注销相对简单,但同样有讲究:

void video_unregister_device(struct video_device *vdev);

它会做这几件事:

  • 清除V4L2_FL_REGISTERED标志
  • 删除设备节点
  • 等待所有打开的文件描述符关闭
  • 释放minor号

注意:这个函数不会释放video_device结构体本身。你需要自己调用video_device_release或者video_device_put来释放内存。我见过有人只调了video_unregister_device,忘了释放内存,结果每次insmod/rmmod都泄漏一次。

我的建议:在remove函数里,先调用video_unregister_device,然后调用video_device_put。如果你用了devm_系列接口,可以注册一个devres回调来自动处理。

3.4 完整示例:注册与注销的典型流程

static int my_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 = &my_v4l2_dev;
    vdev->fops = &my_fops;
    vdev->ioctl_ops = &my_ioctl_ops;
    vdev->release = video_device_release;
    vdev->minor = -1;  // 自动分配
    snprintf(vdev->name, sizeof(vdev->name), "my_camera");

    // 3. 初始化硬件(先做!)
    ret = my_hardware_init(pdev);
    if (ret) {
        video_device_release(vdev);
        return ret;
    }

    // 4. 注册video设备(最后一步)
    ret = video_register_device(vdev, VFL_TYPE_VIDEO, -1);
    if (ret) {
        my_hardware_deinit(pdev);
        video_device_release(vdev);
        return ret;
    }

    platform_set_drvdata(pdev, vdev);
    return 0;
}

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

    // 先注销,再释放硬件
    video_unregister_device(vdev);
    my_hardware_deinit(pdev);
    // video_device_release 由 release 回调自动处理
    return 0;
}

3.5 注意事项与避坑指南

注意事项 说明
注册前硬件就绪 确保MIPI CSI PHY、时钟、电源等都已初始化完成
注销顺序 先注销video_device,再释放硬件资源
release回调 必须设置release回调,否则内核会WARN
并发访问 注册后用户空间可能立即访问,驱动要能处理这种情况
设备编号冲突 指定nr时确保不与其他设备冲突,建议用-1自动分配

核心原则:注册是最后一步,注销是第一步。这个顺序能避免绝大多数竞态问题。

3.6 核心流程图

下面我用SVG画了一张图,把注册和注销的完整流程展示出来。你一看就明白了:

V4L2设备注册与注销流程图 注册流程 1. 分配video_device结构体 2. 设置fops/ioctl_ops/v4l2_dev 3. 初始化硬件(MIPI CSI PHY等) 4. 调用video_register_device 5. 创建/dev/videoX节点 ⚠ 用户空间可立即访问 注销流程 1. 调用video_unregister_device 2. 删除/dev/videoX节点 3. 等待所有fd关闭 4. 释放硬件资源 5. 调用video_device_release ✓ 设备完全注销 卸载驱动

这张图把注册和注销的流程分左右两边展示。你看,注册是从上到下,硬件初始化在前,注册在后。注销也是从上到下,先注销设备,再释放硬件。这个顺序千万别搞反了。

一个小技巧:如果你在调试时发现video_register_device返回-ENOMEM,别急着怀疑内存不够。先检查一下你的v4l2_dev指针是不是NULL,或者minor号是不是被占用了。我遇到过好几次,都是因为忘了设置v4l2_dev。

好了,关于V4L2设备注册与注销,核心就是这些。记住:注册是最后一步,注销是第一步。这个原则能帮你避免90%的坑。剩下的10%,等你真正写代码的时候,咱们再慢慢聊。


可可精品资料库