2、Video设备节点:深入理解/dev/videoX的设备模型、主次设备号与cdev注册流程

做V4L2开发,你每天都会跟/dev/video0、/dev/video1打交道。但说真的,很多人用了好几年,也没搞懂这个节点到底是怎么冒出来的。

我刚开始接触V4L2时也这样。那时候调一个USB摄像头驱动,死活找不到设备节点。后来一查,原来是cdev注册的时机不对。从那以后,我就把设备模型这部分啃了个透。

今天咱们就把/dev/videoX的来龙去脉讲清楚。你想想看,一个摄像头插上USB口,从硬件识别到用户空间出现/dev/video0,中间到底经历了什么?

2.1 设备模型的三层架构

Linux设备模型不是拍脑袋想出来的。它分三层:总线层、设备层、驱动层。V4L2的视频设备节点,就挂在这套体系上。

核心要点:V4L2设备节点 = 字符设备(cdev) + 视频设备(video_device) + 具体硬件驱动

这三层的关系,我画了张图,你看一眼就明白了:

V4L2 设备节点三层架构 用户空间:/dev/video0 内核字符设备层 (cdev) 主设备号 81 | 次设备号 0~63 V4L2 核心层 (video_device / videodev) video_register_device() → 分配次设备号 + 创建cdev 具体驱动层 (如 uvcvideo / vimc / 平台摄像头驱动) 填充 v4l2_device / video_device 结构体 → 注册 驱动注册 video_device → V4L2核心分配次设备号 → cdev关联 → udev创建 /dev/videoX

2.2 主次设备号:81号背后的故事

V4L2视频设备的主设备号是81。这个号是内核社区分配的,所有视频采集设备共用。那怎么区分不同的摄像头?靠次设备号。

设备类型 主设备号 次设备号范围 典型节点
视频采集 81 0-63 /dev/video0 ~ /dev/video63
视频输出 81 64-127 /dev/video64 ~ /dev/video127
视频元数据 81 128-191 /dev/video128 ~ /dev/video191
视频叠加 81 192-255 /dev/video192 ~ /dev/video255

我个人的习惯:调试时先用 cat /proc/devices | grep video 确认主设备号,再用 ls -l /dev/video* 看次设备号。这样能快速判断驱动是否注册成功。

次设备号的分配不是随机的。V4L2核心维护了一个位图,每分配一个次设备号,就在位图里标记一下。释放时再清掉。这样能保证不冲突。

2.3 cdev注册流程:从video_device到设备节点

驱动里调用video_register_device(),这背后做了三件事:

  1. 分配次设备号 — 从位图里找一个空闲的号
  2. 创建cdev — 初始化字符设备,绑定v4l2_fops文件操作集
  3. 添加到系统 — 调用cdev_add(),让内核知道这个设备存在了

代码流程大致是这样:

// drivers/media/v4l2-core/v4l2-dev.c
int video_register_device(struct video_device *vdev, int type, int nr)
{
    // 1. 分配次设备号
    int minor = video_register_media_controller(vdev);
    
    // 2. 初始化cdev
    cdev_init(&vdev->cdev, &v4l2_fops);
    vdev->cdev.owner = vdev->fops->owner;
    
    // 3. 注册cdev
    ret = cdev_add(&vdev->cdev, 
                   MKDEV(VIDEO_MAJOR, vdev->minor), 1);
    
    // 4. 创建设备节点(由udev在用户空间完成)
    device_create(vdev->dev_parent, ...);
}

我曾经踩过的坑:cdev_add()之后,设备节点还没出现。很多人以为驱动加载完就能看到/dev/video0。其实cdev_add只是告诉内核有这个设备,真正的节点创建是udev监听到内核事件后做的。如果udev没起来,或者规则没配好,节点就是不出来。

2.4 v4l2_fops:文件操作背后的秘密

你打开/dev/video0,调用read()、write()、ioctl(),最终都会落到v4l2_fops上。这个结构体是V4L2核心定义好的:

static const struct file_operations v4l2_fops = {
    .owner      = THIS_MODULE,
    .open       = v4l2_open,
    .release    = v4l2_release,
    .read       = v4l2_read,
    .write      = v4l2_write,
    .unlocked_ioctl = v4l2_ioctl,
    .compat_ioctl   = v4l2_compat_ioctl32,
    .mmap       = v4l2_mmap,
    .poll       = v4l2_poll,
};

这些函数是V4L2核心的入口。它们会根据video_device里的v4l2_file_operations,再分发到具体驱动。说白了,v4l2_fops是个。

2.5 实战:手动创建一个video设备节点

有时候调试需要手动创建设备节点。比如嵌入式板子上udev没跑,或者你想测试驱动是否正常。命令很简单:

# 手动创建 /dev/video0,主设备号81,次设备号0
mknod /dev/video0 c 81 0

# 验证
ls -l /dev/video0
# 输出: crw-rw---- 1 root video 81, 0 ...

但要注意,手动创建的节点没有udev的权限管理。我建议你只在调试时用,产品上还是老老实实配好udev规则。

2.6 设备节点的生命周期

一个/dev/videoX从生到死,经历这几个阶段:

阶段 触发动作 关键函数/事件
1. 驱动加载 insmod / 设备插入 probe() → video_register_device()
2. 节点创建 内核发出uevent udev 监听 → mknod /dev/videoX
3. 应用打开 open("/dev/video0") v4l2_open() → 驱动open回调
4. 数据流 ioctl/mmap/poll V4L2核心分发到驱动
5. 应用关闭 close() v4l2_release() → 驱动release回调
6. 节点销毁 驱动卸载 / 设备拔出 video_unregister_device() → cdev_del()

记住:设备节点是用户空间和内核空间的桥梁。没有/dev/videoX,应用程序就碰不到摄像头硬件。而这座桥的搭建,全靠cdev注册流程。

2.7 调试技巧:用sysfs看设备关系

我调试驱动时,经常用sysfs来确认设备节点的归属。比如:

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

# 或者直接看sysfs
ls -l /sys/class/video4linux/video0/
cat /sys/class/video4linux/video0/name
cat /sys/class/video4linux/video0/index

这些信息能告诉你:这个video设备对应哪个硬件、是第几个注册的、名字是什么。有一次我发现两个摄像头都注册成了video0,一查sysfs,原来是驱动里video_register_device()传的nr参数写死了。

避坑指南:写驱动时,video_register_device()的nr参数传-1,让内核自动分配次设备号。除非你有特殊需求,否则别手动指定。我曾经见过一个驱动写死了nr=0,结果插两个摄像头时,第二个注册失败。

嗯,设备节点这部分就讲到这里。你理解了cdev注册流程,就掌握了V4L2设备模型的根基。后面讲buffer管理时,你会发现所有操作都建立在这个基础之上。