4、V4L2设备注册与注销:video_register_device、video_unregister_device实战

好,咱们进入第四章。前面几章我们把 V4L2 的框架结构、数据结构理了一遍,算是搭好了认知的骨架。这一章,咱们要让这个骨架真正「活」起来——说白了,就是怎么把摄像头设备注册到内核里,让用户空间能通过 /dev/videoX 找到它;以及怎么在驱动卸载时,干净地把它注销掉。

我刚开始写摄像头驱动时,觉得注册不就是调个函数嘛,有什么难的?结果第一次调试,设备节点死活不出来,dmesg 里也没报错。后来才发现,是注册时机和数据结构初始化顺序搞反了。嗯,这种坑,咱们今天一并填上。

4.1 注册前的准备工作

调用 video_register_device 之前,有几件事必须做扎实。我个人习惯,把这一步叫做「三件套」:

  1. 分配并初始化 video_device 结构体
  2. 填充 v4l2_file_operations 和 v4l2_ioctl_ops
  3. 设置 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 注册与注销的完整生命周期

为了让你更直观地理解整个过程,我画了一张流程图:

V4L2 设备注册与注销生命周期 video_device_alloc 初始化字段/fops/ioctl video_register_device 设备节点 /dev/videoX 可用 用户打开设备 引用计数 +1 video_unregister_device 设备节点立即移除 release 回调(引用归零) 关键点 • 注销 ≠ 释放 • 节点移除是同步的 • 结构体释放是异步的 • 别手动调 release

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,那就尴尬了。