3. V4L2设备注册与注销:设备节点与主/次设备号

V4L2(Video for Linux 2)框架的核心任务之一是将摄像头硬件抽象为标准的字符设备,供用户空间通过 open()、ioctl() 等系统调用访问。本章深入剖析设备注册与注销的底层机制,包括 video_register_device 与 video_unregister_device 的调用流程、设备节点在 /dev 下的创建规则,以及主设备号与次设备号的分配策略。

3.1 设备注册核心函数:video_register_device

video_register_device 是 V4L2 驱动向内核注册视频设备的入口。该函数完成以下关键步骤:

  1. 分配次设备号:从 V4L2 管理的次设备号池中获取一个空闲编号。
  2. 创建字符设备:调用 cdev_add 将设备添加到内核字符设备层。
  3. 生成设备节点:通过 device_create 在 /dev 下创建 videoX 节点。
  4. 关联 file_operations:将 V4L2 标准的 v4l2_fops 绑定到该设备。
函数原型:
int video_register_device(struct video_device *vdev, 
                          enum v4l2_device_type type, 
                          int nr);
  • vdev:已初始化的 video_device 结构体指针。
  • type:设备类型,如 VFL_TYPE_VIDEO(视频捕获)、VFL_TYPE_VBI(VBI 数据)、VFL_TYPE_RADIO(收音机)。
  • nr:期望的次设备号,传入 -1 表示由内核自动分配。

3.2 设备注销核心函数:video_unregister_device

当驱动卸载或设备热拔出时,必须调用 video_unregister_device 执行清理:

  1. 移除设备节点:删除 /dev/videoX 文件。
  2. 销毁字符设备:调用 cdev_del 从内核中移除该设备。
  3. 释放次设备号:将次设备号归还给 V4L2 管理池。
  4. 通知用户空间:通过 uevent 机制广播设备移除事件。
注意: 注销操作必须在所有用户空间文件描述符关闭之后进行,否则可能导致内核崩溃。驱动应在 release() 回调中确保引用计数归零后再调用注销函数。

3.3 设备节点创建机制

V4L2 设备节点位于 /dev 目录下,命名规则为 videoX(X 为次设备号)。节点创建由内核的 device_create 函数完成,该函数在 sysfs 中创建对应的设备目录,并生成 devtmpfs 节点。

设备类型 节点名称 主设备号 次设备号范围
视频捕获/输出 /dev/video0 ~ /dev/video63 81 0 ~ 63
VBI 数据 /dev/vbi0 ~ /dev/vbi31 81 224 ~ 255
收音机 /dev/radio0 ~ /dev/radio63 81 64 ~ 127
软件定义无线电 (SDR) /dev/swradio0 ~ /dev/swradio15 81 192 ~ 207
提示: 所有 V4L2 设备共享同一个主设备号 81,通过次设备号区分不同类型和实例。这种设计简化了内核的设备管理,同时保持了用户空间接口的一致性。

3.4 主设备号与次设备号分配策略

V4L2 框架使用静态分配的次设备号范围来区分设备类型。主设备号 81 在 Linux 内核中预留给 V4L2 使用,驱动开发者无需动态申请。

次设备号的分配遵循以下规则:

  • 自动分配:调用 video_register_device 时传入 nr = -1,内核从对应类型的空闲号池中选取最小的可用编号。
  • 固定分配:传入非负整数 nr,内核尝试使用该编号,若已被占用则返回 -EBUSY。
  • 类型隔离:每种设备类型拥有独立的次设备号区间,互不重叠。
V4L2 设备注册与注销完整流程 驱动 probe() 入口 video_device 分配与初始化 video_register_device() 分配次设备号 cdev_add 创建设备 device_create 创建节点 video_unregister_device()

3.5 完整注册与注销代码示例

以下代码展示了一个典型的 V4L2 摄像头驱动中设备注册与注销的实现模式:

#include <media/v4l2-device.h>
#include <media/v4l2-dev.h>
#include <media/videobuf2-v4l2.h>

static struct video_device *vdev;

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

    /* 1. 分配 video_device 结构体 */
    vdev = video_device_alloc();
    if (!vdev)
        return -ENOMEM;

    /* 2. 设置标准 V4L2 文件操作 */
    vdev->fops = &mycam_fops;
    vdev->v4l2_dev = &mycam_v4l2_dev;
    vdev->queue = &mycam_queue;
    strscpy(vdev->name, "MyCamera", sizeof(vdev->name));

    /* 3. 注册设备,自动分配次设备号 */
    ret = video_register_device(vdev, VFL_TYPE_VIDEO, -1);
    if (ret < 0) {
        video_device_release(vdev);
        return ret;
    }

    dev_info(&pdev->dev, "Registered /dev/video%d\n", vdev->num);
    return 0;
}

static int mycam_remove(struct platform_device *pdev)
{
    /* 注销设备,自动清理节点和次设备号 */
    video_unregister_device(vdev);
    video_device_release(vdev);
    return 0;
}

3.6 常见错误与调试方法

错误现象 可能原因 排查方法
/dev/videoX 未创建 次设备号耗尽或 device_create 失败 检查 dmesg 中的错误信息;确认 CONFIG_DEVTMPS 已启用
注册返回 -ENOMEM 内存分配失败或 cdev_add 资源不足 使用 slabtop 检查内核内存使用情况
注销时系统崩溃 设备仍在被用户空间使用 在注销前调用 v4l2_device_disconnect 阻止新访问
次设备号冲突 多个驱动使用了相同的固定次设备号 统一使用 nr = -1 让内核自动分配
关键调试命令:
# 查看所有 V4L2 设备节点
ls -la /dev/video*

# 查看设备主次设备号
cat /proc/devices | grep video

# 查看 sysfs 中的设备信息
udevadm info --query=all --name=/dev/video0

# 实时监控设备注册事件
udevadm monitor --kernel --property

3.7 设备注册的生命周期管理

V4L2 设备从注册到注销经历以下状态转换:

  • 未注册状态:video_device 已分配但未调用 video_register_device,用户空间无法访问。
  • 已注册状态:设备节点存在,用户空间可打开设备进行 IOCTL 操作。
  • 断开状态:调用 v4l2_device_disconnect 后,新 open() 调用返回错误,但已有文件描述符仍可继续操作。
  • 已注销状态:video_unregister_device 执行完毕,设备节点消失,所有资源释放。
V4L2 设备生命周期状态机 未注册 register 已注册 disconnect 已断开 unregister 已注销 释放 video_device 后回到未注册

3.8 多设备实例管理

当驱动需要管理多个摄像头时,每个实例独立调用 video_register_device,内核自动分配不同的次设备号:

#define MAX_CAMERAS 4
static struct video_device *cam_devs[MAX_CAMERAS];

static int probe_all_cameras(struct platform_device *pdev)
{
    int i, ret;

    for (i = 0; i < MAX_CAMERAS; i++) {
        cam_devs[i] = video_device_alloc();
        if (!cam_devs[i]) {
            ret = -ENOMEM;
            goto err_cleanup;
        }

        /* 每个设备独立配置 */
        cam_devs[i]->fops = &mycam_fops;
        snprintf(cam_devs[i]->name, sizeof(cam_devs[i]->name),
                 "MyCamera-%d", i);

        ret = video_register_device(cam_devs[i], VFL_TYPE_VIDEO, -1);
        if (ret < 0) {
            video_device_release(cam_devs[i]);
            goto err_cleanup;
        }

        dev_info(&pdev->dev, "Camera %d: /dev/video%d\n",
                 i, cam_devs[i]->num);
    }
    return 0;

err_cleanup:
    while (--i >= 0) {
        video_unregister_device(cam_devs[i]);
        video_device_release(cam_devs[i]);
    }
    return ret;
}
最佳实践: 在 probe() 函数中,建议先分配所有资源,最后再调用 video_register_device。这样如果中途出错,可以避免已注册的设备节点暴露给用户空间,防止出现不完整设备状态。