3. V4L2设备注册:video_device结构体、v4l2_device注册流程、设备节点创建

在V4L2框架中,设备注册是驱动与用户空间建立通信通道的核心步骤。本章深入剖析video_device结构体的设计、v4l2_device的注册流程以及设备节点(/dev/videoX)的创建机制,帮助开发者理解从内核对象到用户空间文件节点的完整链路。

3.1 video_device结构体深度解析

struct video_device是V4L2驱动中最核心的数据结构之一,它抽象了一个视频设备节点在内核中的表示。该结构体不仅包含设备属性、操作函数集,还负责与V4L2核心框架交互。

关键字段速查表
字段名 类型 说明
fops const struct v4l2_file_operations * 设备文件操作函数集(open、close、ioctl等)
v4l2_dev struct v4l2_device * 指向所属的顶层V4L2设备实例
ctrl_handler struct v4l2_ctrl_handler * 关联的控制处理器,管理曝光、增益等参数
queue struct vb2_queue * 指向videobuf2队列,管理帧缓冲区
prio struct v4l2_prio_state 优先级状态管理,防止多进程冲突
dev struct device * 内核通用设备结构体,用于设备模型
index int 设备节点编号(如video0中的0)
minor int 次设备号,由V4L2核心分配
flags unsigned long 设备标志位(如V4L2_FL_REGISTERED)

以下是一个典型的video_device初始化代码片段,展示了如何填充关键字段:

static struct video_device vdev_template = {
    .name = "MIPI Camera",
    .vfl_dir = VFL_DIR_RX,          // 接收方向(摄像头)
    .fops = &camera_fops,           // 文件操作函数集
    .ioctl_ops = &camera_ioctl_ops, // V4L2 ioctl操作集
    .release = video_device_release,
    .device_caps = V4L2_CAP_VIDEO_CAPTURE |
                   V4L2_CAP_STREAMING |
                   V4L2_CAP_READWRITE,
};
最佳实践:建议使用video_device_alloc()动态分配video_device,而非静态定义。动态分配可以避免生命周期管理问题,并支持引用计数。

3.2 v4l2_device注册流程

v4l2_device是V4L2驱动的顶层容器,它管理一个或多个video_device子设备。注册流程遵循“先父后子”的原则,即先注册v4l2_device,再注册其下的video_device。

3.2.1 注册步骤清单

  1. 分配并初始化v4l2_device:调用v4l2_device_alloc()或静态初始化,设置名称、父设备指针。
  2. 注册v4l2_device:调用v4l2_device_register(struct device *parent, struct v4l2_device *v4l2_dev),将设备注册到内核设备模型。
  3. 初始化video_device:填充video_device结构体,关联v4l2_dev指针。
  4. 注册video_device:调用video_register_device(struct video_device *vdev, int type, int nr),创建设备节点。
  5. 错误处理与清理:注册失败时依次释放已分配的资源。
核心API调用序列
struct v4l2_device *v4l2_dev;
struct video_device *vdev;
int ret;

// 1. 分配v4l2_device
v4l2_dev = kzalloc(sizeof(*v4l2_dev), GFP_KERNEL);
if (!v4l2_dev)
    return -ENOMEM;

// 2. 注册v4l2_device
ret = v4l2_device_register(&client->dev, v4l2_dev);
if (ret < 0) {
    dev_err(&client->dev, "Failed to register v4l2_device\n");
    goto err_free_v4l2;
}

// 3. 分配并初始化video_device
vdev = video_device_alloc();
if (!vdev) {
    ret = -ENOMEM;
    goto err_unregister_v4l2;
}
*vdev = vdev_template;
vdev->v4l2_dev = v4l2_dev;
vdev->queue = &queue;  // 关联vb2队列

// 4. 注册video_device
ret = video_register_device(vdev, VFL_TYPE_VIDEO, -1);
if (ret < 0) {
    dev_err(&client->dev, "Failed to register video device\n");
    goto err_free_vdev;
}

// 保存私有数据引用
video_set_drvdata(vdev, sensor);

3.2.2 注册流程图

v4l2_device 注册流程 1. 分配并初始化 v4l2_device 2. v4l2_device_register() 3. 初始化 video_device 结构体 4. video_register_device() 5. 创建 /dev/videoX 节点 关键检查点 • v4l2_dev 非空 • fops 已赋值 • queue 已关联 错误处理 • 失败时调用 video_unregister_device()

3.3 设备节点创建机制

当调用video_register_device()时,V4L2核心框架自动完成以下工作:

  • 分配次设备号:从V4L2动态次设备号池中分配一个空闲的minor号。
  • 创建设备文件:通过device_create()在/dev目录下创建videoX节点,主设备号为VIDEO_MAJOR(81)。
  • 关联file_operations:将video_device->fops注册到内核VFS层。
  • 添加到设备列表:将设备加入video_device_list全局链表,便于查找。

3.3.1 设备节点命名规则

VFL_TYPE 设备节点名称 典型用途
VFL_TYPE_VIDEO /dev/videoX 视频捕获/输出设备
VFL_TYPE_VBI /dev/vbiX 垂直消隐期数据
VFL_TYPE_RADIO /dev/radioX 无线电调谐器
VFL_TYPE_SUBDEV /dev/v4l-subdevX 子设备节点(MC架构)
VFL_TYPE_SDR /dev/swradioX 软件定义无线电
VFL_TYPE_TOUCH /dev/v4l-touchX 触摸传感器设备

3.3.2 设备节点创建内部流程

设备节点创建内部流程 用户空间应用 VFS 虚拟文件系统 V4L2 核心框架 驱动 video_register_device() 内部步骤 ① 检查 video_device 有效性(fops、v4l2_dev、queue 非空) ② 调用 get_index() 分配设备编号 index ③ 通过 device_create() 创建 /dev/videoX 节点 ④ 注册 cdev 字符设备,关联 fops ⑤ 设置 V4L2_FL_REGISTERED 标志位 ⑥ 添加到 video_device_list 全局链表 ⑦ 调用 v4l2_device->notify() 通知子设备

3.4 注册与注销的对称性

V4L2驱动必须保证注册与注销操作的对称性,否则会导致资源泄漏或内核崩溃。标准的注销流程如下:

static void camera_remove(struct i2c_client *client)
{
    struct camera_sensor *sensor = i2c_get_clientdata(client);

    // 1. 注销video_device(先注销子设备)
    video_unregister_device(sensor->vdev);

    // 2. 释放video_device
    video_device_release(sensor->vdev);

    // 3. 注销v4l2_device(后注销父设备)
    v4l2_device_unregister(sensor->v4l2_dev);

    // 4. 释放v4l2_device
    kfree(sensor->v4l2_dev);
}
常见错误:初学者容易忘记调用video_unregister_device()直接释放video_device,这会导致设备节点残留,用户空间仍可访问已释放的内存,引发use-after-free漏洞。务必遵循“先注销子设备,再注销父设备”的顺序。

3.5 多实例设备注册

对于支持多路摄像头的驱动,通常使用video_register_device()的nr参数控制设备编号:

  • nr = -1:由V4L2核心自动分配下一个可用编号。
  • nr >= 0:指定固定编号,若被占用则注册失败。
  • nr = V4L2_VIDEO_NR_AUTO:与-1行为相同,自动分配。

多实例注册示例:

for (i = 0; i < num_cameras; i++) {
    vdev = video_device_alloc();
    *vdev = vdev_template;
    vdev->v4l2_dev = v4l2_dev;
    vdev->queue = &cameras[i]->queue;
    vdev->index = i;  // 记录实例编号

    ret = video_register_device(vdev, VFL_TYPE_VIDEO, -1);
    if (ret < 0) {
        dev_err(dev, "Failed to register camera %d\n", i);
        goto err_rollback;
    }
    cameras[i]->vdev = vdev;
}
调试技巧:注册成功后,可以通过ls -l /dev/video*查看设备节点。使用cat /sys/class/video4linux/videoX/name可以读取video_device->name字段,验证设备是否正确注册。

3.6 本章小结