3. V4L2设备注册与注销:设备节点与主/次设备号
V4L2(Video for Linux 2)框架的核心任务之一是将摄像头硬件抽象为标准的字符设备,供用户空间通过 open()、ioctl() 等系统调用访问。本章深入剖析设备注册与注销的底层机制,包括 video_register_device 与 video_unregister_device 的调用流程、设备节点在 /dev 下的创建规则,以及主设备号与次设备号的分配策略。
3.1 设备注册核心函数:video_register_device
video_register_device 是 V4L2 驱动向内核注册视频设备的入口。该函数完成以下关键步骤:
- 分配次设备号:从 V4L2 管理的次设备号池中获取一个空闲编号。
- 创建字符设备:调用
cdev_add将设备添加到内核字符设备层。 - 生成设备节点:通过
device_create在/dev下创建videoX节点。 - 关联 file_operations:将 V4L2 标准的
v4l2_fops绑定到该设备。
函数原型:
int video_register_device(struct video_device *vdev,
enum v4l2_device_type type,
int nr);
- vdev:已初始化的
video_device结构体指针。 - type:设备类型,如
VFL_TYPE_VIDEO(视频捕获)、VFL_TYPE_VBI(VBI 数据)、VFL_TYPE_RADIO(收音机)。 - nr:期望的次设备号,传入
-1表示由内核自动分配。
3.2 设备注销核心函数:video_unregister_device
当驱动卸载或设备热拔出时,必须调用 video_unregister_device 执行清理:
- 移除设备节点:删除
/dev/videoX文件。 - 销毁字符设备:调用
cdev_del从内核中移除该设备。 - 释放次设备号:将次设备号归还给 V4L2 管理池。
- 通知用户空间:通过
uevent机制广播设备移除事件。
注意: 注销操作必须在所有用户空间文件描述符关闭之后进行,否则可能导致内核崩溃。驱动应在
release() 回调中确保引用计数归零后再调用注销函数。
3.3 设备节点创建机制
V4L2 设备节点位于 /dev 目录下,命名规则为 videoX(X 为次设备号)。节点创建由内核的 device_create 函数完成,该函数在 sysfs 中创建对应的设备目录,并生成 devtmpfs 节点。
| 设备类型 | 节点名称 | 主设备号 | 次设备号范围 |
|---|---|---|---|
| 视频捕获/输出 | /dev/video0 ~ /dev/video63 |
81 | 0 ~ 63 |
| VBI 数据 | /dev/vbi0 ~ /dev/vbi31 |
81 | 224 ~ 255 |
| 收音机 | /dev/radio0 ~ /dev/radio63 |
81 | 64 ~ 127 |
| 软件定义无线电 (SDR) | /dev/swradio0 ~ /dev/swradio15 |
81 | 192 ~ 207 |
提示: 所有 V4L2 设备共享同一个主设备号 81,通过次设备号区分不同类型和实例。这种设计简化了内核的设备管理,同时保持了用户空间接口的一致性。
3.4 主设备号与次设备号分配策略
V4L2 框架使用静态分配的次设备号范围来区分设备类型。主设备号 81 在 Linux 内核中预留给 V4L2 使用,驱动开发者无需动态申请。
次设备号的分配遵循以下规则:
- 自动分配:调用
video_register_device时传入nr = -1,内核从对应类型的空闲号池中选取最小的可用编号。 - 固定分配:传入非负整数
nr,内核尝试使用该编号,若已被占用则返回-EBUSY。 - 类型隔离:每种设备类型拥有独立的次设备号区间,互不重叠。
3.5 完整注册与注销代码示例
以下代码展示了一个典型的 V4L2 摄像头驱动中设备注册与注销的实现模式:
#include <media/v4l2-device.h>
#include <media/v4l2-dev.h>
#include <media/videobuf2-v4l2.h>
static struct video_device *vdev;
static int mycam_probe(struct platform_device *pdev)
{
int ret;
/* 1. 分配 video_device 结构体 */
vdev = video_device_alloc();
if (!vdev)
return -ENOMEM;
/* 2. 设置标准 V4L2 文件操作 */
vdev->fops = &mycam_fops;
vdev->v4l2_dev = &mycam_v4l2_dev;
vdev->queue = &mycam_queue;
strscpy(vdev->name, "MyCamera", sizeof(vdev->name));
/* 3. 注册设备,自动分配次设备号 */
ret = video_register_device(vdev, VFL_TYPE_VIDEO, -1);
if (ret < 0) {
video_device_release(vdev);
return ret;
}
dev_info(&pdev->dev, "Registered /dev/video%d\n", vdev->num);
return 0;
}
static int mycam_remove(struct platform_device *pdev)
{
/* 注销设备,自动清理节点和次设备号 */
video_unregister_device(vdev);
video_device_release(vdev);
return 0;
}
3.6 常见错误与调试方法
| 错误现象 | 可能原因 | 排查方法 |
|---|---|---|
/dev/videoX 未创建 |
次设备号耗尽或 device_create 失败 |
检查 dmesg 中的错误信息;确认 CONFIG_DEVTMPS 已启用 |
注册返回 -ENOMEM |
内存分配失败或 cdev_add 资源不足 |
使用 slabtop 检查内核内存使用情况 |
| 注销时系统崩溃 | 设备仍在被用户空间使用 | 在注销前调用 v4l2_device_disconnect 阻止新访问 |
| 次设备号冲突 | 多个驱动使用了相同的固定次设备号 | 统一使用 nr = -1 让内核自动分配 |
关键调试命令:
# 查看所有 V4L2 设备节点
ls -la /dev/video*
# 查看设备主次设备号
cat /proc/devices | grep video
# 查看 sysfs 中的设备信息
udevadm info --query=all --name=/dev/video0
# 实时监控设备注册事件
udevadm monitor --kernel --property
3.7 设备注册的生命周期管理
V4L2 设备从注册到注销经历以下状态转换:
- 未注册状态:
video_device已分配但未调用video_register_device,用户空间无法访问。 - 已注册状态:设备节点存在,用户空间可打开设备进行 IOCTL 操作。
- 断开状态:调用
v4l2_device_disconnect后,新open()调用返回错误,但已有文件描述符仍可继续操作。 - 已注销状态:
video_unregister_device执行完毕,设备节点消失,所有资源释放。
3.8 多设备实例管理
当驱动需要管理多个摄像头时,每个实例独立调用 video_register_device,内核自动分配不同的次设备号:
#define MAX_CAMERAS 4
static struct video_device *cam_devs[MAX_CAMERAS];
static int probe_all_cameras(struct platform_device *pdev)
{
int i, ret;
for (i = 0; i < MAX_CAMERAS; i++) {
cam_devs[i] = video_device_alloc();
if (!cam_devs[i]) {
ret = -ENOMEM;
goto err_cleanup;
}
/* 每个设备独立配置 */
cam_devs[i]->fops = &mycam_fops;
snprintf(cam_devs[i]->name, sizeof(cam_devs[i]->name),
"MyCamera-%d", i);
ret = video_register_device(cam_devs[i], VFL_TYPE_VIDEO, -1);
if (ret < 0) {
video_device_release(cam_devs[i]);
goto err_cleanup;
}
dev_info(&pdev->dev, "Camera %d: /dev/video%d\n",
i, cam_devs[i]->num);
}
return 0;
err_cleanup:
while (--i >= 0) {
video_unregister_device(cam_devs[i]);
video_device_release(cam_devs[i]);
}
return ret;
}
最佳实践: 在
probe() 函数中,建议先分配所有资源,最后再调用 video_register_device。这样如果中途出错,可以避免已注册的设备节点暴露给用户空间,防止出现不完整设备状态。