2、Linux字符设备基础:主次设备号、file_operations结构、设备节点创建与udev规则
V4L2驱动本质上是一个字符设备驱动,它向用户空间暴露 /dev/video0、/dev/video1 等设备节点。用户程序通过 open()、ioctl()、mmap()、poll() 等标准系统调用与内核中的V4L2核心框架交互。在深入V4L2的复杂控制逻辑之前,必须先夯实字符设备的三块基石:设备号的分配与管理、file_operations 操作集的实现、以及设备节点从内核态到用户态的诞生过程(包含udev规则)。这三者构成了V4L2驱动与用户空间通信的物理与逻辑通道。
上图展示了字符设备从内核注册到用户空间访问的完整链路。内核侧通过 register_chrdev_region() 分配设备号,将 file_operations 绑定到 struct cdev,再通过 cdev_add() 将设备加入系统。用户侧通过 VFS 访问设备节点,而 udev 则负责在设备插入或驱动加载时自动创建对应的节点文件。下面我们逐一拆解每个环节。
2.1 主次设备号:设备在内核中的身份证
核心原理
Linux 中每个字符设备或块设备都由一对设备号唯一标识:主设备号(major number) 和 次设备号(minor number)。主设备号用于区分设备类型(即对应哪个驱动程序),次设备号用于区分同一驱动程序管理下的不同设备实例。在内核源码中,设备号使用 dev_t 类型表示(32位无符号整数),其中高12位为主设备号,低20位为次设备号。
对于 V4L2 设备,Linux 内核约定主设备号为 81(见 Documentation/admin-guide/devices.txt)。这意味着所有 video 设备(包括 video0、video1、vbi0、radio0 等)共享主设备号 81,通过次设备号区分具体设备。例如:
/dev/video0→ 主:次 = 81:0/dev/video1→ 主:次 = 81:1/dev/vbi0→ 主:次 = 81:224/dev/radio0→ 主:次 = 81:64
分配方式
驱动开发者有两种方式获取设备号:
- 静态分配:调用
register_chrdev_region(dev_t first, unsigned int count, const char *name),需要提前知道可用的设备号。适合设备数量固定且已知的场景。 - 动态分配:调用
alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, const char *name),由内核自动分配一个未使用的主设备号。适合设备数量不确定或驱动可加载多次的场景。
V4L2 框架内部通常使用动态分配,但主设备号固定为 81。实际上,V4L2 核心在 video_register_device() 中会调用 alloc_chrdev_region() 来获取次设备号,并确保主设备号为 81。
cat /proc/devices 查看当前系统已注册的设备号分配情况。如果看到 81 video 字样,说明 V4L2 设备驱动已成功注册。
判断方法
在驱动代码中,可以使用以下宏来操作设备号:
#include <linux/kdev_t.h>
dev_t devno = MKDEV(major, minor); // 由主次号合成 dev_t
int major = MAJOR(devno); // 从 dev_t 提取主设备号
int minor = MINOR(devno); // 从 dev_t 提取次设备号
在用户空间,可以通过 stat 命令查看设备节点的设备号:
$ stat -c "主设备号: %t 次设备号: %T" /dev/video0
主设备号: 51 次设备号: 0
注意:stat 输出的是十六进制,51 十六进制即 81 十进制。
2.2 file_operations 结构:驱动与内核的接口契约
核心原理
struct file_operations 是字符设备驱动最核心的数据结构,它定义了驱动向内核 VFS 层提供的所有操作函数指针。当用户程序对设备节点执行系统调用时,VFS 会根据 inode 中记录的 cdev 找到对应的 file_operations,并调用相应的函数。
对于 V4L2 驱动,video_device 结构体内部包含一个 fops 字段,类型为 struct file_operations。V4L2 核心框架已经实现了大部分通用操作,驱动开发者只需填充少量回调函数。典型的 V4L2 驱动 file_operations 如下:
static const struct file_operations v4l2_fops = {
.owner = THIS_MODULE,
.open = v4l2_open,
.release = v4l2_release,
.read = v4l2_read,
.write = v4l2_write,
.poll = v4l2_poll,
.unlocked_ioctl = v4l2_ioctl,
.mmap = v4l2_mmap,
.llseek = no_llseek,
};
这些函数中,v4l2_open、v4l2_ioctl、v4l2_mmap 等是 V4L2 核心框架提供的通用实现,它们内部会根据 video_device 的 ioctl_ops 分发到具体的驱动回调。驱动开发者通常只需要实现 struct v4l2_file_operations(注意与 file_operations 区分),它定义了更细粒度的 V4L2 操作:
struct v4l2_file_operations {
struct module *owner;
ssize_t (*read) (struct file *file, char __user *buf, size_t count, loff_t *ppos);
ssize_t (*write) (struct file *file, const char __user *buf, size_t count, loff_t *ppos);
unsigned int (*poll) (struct file *file, struct poll_table_struct *wait);
long (*unlocked_ioctl) (struct file *file, unsigned int cmd, unsigned long arg);
int (*mmap) (struct file *file, struct vm_area_struct *vma);
int (*open) (struct file *file);
int (*release) (struct file *file);
};
关键函数职责
| 函数 | 触发时机 | V4L2 驱动中的典型行为 |
|---|---|---|
open |
用户调用 open("/dev/video0") |
检查设备是否被占用(V4L2 默认独占),初始化私有数据结构,调用 v4l2_fh_open() |
unlocked_ioctl |
用户调用 ioctl(fd, VIDIOC_*, arg) |
V4L2 核心根据 cmd 分发到 video_device->ioctl_ops 中的对应处理函数 |
mmap |
用户调用 mmap() 映射视频缓冲区 |
调用 vb2_mmap() 将 DMA 缓冲区映射到用户空间 |
poll |
用户调用 select()/poll()/epoll() |
调用 vb2_poll() 检查是否有帧数据可读或缓冲区可写 |
release |
用户关闭文件描述符 | 停止视频流,释放缓冲区,调用 v4l2_fh_release() |
ioctl 字段已被 unlocked_ioctl 取代。驱动中不要再使用 .ioctl 字段,否则在 64 位系统上可能引发兼容性问题。V4L2 核心已经统一使用 unlocked_ioctl。
2.3 设备节点创建与 udev 规则
核心原理
设备节点(device node)是用户空间访问内核设备的文件入口。没有设备节点,即使驱动已加载,用户程序也无法通过 open() 访问设备。设备节点的创建有两种方式:
- 手动创建:使用
mknod命令,需要知道主次设备号。例如:mknod /dev/video0 c 81 0。这种方式不灵活,设备号变化时需要手动更新。 - udev 自动创建:udev 是 Linux 的设备管理器,它监听内核的 uevent 事件,根据规则自动创建设备节点、设置权限、创建符号链接等。这是现代 Linux 系统推荐的方式。
udev 工作流程
当 V4L2 驱动调用 video_register_device() 时,内核会生成一个 uevent 事件,包含以下环境变量:
ACTION=add
DEVPATH=/devices/platform/soc/2100000.bus/30c00000.csi/video4linux/video0
MAJOR=81
MINOR=0
DEVNAME=video0
SUBSYSTEM=video4linux
udev 守护进程收到该事件后,会依次匹配 /etc/udev/rules.d/ 和 /lib/udev/rules.d/ 下的规则文件。对于 V4L2 设备,常见的规则如下:
# /etc/udev/rules.d/99-v4l2.rules
# 为 video0 创建符号链接 /dev/v4l/by-id/video-usb-xxx
KERNEL=="video*", SUBSYSTEM=="video4linux", \
ATTR{name}=="USB Camera", \
SYMLINK+="v4l/by-id/video-usb-camera"
# 设置权限为 0666,允许所有用户访问
KERNEL=="video*", SUBSYSTEM=="video4linux", MODE="0666"
# 根据设备名称创建固定节点
KERNEL=="video*", SUBSYSTEM=="video4linux", \
ATTR{name}=="ov5640", SYMLINK+="v4l/camera0"
实战场景:嵌入式平台摄像头节点固定
在嵌入式 Linux 平台(如 i.MX8MP + OV5640 摄像头)上,系统可能同时存在多个视频设备:CSI 摄像头、ISP 输出、编码器输出等。默认情况下,内核注册顺序可能导致 /dev/video0 对应哪个物理设备不确定。例如:
/dev/video0→ ISP 输出(用于预览)/dev/video1→ CSI 原始数据/dev/video2→ 编码器输入
如果应用程序期望 /dev/video0 是 CSI 原始数据,就会出错。解决方案是编写 udev 规则,根据设备的属性(如 name、driver、of_node 等)创建稳定的符号链接:
# /etc/udev/rules.d/99-camera.rules
SUBSYSTEM=="video4linux", \
ATTR{name}=="ov5640", \
KERNEL=="video*", \
SYMLINK+="camera/csi0"
SUBSYSTEM=="video4linux", \
ATTR{name}=="isp", \
KERNEL=="video*", \
SYMLINK+="camera/isp0"
这样,应用程序可以固定使用 /dev/camera/csi0 和 /dev/camera/isp0,不受内核注册顺序影响。
udevadm info /dev/video0 查看设备的所有属性,使用 udevadm monitor 实时监控 uevent 事件,使用 udevadm test /sys/class/video4linux/video0 模拟规则匹配过程。
2.4 常见误区与总结复盘
误区一:设备号冲突
多个驱动模块如果都使用静态分配且指定了相同的主设备号,会导致注册失败。V4L2 主设备号 81 是系统保留的,驱动开发者不应尝试注册其他主设备号。如果使用 alloc_chrdev_region() 动态分配,内核会保证主设备号唯一,但 V4L2 框架内部会强制使用 81。
误区二:file_operations 与 v4l2_file_operations 混淆
V4L2 驱动中,video_device->fops 指向的是 struct v4l2_file_operations,而不是 struct file_operations。V4L2 核心在 video_register_device() 内部会构造一个 struct file_operations(即 v4l2_fops),并将调用转发到 v4l2_file_operations。驱动开发者只需要实现 v4l2_file_operations 即可。
误区三:设备节点权限不足
默认情况下,udev 创建的设备节点权限为 0600(仅 root 可访问)。如果普通用户需要访问摄像头,必须在 udev 规则中显式设置 MODE="0666" 或使用 GROUP="video" 并将用户加入 video 组。
误区四:忘记调用 cdev_del()
在驱动卸载时,必须调用 cdev_del() 和 unregister_chrdev_region() 释放设备号和 cdev。V4L2 框架中,video_unregister_device() 会处理这些操作,但驱动开发者仍需确保在 platform_driver.remove() 回调中正确调用。
总结
| 知识点 | 关键要点 | V4L2 中的体现 |
|---|---|---|
| 设备号 | 主设备号标识驱动,次设备号标识实例 | 主设备号固定 81,次设备号由 video_register_device 分配 |
| file_operations | VFS 与驱动的接口契约 | V4L2 核心提供通用实现,驱动实现 v4l2_file_operations |
| 设备节点 | 用户空间访问内核设备的入口 | udev 根据 uevent 自动创建 /dev/videoX |
| udev 规则 | 自定义设备命名、权限、符号链接 | 通过 ATTR{name} 等属性匹配特定摄像头 |