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) + 具体硬件驱动
这三层的关系,我画了张图,你看一眼就明白了:
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(),这背后做了三件事:
- 分配次设备号 — 从位图里找一个空闲的号
- 创建cdev — 初始化字符设备,绑定
v4l2_fops文件操作集 - 添加到系统 — 调用
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管理时,你会发现所有操作都建立在这个基础之上。