3. V4L2设备注册与注销:video_register_device、video_unregister_device接口详解及驱动加载流程

在V4L2框架中,摄像头驱动的核心任务之一是将抽象的视频设备节点(如 /dev/video0)与具体的硬件操作绑定。本章深入剖析设备注册与注销的两个核心API——video_register_device 与 video_unregister_device,并梳理从驱动入口到设备节点生成的完整加载流程。

3.1 核心数据结构:struct video_device

在调用注册接口之前,驱动必须先初始化一个 struct video_device 实例。该结构体是V4L2设备在内核中的抽象,包含了设备号、文件操作集、控制句柄等关键成员。

成员 类型 说明
fops const struct v4l2_file_operations * 设备文件操作集(open、release、ioctl等)
v4l2_dev struct v4l2_device * 指向父级V4L2设备实例
ctrl_handler struct v4l2_ctrl_handler * 控制句柄,用于管理曝光、白平衡等参数
queue struct vb2_queue * 视频缓冲队列,用于帧数据管理
minor int 次设备号,注册时由内核分配
num int 设备编号(如video0中的0)
name char[32] 设备名称,显示在 /sys 和 dmesg 中
最佳实践:推荐使用 video_device_alloc() 动态分配 struct video_device,而非静态定义。这可以确保设备引用计数正确管理,避免在注销时出现use-after-free问题。

3.2 video_register_device 接口详解

video_register_device 是V4L2设备注册的核心入口,其原型定义在 include/media/v4l2-dev.h 中:

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

参数说明:

  • vdev:已初始化的视频设备结构体指针。
  • type:设备类型,常用值包括:
    • VFL_TYPE_VIDEO —— 标准视频输入/输出设备(/dev/videoX)
    • VFL_TYPE_VBI —— 垂直消隐区间设备(/dev/vbiX)
    • VFL_TYPE_RADIO —— 无线电设备(/dev/radioX)
  • nr:期望的设备编号。传入 -1 表示由内核自动分配。

返回值:成功返回0;失败返回负的错误码(如 -ENOMEM、-ENODEV)。

内部执行流程:

  1. 检查 vdev 的 fops 和 v4l2_dev 是否有效。
  2. 分配次设备号(minor),并创建 /dev/videoX 设备节点。
  3. 初始化 cdev 字符设备,绑定 v4l2_fops 作为通用文件操作入口。
  4. 将设备添加到V4L2框架的内部设备链表,并注册到sysfs文件系统。
  5. 调用 v4l2_device_register_subdev_nodes 创建子设备节点(如果存在subdev)。

3.3 video_unregister_device 接口详解

当驱动卸载或设备热拔出时,必须调用 video_unregister_device 来释放资源:

void video_unregister_device(struct video_device *vdev);

执行要点:

  • 该函数会立即移除 /dev/videoX 设备节点,阻止新的用户空间访问。
  • 等待所有正在进行的文件操作(open、ioctl、mmap等)完成。
  • 释放次设备号,并从sysfs和内部链表中移除设备。
  • 注意:该函数不会释放 struct video_device 本身的内存。驱动需在之后调用 video_device_release() 或 kfree() 完成最终清理。
常见错误:在 video_unregister_device 之后立即释放 video_device 结构体,但仍有用户空间进程持有文件描述符。这会导致内核崩溃。正确的做法是在release回调中通过引用计数延迟释放。

3.4 驱动加载与设备注册完整流程

下图展示了从驱动入口函数到设备节点可用的完整调用链:

V4L2驱动加载与设备注册流程 V4L2驱动加载与设备注册流程 module_init / probe video_device_alloc() 初始化 fops / v4l2_dev / queue video_register_device() /dev/videoX 就绪 关键检查点 • fops 不能为 NULL • v4l2_dev 必须已注册 • queue 需提前初始化 注销流程 video_unregister_device() → video_device_release()

3.5 驱动卸载与设备注销流程

设备注销是注册的逆过程,通常在 remove 回调中执行:

static int mycam_remove(struct platform_device *pdev)
{
    struct mycam_dev *cam = platform_get_drvdata(pdev);

    // 1. 停止视频流
    vb2_video_unregister_device(&cam->vdev);

    // 2. 注销V4L2设备
    video_unregister_device(&cam->vdev);

    // 3. 释放V4L2设备内存
    video_device_release(&cam->vdev);

    // 4. 注销V4L2父设备
    v4l2_device_unregister(&cam->v4l2_dev);

    return 0;
}

注销顺序的重要性:

  • 必须先停止缓冲队列(vb2_queue_release),再注销设备节点。
  • video_unregister_device 只移除用户空间可见的设备文件,不释放内存。
  • video_device_release 最终释放 struct video_device 结构体。
  • 父设备 v4l2_device 必须在所有子设备注销后才能释放。

3.6 完整驱动模板示例

以下是一个简化的摄像头驱动加载/卸载骨架,展示了上述API的典型用法:

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

struct mycam_dev {
    struct v4l2_device   v4l2_dev;
    struct video_device  vdev;
    struct vb2_queue     queue;
    /* 硬件相关成员 */
};

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

    cam = devm_kzalloc(&pdev->dev, sizeof(*cam), GFP_KERNEL);
    if (!cam)
        return -ENOMEM;

    /* 1. 注册V4L2父设备 */
    ret = v4l2_device_register(&pdev->dev, &cam->v4l2_dev);
    if (ret)
        return ret;

    /* 2. 初始化vb2队列 */
    cam->queue.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    cam->queue.io_modes = VB2_MMAP | VB2_USERPTR;
    ret = vb2_queue_init(&cam->queue);
    if (ret)
        goto err_v4l2;

    /* 3. 初始化video_device */
    cam->vdev.v4l2_dev = &cam->v4l2_dev;
    cam->vdev.queue    = &cam->queue;
    cam->vdev.fops     = &mycam_fops;
    cam->vdev.release  = video_device_release_empty;
    strscpy(cam->vdev.name, "MyCamera", sizeof(cam->vdev.name));

    /* 4. 注册V4L2设备节点 */
    ret = video_register_device(&cam->vdev, VFL_TYPE_VIDEO, -1);
    if (ret)
        goto err_queue;

    platform_set_drvdata(pdev, cam);
    return 0;

err_queue:
    vb2_queue_release(&cam->queue);
err_v4l2:
    v4l2_device_unregister(&cam->v4l2_dev);
    return ret;
}

static int mycam_remove(struct platform_device *pdev)
{
    struct mycam_dev *cam = platform_get_drvdata(pdev);

    video_unregister_device(&cam->vdev);
    vb2_queue_release(&cam->queue);
    v4l2_device_unregister(&cam->v4l2_dev);

    return 0;
}

3.7 常见错误与调试技巧

错误现象 可能原因 解决方案
dmesg 显示 "videodev: bad file operations" vdev->fops 为 NULL 或关键回调未实现 检查 v4l2_file_operations 结构体是否完整初始化
设备节点未创建 v4l2_device_register 未调用或失败 确认父设备注册成功,检查 v4l2_dev 指针
卸载时系统崩溃 video_unregister_device 后未等待引用计数归零 使用 video_device_release 回调管理生命周期
设备编号冲突 多个驱动使用相同的 nr 参数 传入 -1 让内核自动分配

调试建议:在 probe 和 remove 函数中添加 dev_info 打印,输出 video_device 的 minor 和 num 值。配合 ls -l /dev/video* 和 cat /sys/class/video4linux/videoX/name 可以快速验证设备注册状态。

3.8 本章小结

  • video_register_device 是V4L2设备注册的核心API,负责创建设备节点、绑定文件操作、注册到sysfs。
  • video_unregister_device 执行逆操作,但内存释放需由驱动通过 video_device_release 完成。
  • 驱动加载流程遵循:分配结构体 → 初始化成员 → 注册父设备 → 注册子设备节点的顺序。
  • 注销流程必须严格逆序,并确保所有用户空间引用已释放。
  • 使用 -1 作为设备编号参数可避免手动管理设备号冲突。