夜雨聆风学习资料网

ARTICLE · 1197407

V4L2 源码深度解析:从 video_device 到 /dev/videoX 的完整生命周期

V4L2 源码深度解析:从 video_device 到 /dev/videoX 的完整生命周期

应用程序访问视频设备,通常从下面的调用开始:

int fd = open("/dev/video0", O_RDWR);

这个接口看起来和打开普通文件没有区别,内核走的却是字符设备路径。文件名经过路径解析,变成带有设备号的 inode;设备号找到字符设备对象;字符设备对象提供统一操作入口;统一入口再找到对应的 video_device,调用驱动注册的函数。

这条链路的核心,就是 v4l2-dev.c。

它不是图像采集引擎,而是 V4L2 的设备节点管理层:负责把一个内存中的节点对象,变成可以被访问、能够正确分发操作、也能够安全退出的字符设备。

要真正读懂它,必须把三个问题连在一起:video_device 保存了什么,__video_register_device() 如何建立这些关联,以及打开与注销如何共同维护对象的生命期。


一、总体实现框架:注册、访问、退出是一条闭合链路

这张图中有三个不能合并的阶段。

注册负责建立连接。设备号、操作表、sysfs 对象和驱动回调原本彼此独立,注册函数把它们组织成一个完整节点。

访问负责沿连接查找对象。核心层并不根据 video0 这个字符串判断驱动,而是使用字符设备的次设备号查找节点对象。

退出负责拆除连接。停止接收新访问,不代表所有旧访问已经结束;撤下设备文件,也不代表支撑旧文件的内存可以立即释放。

因此,整个实现贯穿着一个原则:

可见性、可访问性和对象存活,是三种不同的状态。

device_register() 推进设备的对外发布;V4L2_FL_REGISTERED 决定核心是否接受访问;struct device 的引用计数决定对象什么时候能够最终释放。

1.1 文件内部的职责分布

职责
核心对象或函数
解决的问题
公共基础设施
videodev_init()
、video_class
设备号范围和设备类从哪里来
节点身份管理
video_devices[]
、devnode_nums[]、get_index()
如何分配编号,如何通过设备号找到节点
节点注册
__video_register_device()
如何让内存对象成为可访问的字符设备
操作分发
v4l2_fops
、video_devdata()
如何从文件操作进入当前节点的驱动回调
能力筛选
determine_valid_ioctls()
哪些标准 ioctl 应当对当前节点开放
生命期管理
video_get()
、video_put()、注销与释放函数
如何防止仍被引用的对象提前销毁
辅助功能
优先级、sysfs 属性、媒体管线包装接口
如何维护节点附属状态

缓冲区分配、图像格式协商的具体规则、DMA 提交和中断处理不在这一层完成。即使应用调用的是 VIDIOC_STREAMON,v4l2-dev.c 的直接职责仍然是检查和转发,不是启动某个硬件寄存器。


二、struct video_device:一个节点为什么需要这么多成员

video_device 的核心含义是:一个可以对外提供 V4L2 文件接口的节点实例。

它既不是一帧图像,也不等于整套摄像头硬件。一个硬件设备可以关联多个节点;这些节点可以共用同一个 v4l2_device,但拥有不同的设备号、能力和回调表。

2.1 结构体全貌

下面的声明展示各成员的类型与组织关系。结构体及头文件内联接口使用 Linux v6.1 的接口定义补充说明;注册、分发和释放逻辑对应 media/v4l2-core/v4l2-dev.c 的实现。

structvideo_device {#if defined(CONFIG_MEDIA_CONTROLLER)structmedia_entityentity;structmedia_intf_devnode *intf_devnode;structmedia_pipelinepipe;#endifconststructv4l2_file_operations *fops;    u32 device_caps;structdevicedev;structcdev *cdev;structv4l2_device *v4l2_dev;structdevice *dev_parent;structv4l2_ctrl_handler *ctrl_handler;structvb2_queue *queue;structv4l2_prio_state *prio;char name[32];enum vfl_devnode_type vfl_type;enum vfl_devnode_direction vfl_dir;int minor;    u16 num;unsignedlong flags;int index;spinlock_t fh_lock;structlist_headfh_list;int dev_debug;    v4l2_std_id tvnorms;void (*release)(struct video_device *vdev);conststructv4l2_ioctl_ops *ioctl_ops;    DECLARE_BITMAP(valid_ioctls, BASE_VIDIOC_PRIVATE);structmutex *lock;};

理解这个结构体,不宜只给每个字段翻译一个中文名称。更重要的是弄清:谁写入它、谁读取它、它连接了哪个对象,以及它的生命期由谁保证。

2.2 dev 与 cdev:同一个节点的两种身份

structdevicedev;structcdev *cdev;

cdev 负责字符设备身份。它把 dev_t 设备号和 struct file_operations 关联起来,回答的是:

应用打开这个字符设备号以后,应该调用哪一组文件操作?

dev 负责设备模型身份。它承载设备名称、设备类、父设备、sysfs 属性,以及支撑对象生命期的引用计数,回答的是:

这个节点属于哪个设备层次,如何发布,以及什么时候可以回收?

两者处理的问题不同,所以不能认为注册了 cdev 就自动完成所有设备模型工作,也不能认为 sysfs 已经有一个设备对象,就一定存在正确的文件操作入口。

另一个重要区别是内存布局。

dev内嵌在video_device 中,两者共享外层内存分配的生命期。设备模型释放回调收到 struct device * 时,可以通过 to_video_device() 找回外层 video_device。

cdev 则是一个单独分配的对象,通过指针挂到 video_device。注册时调用 cdev_alloc(),正常最终释放时调用 cdev_del() 撤销字符设备关联。这里的“删除”也应理解为撤销映射并归还相应引用,不应推导成所有情况下都立即释放其全部底层内存。

2.3 fops 与 ioctl_ops:一张表负责操作类别,另一张表负责具体命令

conststructv4l2_file_operations *fops;conststructv4l2_ioctl_ops *ioctl_ops;

fops 保存节点的文件操作回调,例如 open、release、read、poll、mmap 和 unlocked_ioctl。

ioctl_ops 保存标准 V4L2 命令的具体实现,例如 vidioc_querycap、vidioc_s_fmt_vid_cap_mplane、vidioc_reqbufs、vidioc_streamon。

在采用标准 ioctl 辅助层的节点上,调用关系是:

应用 ioctl()    → 核心 v4l2_ioctl()    → vdev->fops->unlocked_ioctl()    → video_ioctl2()    → 标准命令分发    → vdev->ioctl_ops 中相应的 vidioc_* 回调

这条连接不是由结构体字段名称自动产生的。驱动必须把自己的 .unlocked_ioctl 设置为 video_ioctl2,或者在自定义入口中主动复用它。

如果驱动选择完全自定义的 ioctl 分发函数,不能假设它仍会自动使用 ioctl_ops、有效命令位图和标准分发锁。

更不能把 vdev->fops 直接赋给 cdev->ops:前者是 struct v4l2_file_operations,后者要求 struct file_operations,不仅职责不同,类型也不同。

2.4 v4l2_dev 与 dev_parent:框架归属不等于设备模型父节点

structv4l2_device *v4l2_dev;structdevice *dev_parent;

v4l2_dev 指向 V4L2 的组织对象。它可以管理一组节点、子设备以及共享控制和优先级状态。注册函数要求这个指针有效,并在节点注册后增加它的引用。

dev_parent 指向 Linux 设备模型中的父设备。注册时,它被赋给:

vdev->dev.parent = vdev->dev_parent;

如果驱动没有单独设置 dev_parent,核心默认采用 vdev->v4l2_dev->dev。

因此,常见关系是:

一个上层设备对象    └── 一个 v4l2_device          ├── video_device A          ├── video_device B          └── video_device C

但不能据此把 v4l2_device 当成通用 struct device 使用。它们是不同的类型,也使用不同的引用管理接口。

2.5 name、num、minor、index:四个“名字或编号”各管一件事

成员
实际含义
典型用途
name
驱动填写的描述性名称
sysfs name 属性、媒体实体名称
num
节点名称后缀
video8
 中的 8
minor
字符设备次设备号
MKDEV()
、cdev_add()、video_devices[]
index
同一个 v4l2_device 内的空闲节点索引
区分同组节点,形成 sysfs index 属性

例如,下面这些值可以同时成立:

vdev->name                   = "Example Capture"dev_name(&vdev->dev)          = "video8"vdev->num                    = 8vdev->minor                  = 0vdev->index                  = 0字符设备号                    = 81:0

条件是没有启用固定 minor 范围,并且相应编号都空闲,注册时优先申请名称后缀 8。

video8 不必对应 minor 8。路径名中的数字是命名结果,inode 中的设备号才参与字符设备分发。

2.6 vfl_type、vfl_dir 与 device_caps:类别、方向、能力不能混为一谈

enum vfl_devnode_type vfl_type;enum vfl_devnode_direction vfl_dir;u32 device_caps;

vfl_type 表示节点类别,决定名称前缀,也参与 ioctl 分类。VFL_TYPE_VIDEO 对应 videoX,VFL_TYPE_SUBDEV 对应 v4l-subdevX。

vfl_dir 表示节点的数据方向。采集节点通常是 VFL_DIR_RX,输出节点通常是 VFL_DIR_TX,内存到内存处理节点是 VFL_DIR_M2M。

这里的 RX/TX 描述节点接口的接收或输出角色,不是在说明当前执行了哪条 DMA 总线事务。对 M2M 节点而言,应用可以提交待处理数据,再取回处理结果,因此需要同时考虑输入和输出两侧。

device_caps 表示当前节点对外声明的能力,例如:

V4L2_CAP_VIDEO_CAPTURE_MPLANE | V4L2_CAP_STREAMING

它表达的是支持多平面采集接口和流式缓冲区接口,而不是保证已经分配好了缓冲区,更不代表设备已经开始采集。

多平面是缓冲区 API 的组织方式,不是“三个颜色分量一定对应三块内存”。 同样,能力位也不能替代函数实现:声明 V4L2_CAP_STREAMING 不会自动产生 REQBUFS、QBUF 或 STREAMON 回调。

注册函数只检查非子设备节点的 device_caps 不能为零,并不会证明全部能力位都与驱动实际实现一致。

2.7 flags:这里既有生命期状态,也有框架行为标志

unsignedlong flags;

其中最关键的是 V4L2_FL_REGISTERED。注册最后设置它,注销首先清除它,文件操作包装函数用它判断当前节点是否仍然允许访问。

它不是“对象是否分配”的标志,也不是“设备是否正在出图”的标志。

V4L2_FL_USES_V4L2_FH 则说明节点使用标准 v4l2_fh 文件句柄机制。它由相关句柄初始化路径设置,后续代码据此解释 file->private_data,并在注销时唤醒标准事件等待者。

标志枚举给出的是位的位置。因此应使用 set_bit()、clear_bit()、test_bit(),不能把枚举编号直接当成掩码执行 flags |= V4L2_FL_REGISTERED。

同时,原子位操作只保证这个位本身的更新规则,不会自动保护“查表、判定状态、增加引用”这一整组操作。后者仍然需要 videodev_lock。

另外,V4L2_FL_QUIRK_INVERTED_CROP 用于旧式 M2M 裁剪语义兼容,V4L2_FL_SUBDEV_RO_DEVNODE 用于只读子设备节点的接口限制。它们不表示节点是否正在采集,也不能替代通用注册状态。

不能通过手工设置注册位来代替注册函数。正常退出也不能简单地先自行清除此位、再期待 video_unregister_device() 总能完成剩余工作:后者的入口检查可能因为已观察到未注册状态而提前返回。

2.8 ctrl_handler:控制对象的关联,不是控制值的副本

structv4l2_ctrl_handler *ctrl_handler;

这个指针把节点接到 V4L2 控制框架,用来管理曝光、增益、亮度等控制项所对应的控制对象。

没有显式设置时,注册函数继承 v4l2_dev->ctrl_handler。这里是共享指针,不是复制一份控制状态。

它也会影响注册阶段的 ioctl 筛选:即使没有提供某些传统 vidioc_g_ctrl、vidioc_s_ctrl 回调,只要接入控制处理器,相应标准控制命令仍可能由框架处理。

此外,控制处理器还可能绑定在每次打开的 v4l2_fh 上。这种信息在节点注册时尚不存在,所以注册期位图不能覆盖全部控制命令判定,运行期仍需结合当前文件句柄检查。

2.9 queue 与 lock:挂上一个指针,不等于框架替驱动完成初始化

structvb2_queue *queue;structmutex *lock;

queue 关联节点的 videobuf2 队列。它不是一帧缓冲区,而是队列级对象,管理缓冲区、状态及相应操作。

__video_register_device() 不会因为这个指针非空,就自动调用队列初始化、分配图像内存或者开始流传输。队列必须由相关驱动路径提前准备好。

lock 通常供标准 ioctl 分发使用。在采用 video_ioctl2() 的路径上,v4l2-ioctl.c 选择合适的互斥锁;队列类 ioctl 还可能优先使用 queue->lock,M2M 文件上下文也可能提供专用队列锁。

但是,v4l2-dev.c 中的 v4l2_ioctl() 本身并没有获取 vdev->lock。open、read、mmap、poll 等包装函数也不会统一自动获取它。

锁字段表达一种协作约定,不是“填上以后所有回调都自动串行化”的开关。

2.10 fh_lock 与 fh_list:一个节点可以拥有多次打开

spinlock_t fh_lock;structlist_headfh_list;

节点是设备级对象,文件句柄是打开级对象。应用对同一个节点执行两次独立 open(),通常会产生两个独立文件对象;标准句柄机制可以让它们各自关联一个 v4l2_fh。

fh_list 把同一节点的这些标准文件句柄串起来,fh_lock 保护链表的短临界区。

注册函数只做:

spin_lock_init(&vdev->fh_lock);INIT_LIST_HEAD(&vdev->fh_list);

这是初始化“管理句柄的容器”,不是创建文件句柄。实际分配、加入和移除句柄发生在 v4l2_fh_open() 等相关路径,前提是驱动选择了这套机制。[^fh]

链表操作使用自旋锁,适合短小、不能睡眠的临界区;全局注册管理使用互斥锁,可以包含允许睡眠的操作。不能在持有 fh_lock 时任意执行可能睡眠的驱动处理。

2.11 prio:访问优先级不是线程调度优先级

structv4l2_prio_state *prio;

prio 指向一组共享的 V4L2 访问优先级计数。多个文件句柄可以拥有不同的本地优先级,核心据此对需要优先级检查的操作作出允许或拒绝的决定。

没有单独提供时,它继承 v4l2_device 中的优先级状态,所以多个节点可能共享同一组计数。

这不是调高进程的调度优先级,也不是控制帧缓冲区出队顺序。它解决的是“多个访问者竞争设备状态时,哪些操作需要让位”的问题。

2.12 valid_ioctls:注册前和注册后的含义不同

DECLARE_BITMAP(valid_ioctls, BASE_VIDIOC_PRIVATE);

这是最容易被名称误导的成员。

注册前,驱动可以通过 v4l2_disable_ioctl() 把需要禁用的命令对应位置为 1。

注册过程中,determine_valid_ioctls() 先生成按类别、方向、能力和回调推导出的候选集合,再扣掉前面指定的禁用集合。

注册完成后,成员中的 1 才表示该标准命令通过了这一层的有效性筛选。

换句话说,同一块位图经历了转换:

注册前:禁止哪些命令          ↓ determine_valid_ioctls()注册后:允许哪些标准命令

因此,它必须从正确的初始状态开始,不能把旧对象残留的位图当作新对象继续使用,也不能在注册完成后照搬注册前的禁用方式。

2.13 release:它决定节点内存如何最终回收

void (*release)(struct video_device *vdev);

这是节点对象的最终释放回调。它与 vdev->fops->release 不是同一个接口。

前者处理整个节点的结束,后者处理某一次打开形成的文件对象的结束。设备被多次打开时,文件级释放可能发生多次;节点最终释放在正常生命期中只发生一次。

注册函数强制要求 release 存在,是因为核心知道什么时候不再需要这个节点,却不一定知道它的内存是如何组织的。

如果节点由 video_device_alloc() 单独分配,可以使用 video_device_release(),其作用是 kfree(vdev)。

如果节点内嵌在更大的结构体中,必须采用适合外层对象的回收策略。存在多个节点共享同一个外层对象时,更不能在第一个节点释放时就把整个外层对象销毁。

2.14 媒体拓扑成员:描述关联,不直接搬运图像

entity 是节点在媒体拓扑中的实体表示;intf_devnode 是拓扑中的设备接口对象,用来把实体和字符设备号关联起来。

接口定义中的 pipe 是媒体管线对象。这些媒体成员并不意味着节点注册时会自动建立完整硬件链路,更不意味着自动启动采集。管线启停包装接口仍需要显式调用,普通 M2M 节点也有不同的实体组织方式。

2.15 dev_debug 与 tvnorms:辅助状态仍需分清层次

dev_debug 是核心调试位图,可以通过 sysfs 调整。不同位决定是否输出 ioctl、文件操作、流式操作或 poll 的调试信息。

tvnorms 描述模拟电视制式集合,主要用于相关标准接口,不是数字摄像头的分辨率列表,也不是帧率字段。它并不参与这里的设备号分配。

2.16 最重要的所有权结论

video_device 中很多成员只是指针。

把指针写入结构体,不等于移交了被指向对象的内存所有权。 注册函数不会自动复制 fops、创建 queue、初始化驱动互斥锁,或者让任意驱动私有对象永久存活。

所有回调表和被引用对象,都必须在可能被访问的期间保持有效;最终由哪个回调销毁,则要根据各自的所有权关系安排。


三、公共初始化:为什么没有注册节点,框架也已经存在

videodev_init() 先为整个节点管理层准备公共基础设施。

ret = register_chrdev_region(dev, VIDEO_NUM_DEVICES, VIDEO_NAME);if (ret < 0)return ret;ret = class_register(&video_class);if (ret < 0) {    unregister_chrdev_region(dev, VIDEO_NUM_DEVICES);return -EIO;}

其中 dev 由 MKDEV(VIDEO_MAJOR, 0) 构造,管理范围包含 256 个次设备号。VIDEO_MAJOR 为 81,设备类名称是 video4linux。

register_chrdev_region() 是保留字符设备号范围,不是为每个号码创建节点。class_register() 是注册公共设备类,也不是注册具体的 video0。

此时的状态可以概括为:

号码范围有了设备类别有了具体 video_device 还没有接入具体 cdev 还没有注册具体 videoX 还没有发布

节点驱动之后调用注册接口,才会逐个建立实际实例。核心退出时通过 class_unregister() 和 unregister_chrdev_region() 撤销公共设施;各节点仍需先完成自己的退出流程。


四、__video_register_device() 的本质:建立关联,最后发布可用状态

通常的调用入口是:

ret = video_register_device(vdev, VFL_TYPE_VIDEO, -1);

头文件内联包装将它转到:

__video_register_device(vdev, type, nr, 1, vdev->fops->owner);

底层函数的参数为:

int __video_register_device(struct video_device *vdev,enum vfl_devnode_type type,int nr, int warn_if_nr_in_use,                           struct module *owner);
参数
职责
容易混淆的地方
vdev
待注册的节点对象
不是整组设备的 v4l2_device
type
节点类别
不是像素格式,也不是 v4l2_buf_type
nr
优先尝试的名称后缀;-1 表示自动分配
不是必须分配成功的固定 minor
warn_if_nr_in_use
表达申请编号被替换时的告警意图
不改变实际编号搜索策略
owner
字符设备所属模块
不是设备模型中的父设备

从实现角度看,注册不是一次不可分割的动作,而是多阶段建立资源和关联。它依靠编号预留、全局互斥锁和最终注册标志,避免其他线程把初始化中的节点当成完整设备使用。

这条路径最值得关注的不是函数调用数量,而是每个阶段结束后,系统里“已经有什么”以及“还不允许做什么”。

五、注册第一阶段:先建立有效的初始状态

5.1 minor = -1 不是分配编号,而是先标记“尚未完成注册”

vdev->minor = -1;

编号尚未分配时,minor 不能被当成可用设备号。以 -1 作为无效值,能把“分配出来的 minor 0”和“还没有编号”区分开。

但这个值不是运行期访问状态的最终依据。注册过程中,minor 会提前变成非负数,而 V4L2_FL_REGISTERED 仍然没有设置;注销后,minor 也未必立即改回 -1。

所以不能用下面的判断替代核心状态检查:

/* 不能据此判断节点仍可接受访问。 */if (vdev->minor >= 0)/* ... */;

真正的准入状态由 video_is_registered(vdev) 判断。

5.2 三项显式检查分别防止什么问题

if (WARN_ON(!vdev->release))return -EINVAL;if (WARN_ON(!vdev->v4l2_dev))return -EINVAL;if (WARN_ON(type != VFL_TYPE_SUBDEV && !vdev->device_caps))return -EINVAL;

没有 release,节点最后一次引用消失时,核心无法按正确的内存布局完成最终清理。

没有 v4l2_dev,后面的默认父设备、控制处理器、优先级继承以及上层引用操作都失去有效依托。

非子设备节点没有 device_caps,核心不能把节点理解成具有明确能力的设备。子设备节点使用不同的接口定位,因此不适用这里的同一检查。

这三项只是显式检查,不是完整初始化清单。特别是 vdev->fops 必须有效:常用包装接口在进入底层函数前就要读取 vdev->fops->owner。

“源码没有检查”不等于“允许不设置”。 内核内部接口往往依赖调用方遵守初始化契约。

5.3 为什么对象必须从清零状态开始

video_device_alloc() 使用 kzalloc(),为整个结构体分配清零内存。

清零不仅是让空指针更容易判断,也使初始 flags 中没有残留的注册位,让 valid_ioctls 没有意外的禁用位。

注册函数不会重新清洗所有成员。例如,它不会自动清除任意旧 flags,也不会在计算有效 ioctl 前把驱动设置的禁用位抹掉。

因此,不能对一个仍在使用、尚未最终释放,或者保留旧生命期状态的对象再次调用注册函数。初始化一个新对象与复用旧对象不是同一件事。

5.4 确定节点类别,就是确定命名空间

节点类别
名称前缀
示例
VFL_TYPE_VIDEOvideo/dev/video0
VFL_TYPE_VBIvbi/dev/vbi0
VFL_TYPE_RADIOradio/dev/radio0
VFL_TYPE_SUBDEVv4l-subdev/dev/v4l-subdev0
VFL_TYPE_SDRswradio/dev/swradio0
VFL_TYPE_TOUCHv4l-touch/dev/v4l-touch0

type 既决定 name_base,也被写入 vdev->vfl_type,供编号位图选择、ioctl 筛选和媒体接口注册使用。

这也说明 VFL_TYPE_VIDEO 只是节点类别,不能直接等同于“一个摄像头”。采集、输出、编解码 M2M 和某些元数据节点都需要结合方向与能力进一步区分。

5.5 默认继承是关联复用,不是新建资源

vdev->vfl_type = type;vdev->cdev = NULL;if (vdev->dev_parent == NULL)    vdev->dev_parent = vdev->v4l2_dev->dev;if (vdev->ctrl_handler == NULL)    vdev->ctrl_handler = vdev->v4l2_dev->ctrl_handler;if (vdev->prio == NULL)    vdev->prio = &vdev->v4l2_dev->prio;

三个 if 都只在当前成员为空时采用默认值,因此允许节点覆盖默认关联。

这种设计同时支持两类情况:多个节点共享一个组织对象的公共状态,或者某个节点拥有独立控制处理器、独立优先级域及不同的设备模型父对象。

不过,共享对象必须比依赖它的访问存活得更久。复制指针并没有复制控制处理器,也没有把一把互斥锁或一组状态保存成节点私有副本。


六、注册第二阶段:num、minor、index 如何分配

6.1 全局表、名称位图和互斥锁各自负责什么

staticstructvideo_device *video_devices[VIDEO_NUM_DEVICES];staticDEFINE_MUTEX(videodev_lock);staticDECLARE_BITMAP(devnode_nums[VFL_TYPE_MAX], VIDEO_NUM_DEVICES);

video_devices[] 以 minor 为索引,保存对应的节点指针。

devnode_nums[] 保存名称后缀的占用情况。未启用固定范围时,不同节点类别通常各有一张位图,所以 video0 和 radio0 可以同时存在。

videodev_lock 保护分配与登记的组合操作。仅把单个占用位设成原子操作还不够,因为一次分配要同时更新名称编号、minor 槽位、组内 index 和全局指针表。

6.2 nr 的算法:先从期望编号向后找,再回到起点

mutex_lock(&videodev_lock);nr = devnode_find(vdev, nr == -1 ? 0 : nr, minor_cnt);if (nr == minor_cnt)    nr = devnode_find(vdev, 0, minor_cnt);if (nr == minor_cnt) {    pr_err("could not get a free device node number\n");    mutex_unlock(&videodev_lock);return -ENFILE;}

devnode_find() 使用 find_next_zero_bit()。某一位为 0,表示相应名称编号尚未占用;找不到时返回搜索上界。

假设申请编号 8,而名称编号占用情况如下:

0 1 2 3 4 5 6 7 8 9 10 ...占 占 空 占 占 占 占 占 占 空 ...

第一次从 8 向后找,会得到 9,而不是立即返回编号冲突。

如果 8 到上界全部占用,第二次从 0 开始找,就可能得到 2。

因此,nr 表示优先搜索的起点。它既不是强制固定的名称后缀,也不是不允许回退的最小编号。

6.3 未启用固定范围:名称后缀和 minor 独立分配

名称后缀确定后,核心扫描全局指针表:

for (i = 0; i < VIDEO_NUM_DEVICES; i++)if (video_devices[i] == NULL)break;if (i == VIDEO_NUM_DEVICES) {    mutex_unlock(&videodev_lock);    pr_err("could not get a free minor\n");return -ENFILE;}

这里找的是所有节点共用的空闲字符设备槽位,并不关心名称后缀刚刚分到了多少。

假设一开始没有节点,按下面的顺序注册:

注册动作
节点名称
minor
同组 index
设备组 A 注册视频节点,优先申请 8
video8
0
0
设备组 B 注册无线电节点,自动编号
radio0
1
0
设备组 A 再注册一个视频节点,自动编号
video0
2
1

这些值由分配规则推导,说明三种编号独立解决不同问题:名称不冲突、字符设备号不冲突,以及同组节点索引不冲突。

6.4 启用固定范围:名称后缀通过偏移映射到 minor

启用 CONFIG_VIDEO_FIXED_MINOR_RANGES 时,代码按类型选择范围:

类型
起始 minor
数量
minor 范围
视频节点
0
64
0~63
无线电节点
64
64
64~127
VBI 节点
224
32
224~255
其余受支持类别共享范围
128
64
128~191

此时 i = nr,因此:

minor = num + minor_offset

例如 radio0 对应 minor 64,而不是 minor 0。

共享 128~191 范围的类别还必须共享相应名称占用位图,否则不同类别可能各自认为同一个偏移位置空闲,最终产生 minor 冲突。devnode_bits() 中的映射正是为了保持这两种分配规则一致。

这条固定范围路径没有把 192~223 加入上述可分配区间,不能把表中未使用的范围理解成自动回退空间。

6.5 index 的算法:收集同组占用,再找最小空位

staticintget_index(struct video_device *vdev){staticDECLARE_BITMAP(used, VIDEO_NUM_DEVICES);int i;    bitmap_zero(used, VIDEO_NUM_DEVICES);for (i = 0; i < VIDEO_NUM_DEVICES; i++) {if (video_devices[i] != NULL &&            video_devices[i]->v4l2_dev == vdev->v4l2_dev) {            __set_bit(video_devices[i]->index, used);        }    }return find_first_zero_bit(used, VIDEO_NUM_DEVICES);}

它并不是读取一个不断增长的计数器,而是重新计算当前同组节点占用了哪些 index。

判断“同组”的依据是 v4l2_dev 指针相同,不是节点前缀相同,也不是某个设备号范围相同。

因此,index 可以复用。如果同组 index 0 和 2 仍然被占用,index 1 对应的旧对象已经最终释放,那么新节点可以获得 1。

临时位图使用 static,依赖调用方已经持有 videodev_lock。脱离这把锁单独并发调用,就不再满足它的同步前提。

6.6 真正的“预留”发生在同一个临界区中

vdev->minor = i + minor_offset;vdev->num = nr;if (WARN_ON(video_devices[vdev->minor])) {    mutex_unlock(&videodev_lock);    pr_err("video_device not empty!\n");return -ENFILE;}devnode_set(vdev);vdev->index = get_index(vdev);video_devices[vdev->minor] = vdev;mutex_unlock(&videodev_lock);

顺序有明确含义。

先记录当前节点的 minor 和 num,再把名称占用位置为 1;接着计算 index;最后把节点指针写入全局表。

计算 index 时,新节点尚未进入表内,所以不会把自己尚未确定的 index 当成已有占用。

写入全局表后,即使接下来的 cdev 分配和设备模型注册尚未完成,其他注册线程也会把这个槽位视为已占用,不会重复分配。

这是资源预留,不是可用状态发布。 此时 V4L2_FL_REGISTERED 仍然为 0。


七、注册第三阶段:为什么要提前计算 ioctl 有效集合

编号预留之后,注册函数执行:

if (vdev->ioctl_ops)    determine_valid_ioctls(vdev);

目的不是执行某个 ioctl,而是把节点的静态接口条件提前整理成一个运行期容易查询的集合。

7.1 四类信息共同决定命令是否有意义

函数先提取一组条件:

bool is_vid = vdev->vfl_type == VFL_TYPE_VIDEO &&              (vdev->device_caps & vid_caps);bool is_meta = vdev->vfl_type == VFL_TYPE_VIDEO &&               (vdev->device_caps & meta_caps);bool is_rx = vdev->vfl_dir != VFL_DIR_TX;bool is_tx = vdev->vfl_dir != VFL_DIR_RX;

这里同时使用节点类别、能力、方向与回调表。

VFL_TYPE_VIDEO 只是第一层分类。只有包含相应视频能力位,才进入普通视频接口的筛选;元数据能力则走相应元数据分支。

方向判定采用“不等于”而不是“等于”,因此 VFL_DIR_M2M 既满足 is_rx,又满足 is_tx,可以覆盖两侧接口。

7.2 最简单的规则:有对应回调,就设置候选位

#define SET_VALID_IOCTL(ops, cmd, op) \    do { if ((ops)->op) __set_bit(_IOC_NR(cmd), valid_ioctls); } while (0)

例如:

SET_VALID_IOCTL(ops, VIDIOC_QUERYCAP, vidioc_querycap);SET_VALID_IOCTL(ops, VIDIOC_REQBUFS, vidioc_reqbufs);SET_VALID_IOCTL(ops, VIDIOC_STREAMON, vidioc_streamon);

实际调用处还处于相应的类别分支内,因此不能脱离外层条件理解成“只要结构体里有任何同名回调,就一定对所有节点开放”。

位图下标是 _IOC_NR(cmd),不是整个 ioctl 编码。完整命令编码还包含类型、方向和参数大小等信息,标准分发层会进行进一步识别,不能靠这一位图验证任意构造的命令值。

7.3 一个标准命令可能对应多个具体回调

VIDIOC_S_FMT 是统一命令,但格式类型可能是单平面采集、多平面采集、输出或者其他接口。

候选位的设置逻辑会汇总这些回调。例如,只要方向和能力匹配,并提供了相应采集格式回调,就可以设置 VIDIOC_S_FMT 的候选位。

但这个位为 1,只说明命令类别可进入下一层。运行期还要根据传入的 v4l2_format.type 选择具体回调,并验证参数。

命令可用,不等于这个命令的所有参数组合都可用。

7.4 框架代办的命令,不能简单按回调指针判定

部分命令由框架逻辑提供。例如优先级命令可以由通用层处理;控制命令可以通过控制处理器处理;某些兼容接口可以由其他回调支撑。

源码还根据事件订阅回调开启 VIDIOC_DQEVENT,并根据 selection 回调开放相应的旧裁剪接口。

因此,“驱动没有填一个名字完全对应的回调”并不足以证明该命令不可用,必须看框架是否有对应的代办路径。

7.5 注册前的禁用集合,怎样转成注册后的允许集合

函数最后执行:

bitmap_andnot(vdev->valid_ioctls,              valid_ioctls,              vdev->valid_ioctls,              BASE_VIDIOC_PRIVATE);

设候选集合为 C,注册前保存的禁用集合为 D,则结果是:

允许集合 A = C AND NOT D

例如,一个公共回调表同时实现 A、B、C 三个命令,但某个节点不应开放 B:

根据回调与类别推导的候选集合:A B C注册前显式禁用的集合:        B最终得到的允许集合:        A   C

v4l2_disable_ioctl() 必须在注册前调用。注册完成以后再把同一位置设为 1,得到的含义已经不是“禁用”。

同时,标准控制命令存在文件句柄控制处理器的例外;私有命令也有自己的分发路径。这个位图不是节点全部行为的唯一授权表。


八、注册第四阶段:cdev 为什么绑定统一的 v4l2_fops

8.1 分配并注册字符设备

vdev->cdev = cdev_alloc();if (vdev->cdev == NULL) {    ret = -ENOMEM;goto cleanup;}vdev->cdev->ops = &v4l2_fops;vdev->cdev->owner = owner;ret = cdev_add(vdev->cdev, MKDEV(VIDEO_MAJOR, vdev->minor), 1);if (ret < 0) {    pr_err("%s: cdev_add failed\n", __func__);    kfree(vdev->cdev);    vdev->cdev = NULL;goto cleanup;}

cdev_add() 的最后一个参数为 1,说明这个对象只覆盖当前节点对应的一个字符设备号。

这里建立的关联是:

主设备号与次设备号    → 当前 cdev    → 核心 v4l2_fops

最关键的是 vdev->cdev->ops = &v4l2_fops。所有节点先进入同一组核心包装函数,再通过当前文件找到自己的 video_device。

8.2 统一入口带来的不是统一驱动,而是统一约束

核心操作表主要包括:

staticconststructfile_operationsv4l2_fops = {    .owner          = THIS_MODULE,    .read           = v4l2_read,    .write          = v4l2_write,    .open           = v4l2_open,    .mmap           = v4l2_mmap,    .unlocked_ioctl = v4l2_ioctl,    .release        = v4l2_release,    .poll           = v4l2_poll,    .llseek         = no_llseek,};

这里列出主要入口;实现还按配置接入兼容 ioctl 和无 MMU 地址选择接口。

统一包装层可以在驱动回调之前执行节点状态检查,在打开和关闭之间配对设备引用,并提供一致的核心调试入口。

不同节点仍然可以有完全不同的 vdev->fops。共享的是外层分发框架,不是底层硬件实现。

8.3 两种 owner 保护的是模块代码

vdev->cdev->owner 关联具体节点所属模块,核心 v4l2_fops.owner 关联核心操作表所在模块。

通用字符设备打开路径涉及字符设备对象和操作表的模块引用,以防相关模块代码在仍被使用时被正常卸载。

但模块引用和设备对象引用是两回事。

模块代码仍在,不代表设备硬件没有拔出;硬件不再可用,也不代表节点对象内存可以立即释放。这正是后面还要管理注册标志和 struct device 引用的原因。

8.4 cdev_add() 成功后,为什么还不能直接当成注册完成

从字符设备机制看,设备号到操作表的关联已经建立。但设备模型注册、上层引用和媒体关联尚未完成。

如果通过已有的字符设备文件进入打开路径,核心会看到节点尚未设置注册标志,从而拒绝把它当成可用节点。

因此,全局表登记、字符设备映射和最终开放访问是三个不同时间点。这个分阶段实现不能被简化成“cdev_add() 成功,整个设备就完全可用了”。


九、注册第五阶段:设备模型如何把节点发布出去

9.1 填写 class、设备号、父对象和名称

vdev->dev.class = &video_class;vdev->dev.devt = MKDEV(VIDEO_MAJOR, vdev->minor);vdev->dev.parent = vdev->dev_parent;dev_set_name(&vdev->dev, "%s%d", name_base, vdev->num);

这四项分别建立分类、字符设备身份、设备层次和设备模型名称。

name_base 来自节点类别,num 来自名称位图分配,二者拼出 video8 这样的名称。

vdev->name 不参与这次拼接。它是描述性名称,不能通过把 vdev->name 填成 video8 就控制实际设备文件编号。

9.2 再次获取全局锁,覆盖设备发布与最终激活

mutex_lock(&videodev_lock);ret = device_register(&vdev->dev);if (ret < 0) {    mutex_unlock(&videodev_lock);    pr_err("%s: device_register failed\n", __func__);goto cleanup;}vdev->dev.release = v4l2_device_release;

注册函数没有从头到尾持有 videodev_lock。它先在编号分配阶段持锁,预留成功后释放;到设备模型注册前,再次获取这把锁,并一直持有到注册标志设置完成。

这样,设备模型发布期间即使外部已经观察到节点,进入 v4l2_open() 的线程仍然需要等待同一把锁。等它取得锁后,才能在受保护的临界区内判断节点是否已经完成激活,并取得设备引用。

这并不意味着框架替驱动建立了全部硬件同步;它保护的是节点管理层的发布和打开交接。

9.3 为什么不是在这里直接调用 mknod()

V4L2 调用到 device_register(),就进入了通用设备模型。

通用路径把设备初始化并加入设备模型,建立 sysfs 表示。存在有效设备号时,添加设备的流程会涉及 devtmpfs 节点创建,并发出设备添加事件。

在启用相应机制并把 devtmpfs 挂载到 /dev 的系统上,基础设备文件由这一机制提供;设备管理服务还可以处理权限和别名。其他系统也可能使用不同的设备文件管理方式。

因此应当分层回答“谁创建了 /dev/videoX”:

V4L2 注册逻辑    决定节点名字、设备号、操作入口        ↓Linux 设备模型    发布设备及属性,触发节点与事件机制        ↓/dev 管理机制    使字符设备文件在相应文件系统视图中可见

cdev_add() 解决操作入口映射,device_register() 解决设备模型发布,/dev 中的可见文件则取决于相应节点管理机制。它们有关联,但不是同一个动作。

9.4 设备模型释放回调为何先回到核心

vdev->dev.release = v4l2_device_release;

这个回调收到的是 struct device *。核心先通过内嵌关系找到 video_device,清理全局表、字符设备和编号,再调用驱动提供的 vdev->release。

如果把设备模型最终回调直接当成驱动释放回调,核心维护的全局资源就没有正确的清理位置。

因此,释放路径同样有两层:

设备模型最终回调    → V4L2 核心清理公共关联    → 驱动回调按对象布局释放私有资源

十、注册第六阶段:保持上层对象,接入媒体关联,最后开放访问

10.1 节点存活期间,上层组织对象也可能继续被访问

v4l2_device_get(vdev->v4l2_dev);

节点的打开、关闭和最终清理都会访问上层 v4l2_device。因此,成功注册路径增加其引用,把节点生命期与上层组织对象的生命期连接起来。

注意,这不是 video_get(vdev)。两者保护的对象不同:

video_get(vdev)    → get_device(&vdev->dev)    → 保持当前节点设备模型对象v4l2_device_get(vdev->v4l2_dev)    → 保持上层 V4L2 组织对象

10.2 video_register_media_controller() 不是再次创建 /dev/videoX

ret = video_register_media_controller(vdev);

启用媒体控制器并具备相应 mdev 时,这个辅助函数把节点接入媒体拓扑。

对于普通视频节点,它为实体设置功能与名称,注册实体,创建记录 major/minor 的接口对象,并建立启用且不可变的接口连接。

其中 media_devnode_create() 创建的是媒体拓扑中的接口对象,不是 devtmpfs 中的字符设备文件。它的作用是让拓扑中的接口能够对应到已经确定的设备号。

M2M 节点在这条辅助路径上直接跳过,因为它需要另外组织媒体实体;没有 mdev 时也不在这里建立媒体拓扑。

实体注册完成仍不等于全部设备间连接已经建好。传感器、接收器、处理单元之间的链路,以及管线启停,还需要相应的其他逻辑。

10.3 最后的 set_bit() 是访问准入点

set_bit(V4L2_FL_REGISTERED, &vdev->flags);mutex_unlock(&videodev_lock);return0;

到这里,节点才进入核心认可的已注册状态。

从调用方看,注册函数返回后就可以使用节点;从并发系统看,另一个线程也可能在这次解锁后、调用方继续执行后续语句前,已经开始打开节点。

所以,驱动必须在调用注册接口前准备好回调依赖的对象、反向指针、锁和状态。

下面的顺序存在明显问题:

video_register_device(vdev, VFL_TYPE_VIDEO, -1);/* 太晚:节点已经可能进入 open/ioctl。 */initialize_state_used_by_callbacks();

正确的组织方式是先完成回调所依赖的初始化,再把节点发布出去。

10.4 当前实现中两个值得注意的行为

第一个与编号告警有关。

前面的搜索直接改写了 nr,随后执行 vdev->num = nr。后面的告警却判断:

if (nr != -1 && nr != vdev->num && warn_if_nr_in_use)    pr_warn(/* ... */);

沿正常路径,nr 和 vdev->num 已经相等,因此不能依赖这条分支报告最初申请的编号被替换。申请编号的接口意图和这个实现的实际告警效果,要分开理解。

第二个与媒体注册返回值有关。

ret = video_register_media_controller(vdev);set_bit(V4L2_FL_REGISTERED, &vdev->flags);mutex_unlock(&videodev_lock);return0;

这里没有检查 ret 并跳转失败清理。因此,返回 0 能说明这条函数路径完成了字符设备节点激活,但不能据此断定媒体拓扑关联也一定全部成功。

这不等于可以推断每种媒体注册失败都会造成同一种后果。辅助函数已经包含自己的部分回滚;具体失败后的拓扑和指针状态,需要结合失败位置及媒体对象的清理实现判断。


十一、注册失败:回滚的是已经取得的资源

11.1 公共清理分支的动作

cleanup:    mutex_lock(&videodev_lock);if (vdev->cdev)        cdev_del(vdev->cdev);    video_devices[vdev->minor] = NULL;    devnode_clear(vdev);    mutex_unlock(&videodev_lock);    vdev->minor = -1;return ret;

这个分支撤销字符设备关联、清空全局槽位、归还名称编号,并恢复无效 minor。

它没有调用 vdev->release(vdev),也没有统一销毁驱动提前建立的队列、控制处理器和私有状态。

11.2 为什么早期失败不跳到同一个标签

参数检查或编号搜索失败时,节点还没有进入全局表,不一定拥有可用 minor。直接进入依赖有效 minor 的公共清理分支,反而可能访问错误的数组位置。

因此,失败路径必须与资源取得阶段对应,不能把“所有错误都 goto cleanup”当作固定写法。

失败位置
已建立的关键资源
此处的处理
参数或类别检查
尚无编号与 cdev
直接返回 -EINVAL
名称或 minor 不足
尚未完成预留
解锁并返回 -ENFILE
cdev_alloc()
编号与全局表预留
公共清理归还预留
cdev_add()
编号预留、已分配 cdev
分支释放该 cdev 并置空,再清理预留
device_register()
编号、已添加 cdev
解锁后进入公共清理
媒体辅助注册
设备模型已注册
当前实现未据其错误转入公共清理

11.3 注册失败不走正常注销后的延迟释放路径

接口约定要求调用方处理注册失败后的节点存储,不能等一个不会发生的正常最终释放回调,也不应把失败对象当成成功节点再次注销。

但还要区分“这个接口的调用方契约”和“通用设备模型的失败清理规则”。

通用 device_register() 即使失败,也已经涉及设备初始化,通用规则要求正确归还设备引用。这里的公共清理标签没有显式出现 put_device(&vdev->dev),释放回调又在成功之后才设置,因此不能把它当作适用于任意新字符设备驱动的完整模板。

单独分析这个函数,只能确认它显式撤销了哪些资源,不能把未出现的设备模型回滚动作自行补成“已经自动完成”。

同样,公共清理调用 cdev_del() 后没有把成员统一清零。失败对象不应靠修改几个字段就原样重注册;重新进入注册路径前必须建立正确的新初始状态。


十二、open() 如何从文件路径到达当前节点

12.1 内核识别的是设备号,不是 videoX 字符串

文件路径解析得到字符设备 inode,其中 i_rdev 保存设备号。通用字符设备打开路径查找或取得对应的 cdev,再把 cdev->ops 接到当前文件对象。[^char]

由于注册阶段设置的是统一操作表,后续首先执行核心 v4l2_open(),而不是直接执行驱动的 open()。

open("/dev/video8")    → 字符设备 inode,设备号例如 81:0    → cdev    → file->f_op = 核心 v4l2_fops    → v4l2_open()    → 当前节点 vdev->fops->open()

12.2 video_devdata() 与注册阶段的全局表构成闭环

struct video_device *video_devdata(struct file *file){return video_devices[iminor(file_inode(file))];}

注册阶段写入:

video_devices[vdev->minor] = vdev;

访问阶段读取:

video_devices[iminor(file_inode(file))]

这就是从字符设备文件回到节点对象的关键连接。

这里没有使用 file->private_data 反查节点,也没有从路径字符串中提取数字。这个辅助函数依赖文件确实属于相应 V4L2 字符设备路径,不能用来无条件检测任意文件是否是视频设备。

12.3 核心打开函数的关键逻辑

mutex_lock(&videodev_lock);vdev = video_devdata(filp);if (vdev == NULL || !video_is_registered(vdev)) {    mutex_unlock(&videodev_lock);return -ENODEV;}video_get(vdev);mutex_unlock(&videodev_lock);if (vdev->fops->open) {if (video_is_registered(vdev))        ret = vdev->fops->open(filp);else        ret = -ENODEV;}if (ret)    video_put(vdev);

ret 初始为 0。核心先在同一临界区中查表、检查注册状态并增加引用,然后解锁,再调用驱动打开回调。

12.4 为什么“查到指针”以后不能立刻解锁

假设先查到 vdev,但没有增加引用就释放全局锁。另一个线程可能立即注销节点,并在最后一个引用消失时释放它。打开线程再对旧指针执行 video_get(),就已经失去对象有效性的保证。

因此,必须把下面三个动作当成一个整体:

查表 → 判断仍可打开 → 取得保持对象存活的引用

全局锁保护的是这个交接窗口。

12.5 为什么调用驱动 open() 前就释放全局锁

驱动打开过程可能分配内存、取得设备专用锁,或等待其他操作。如果一直持有管理所有节点的全局锁,一个节点的复杂打开过程就会扩大对其他节点管理操作的阻塞范围。

核心先拿到设备引用,保证节点不会因为解锁就消失,再把复杂工作交给驱动自己的同步机制处理。

这体现了锁与引用的不同职责:锁保护交接,引用保护存活。

12.6 第二次注册状态检查不是完整的硬件并发保护

第一次检查后已经解锁,设备可能随后注销,所以在存在驱动 open 回调时,核心再次检查注册状态。

但检查之后依旧可能发生注销。因此,不能据此声称“驱动回调与注销完全互斥”。驱动仍需正确处理硬件断开、流停止以及并发回调。

此外,第二次检查位于 if (vdev->fops->open) 内。如果没有提供打开回调,核心不会走这次检查,初始结果保持为 0。

12.7 file->private_data 应该保存什么

v4l2_open() 不会自动给它赋值。它由驱动打开路径或驱动选用的 v4l2_fh_open() 等辅助函数设置。

三种常用接口分别处理不同层次:

video_devdata(file)    → 当前节点的 video_devicevideo_drvdata(file)    → 节点上挂接的驱动私有对象file->private_data    → 当前这次打开的上下文

节点级对象由多次打开共享;打开级对象通常分别创建和清理。把二者混用,会直接破坏多进程或多次打开情况下的状态管理。

驱动打开失败时,还必须清理自己已经分配的临时资源。核心只负责撤销核心取得的节点引用,不能依赖随后一定发生一次正常文件关闭回调。

十三、read、mmap、ioctl 与 poll:统一转发也有细节差异

打开路径已经把核心操作表安装到 file->f_op。后续文件操作通过当前文件对象的操作表分发,不会为每次 read() 或 ioctl() 重新执行一遍字符设备打开流程。

13.1 read() 和 write():先检查回调,再检查注册状态

以读取为例:

structvideo_device *vdev = video_devdata(filp);int ret = -ENODEV;if (!vdev->fops->read)return -EINVAL;if (video_is_registered(vdev))    ret = vdev->fops->read(filp, buf, sz, off);

没有读取回调时,返回 -EINVAL;存在读取回调但节点已经注销时,返回 -ENODEV;通过检查后才交给驱动。

这里不负责保证返回完整一帧,也不决定是否阻塞等待,更不直接把 DMA 缓冲区复制到应用。实际语义由下一层实现决定。

13.2 mmap():转发映射,不负责自动创建缓冲区

if (!vdev->fops->mmap)return -ENODEV;if (video_is_registered(vdev))    ret = vdev->fops->mmap(filp, vm);

核心只判断回调和注册状态。映射什么内存、如何验证偏移、怎样维护缓冲区引用,属于驱动或其采用的缓冲区框架。

即使 vdev->queue 非空,也不会让这个包装函数自动选择某个 videobuf2 映射函数。驱动的 .mmap 必须建立相应连接。

13.3 ioctl():包装函数不直接解析所有 VIDIOC_*

if (vdev->fops->unlocked_ioctl) {if (video_is_registered(vdev))        ret = vdev->fops->unlocked_ioctl(filp, cmd, arg);} else {    ret = -ENOTTY;}

这里没有按 VIDIOC_QUERYCAP、VIDIOC_S_FMT 和 VIDIOC_QBUF 编写完整的命令分支,标准命令解析属于下一层 video_ioctl2() 等逻辑。

因此,发现 ioctl 返回 -ENOTTY 时,要沿链路判断究竟是缺少文件级 ioctl 回调,还是标准分发层认为命令不支持,或者具体驱动又返回了同一错误。仅凭最终错误码,不能断定失败位置。

13.4 poll():返回的是事件掩码,不是负 errno

__poll_t res = EPOLLERR | EPOLLHUP | EPOLLPRI;if (video_is_registered(vdev)) {if (!vdev->fops->poll)        res = DEFAULT_POLLMASK;else        res = vdev->fops->poll(filp, poll);}

注销状态下,包装函数返回错误、挂起和优先事件组成的掩码。

节点仍注册但没有 poll 回调时,返回默认掩码。这个默认结果并不等于框架已经检查了图像队列并确认有帧可取;需要精确表达帧可用性和事件等待时,仍需相应的 poll 实现。

13.5 为什么不能笼统地说“注销后所有接口都返回 ENODEV”

接口
缺少回调
已注销时的核心行为
read
 / write
-EINVAL
有回调时返回 -ENODEV
mmap-ENODEV
返回 -ENODEV
unlocked_ioctl-ENOTTY
有回调时返回 -ENODEV
poll
注册状态下返回默认掩码
返回 EPOLLERR | EPOLLHUP | EPOLLPRI
文件 release
没有驱动回调也继续归还引用
仍必须执行清理与 video_put()

检查顺序会影响结果。已经进入驱动的操作,也不会因为注册标志随后被清除就被自动回滚。

13.6 配置相关的补充入口

启用兼容 ABI 支持时,核心还提供 v4l2_compat_ioctl32 入口,处理兼容 ioctl 路径。它不是把任意兼容命令无条件原样交给某一个驱动回调。

在没有 MMU 的配置下,get_unmapped_area 包装函数会参与地址选择;有 MMU 的配置下,本文件将这一成员设为空。这个差异不能解读为“有 MMU 就不支持 mmap”,因为地址选择与具体映射是不同环节。


十四、注销:先停止接收访问,而不是立即销毁对象

video_unregister_device() 的主要逻辑是:

if (!vdev || !video_is_registered(vdev))return;mutex_lock(&videodev_lock);clear_bit(V4L2_FL_REGISTERED, &vdev->flags);mutex_unlock(&videodev_lock);if (test_bit(V4L2_FL_USES_V4L2_FH, &vdev->flags))    v4l2_event_wake_all(vdev);device_unregister(&vdev->dev);

它主要完成三项工作:清除访问准入状态、唤醒标准事件等待者,以及从设备模型撤下节点并归还相应注册引用。

14.1 为什么清除注册标志要和 open() 使用同一把锁

打开线程在锁内检查注册状态并取得引用;注销线程在锁内清除注册状态。

因此,这两个交接动作能够形成明确顺序。

如果打开线程先取得锁并完成检查与加引用,注销不能让它手中的节点对象立刻失效。

如果注销线程先清除注册标志,后续打开线程看到状态不再可用,就不能再通过这个正常打开路径取得新的节点引用。

这就是“从此不再接收新打开”的管理边界。

14.2 为什么要唤醒事件等待者

使用标准 v4l2_fh 事件机制的应用可能正在等待事件。如果设备退出时只清除标志,不通知等待者,它们可能继续等待一个已经不再提供事件的节点。

v4l2_event_wake_all() 用于推动这些等待者重新检查状态。

但这不是“唤醒驱动中所有可能的等待”。自定义等待队列、阻塞的缓冲区出队、DMA 完成等待等,需要相关驱动和缓冲区框架按各自机制处理。

14.3 device_unregister() 撤下对象,不保证内存立刻消失

设备模型撤销注册后,注册时持有的引用被归还。如果还有成功打开的文件持有节点引用,最终释放回调就不能立即执行。

如果没有剩余引用,最终释放也可能在注销过程中就发生。

所以,调用方既不能假设“注销返回以后对象一定还活着”,也不能假设“注销返回以后所有旧文件一定已结束”。

注销后的代码是否还能访问 vdev,取决于调用方是否另外持有有效的生命期保证,而不是取决于函数是否刚刚返回。

14.4 不能把“重复调用会提前返回”当成并发注销协议

函数入口虽然检查注册状态,但检查发生在获取 videodev_lock 之前,不能据此保证两个线程同时注销同一个对象是安全的。

更不能在对象可能已经最终释放后再次调用它:第一次判断就需要读取对象内存。

注销动作应由明确的设备退出流程协调,不能靠一个状态判断代替调用方的所有权和并发管理。


十五、关闭文件与释放节点:几个 release 必须分清

15.1 先看名字相似的不同接口

接口
参数或所属对象
触发条件
主要职责
v4l2_release()
核心 file_operations
文件对象进入最终释放
转发本次关闭并归还节点引用
vdev->fops->release()
驱动文件操作
核心关闭路径转入驱动
清理本次打开的上下文和相关资源
v4l2_device_release(struct device *)
节点内嵌 dev 的回调
节点设备引用归零
清理 V4L2 公共登记资源
vdev->release(vdev)
节点对象回调
核心最终清理之后
按对象分配方式回收节点及私有资源
v4l2_dev->release(v4l2_dev)
上层组织对象回调
相应上层引用归零
回收整组设备的组织对象

相邻 v4l2-device.c 中还有一个接收 struct kref * 的静态 v4l2_device_release()。它与当前文件里接收 struct device * 的同名静态函数不在同一翻译单元,也不管理同一个引用对象。

15.2 核心文件关闭路径先让驱动清理,再归还节点引用

if (vdev->fops->release) {if (v4l2_device_supports_requests(vdev->v4l2_dev)) {        mutex_lock(&vdev->v4l2_dev->mdev->req_queue_mutex);        ret = vdev->fops->release(filp);        mutex_unlock(&vdev->v4l2_dev->mdev->req_queue_mutex);    } else {        ret = vdev->fops->release(filp);    }}video_put(vdev);

先执行驱动关闭回调,是因为驱动可能还需要访问 vdev、队列及相关节点状态;之后才能归还核心在成功打开时取得的引用。

它不会因为节点已经注销就跳过关闭回调。设备不可再读写,不代表旧文件上下文不需要清理。

15.3 Request API 为什么在关闭路径上需要额外锁

关闭可能触发流式操作取消。如果此时另一条路径正在提交新的 media request,就可能与取消逻辑交错。

因此,当设备支持 requests 时,关闭路径用 req_queue_mutex 串行化相关操作。

这把锁保护的是请求提交与关闭取消之间的关系,不是替代 videodev_lock,也不是自动覆盖所有普通队列操作。

15.4 即使驱动关闭回调返回错误,也必须归还引用

核心无条件执行 video_put(vdev)。

文件最终释放不是一个可以由驱动返回错误就撤销的动作。错误返回不能把已经关闭的文件重新变成有效对象,更不能成为保留核心设备引用的理由。

否则每次关闭失败都会泄漏引用,节点可能永远无法最终释放。

15.5 一次 close(fd) 不一定就是一次核心 release

两个独立 open() 通常得到两个文件对象。dup()、继承和其他文件引用机制则可以让多个引用关联同一个文件对象。

VFS 在文件对象的最后一个引用归还后,才进入相应最终释放路径。因此,文件描述符的数量、调用 close() 的次数和驱动 release 的调用次数并不简单一一对应。[^file]

存在文件映射或其他额外引用时,也可能延后文件最终释放。讨论节点什么时候销毁,必须看实际引用是否全部归还,而不是只数应用显式写了几次 close()。


十六、最终释放:为什么 minor 和名称编号要保留到最后

16.1 从内嵌 dev 找回节点

structvideo_device *vdev = to_video_device(cd);structv4l2_device *v4l2_dev = vdev->v4l2_dev;

设备模型最终释放回调先恢复外层对象,并保存上层组织对象指针。

保存局部指针很重要:后面调用 vdev->release(vdev) 时,驱动可能立即释放节点内存;之后不能再通过 vdev 读取任何成员。

16.2 持全局锁撤销公共登记

mutex_lock(&videodev_lock);if (WARN_ON(video_devices[vdev->minor] != vdev)) {    mutex_unlock(&videodev_lock);return;}video_devices[vdev->minor] = NULL;cdev_del(vdev->cdev);vdev->cdev = NULL;devnode_clear(vdev);mutex_unlock(&videodev_lock);

这里才真正归还全局槽位和名称编号。

一致性检查要求当前 minor 对应的全局表项仍然是正在释放的这个节点,防止在公共登记已经异常时继续盲目拆除其他对象的映射。

cdev 成员随后被置空,避免驱动最终回调把它误当成仍然有效的字符设备关联。

16.3 为什么不能在 video_unregister_device() 中立刻清空全局表

旧文件的关闭路径还会执行:

video_devdata(filp)

而这个函数仍然通过 inode 中的 minor 查找全局表。

假设注销后立即清空表项并把 minor 分配给新节点:旧文件后来关闭时,可能根据旧 inode 的 minor 查到新节点,从而把旧文件的清理交给完全错误的对象。

因此,在这条实现中,需要一直保留:

旧文件 inode 中的 minor    → video_devices[minor]    → 原来的 video_device

直到旧节点的设备引用全部归还,才撤销这个关系并允许编号复用。

延迟回收 minor 不是为了保留一个好看的名字,而是为了保持旧文件到旧对象的映射不变。

16.4 媒体关联为什么也在最终清理中撤销

启用媒体控制器的相应路径上,核心移除接口对象、接口连接及节点实体。

这些动作清理的是媒体框架登记关系。驱动自身初始化的其他实体资源或外层存储,仍然要遵循其对应清理规则,不能假设 media_device_unregister_entity() 等同于销毁所有相关内存。

16.5 调用驱动释放后,为什么还要有条件地归还上层引用

if (v4l2_dev->release == NULL)    v4l2_dev = NULL;vdev->release(vdev);if (v4l2_dev)    v4l2_device_put(v4l2_dev);

核心并没有不加条件地执行最后一次上层 put。

一些旧式驱动没有设置 v4l2_device 的释放回调,而是在节点释放回调中直接销毁了包含上层对象的外层结构。若之后继续对已被销毁的上层对象执行 put,就会访问无效内存。

因此,这个实现提前检查上层是否具备释放回调,并按相应分支决定是否归还上层引用。理解正常对称关系时,不能忽略这个兼容分支。

16.6 完整退出时序

没有旧文件等剩余引用时,最终回调也可以在 device_unregister() 归还注册引用时触发,不必等待一个未来的 close()。


十七、把整个生命周期画成状态机

这里存在两个容易忽略的中间状态。

编号已预留,但尚未激活。 全局表可以非空,设备号也可能已经接入 cdev,但核心仍不允许把它当成完整节点访问。

已经注销,但尚未最终释放。 对外设备入口可以被撤下,但旧文件依然需要查到原对象并完成关闭。

这两个中间状态解释了为什么全局表非空不等于已注册,minor 非负不等于可访问,节点名称消失也不等于节点内存已经销毁。

17.1 用一个双打开场景看引用配对

只考虑注册路径和成功打开路径持有的节点设备引用,不计其他临时引用,可得到下面的示意过程:

时刻
动作
节点引用示意
核心状态
T0
节点完成设备模型注册
1
已注册
T1
文件 A 成功打开
2
已注册
T2
文件 B 成功打开
3
已注册
T3
节点注销并归还注册引用
2
不再接受新访问,旧对象仍在
T4
文件 A 最终释放
1
仍等待文件 B
T5
文件 B 最终释放
0
触发节点最终清理

这是引用关系的简化推演,不是实际系统的固定计数公式。它用于说明:注销归还的是注册引用,文件关闭归还的是成功打开留下的引用;只有相应引用全部结束,节点才能销毁。

17.2 三种保护,缺一不可

机制
保护内容
不能替代的事情
videodev_lock
查表、编号与生命期交接的临界区
不能让解锁后的对象自动存活
V4L2_FL_REGISTERED
当前是否接受新的核心访问
不能保证硬件尚未断开,也不阻止所有在途回调
节点引用计数
对象内存和相关生命期
不能自动停止 DMA、同步中断或保证每个操作成功

设备专用互斥锁、队列锁、请求队列锁以及硬件退出同步,还需要分别覆盖相应的数据和执行过程。


十八、从字段初始化到注册:一段节点级代码如何组织

下面展示独立分配 video_device 时的节点注册组织方式。example_device 的上层 V4L2 对象、队列、锁和回调表都已初始化,并保证覆盖节点及打开上下文的使用期;它们不是由这个辅助函数临时创建的。

staticintexample_register_node(struct example_device *ctx){structvideo_device *vdev;int ret;    vdev = video_device_alloc();if (!vdev)return -ENOMEM;    strscpy(vdev->name, "Example Capture", sizeof(vdev->name));    vdev->v4l2_dev = &ctx->v4l2_dev;    vdev->fops = &example_fops;    vdev->ioctl_ops = &example_ioctl_ops;    vdev->release = video_device_release;    vdev->vfl_dir = VFL_DIR_RX;    vdev->device_caps = V4L2_CAP_VIDEO_CAPTURE_MPLANE |                        V4L2_CAP_STREAMING;    vdev->queue = &ctx->queue;    vdev->lock = &ctx->lock;    video_set_drvdata(vdev, ctx);/* 回调可能读取的反向指针必须在发布节点前建立。 */    ctx->vdev = vdev;    ret = video_register_device(vdev, VFL_TYPE_VIDEO, -1);if (ret) {        ctx->vdev = NULL;        video_device_release(vdev);return ret;    }return0;}

这是节点层的接口组织示例,不是一个完整可编译的视频驱动;example_device、example_fops、example_ioctl_ops 以及实际采集实现均由设备驱动提供。

其中有几个重要约束。

fops 和 ioctl_ops 必须在回调可能被执行时保持有效,不能把注册函数栈上的临时操作表挂进去。

队列能力、方向和格式回调必须匹配。例如声明多平面采集能力时,相关格式和队列接口必须确实使用相应的多平面类型,不能只修改能力位。

ctx->vdev 在节点发布前写入,是因为另一个线程可能在注册返回后的极短窗口就进入回调。节点级私有数据也必须提前关联。

成功注册后,不应在退出函数里把 video_unregister_device(vdev) 与 video_device_release(vdev) 无条件连着调用。前者负责停止访问并撤销注册,后者是最终存储释放策略;成功路径应等待框架按引用状态调用最终回调。

如果 ctx 的生命期不能保证覆盖仍打开的节点文件,则仅使用 video_device_release 释放 vdev 还不够。需要通过上层对象引用或专门的释放回调,把 ctx 的清理推迟到所有依赖结束之后。


十九、辅助机制如何接回这条主线

19.1 sysfs 属性不是额外的一套节点对象

video_class 关联 video_device_groups,统一提供节点的 name、index 和 dev_debug 属性。

属性回调收到 struct device *,通过 to_video_device() 找回同一个节点,然后读取或修改对应成员。

例如,name 读取的是描述性名称,index 读取同组索引,dev_debug 解析新的调试位图后写回节点。它们都不是再分配一套设备身份。

19.2 优先级函数维护的是“各等级有多少打开上下文”

v4l2_prio_change() 先校验新优先级。如果与当前优先级相同,直接成功返回;否则增加新等级计数,并在旧等级有效时减少旧等级计数,最后更新本地优先级。

v4l2_prio_max() 从记录、交互、后台等等级中找出当前实际被占用的最高优先级。

v4l2_prio_check() 则根据当前文件句柄的本地优先级与全局最高优先级比较,决定是否返回 -EBUSY。

这些接口不会自动应用到所有操作。采用标准 ioctl 分发时,只有带有相应优先级检查要求、且具备标准文件句柄上下文的操作才进入对应检查路径。

19.3 媒体管线包装函数不等于硬件流启动

video_device_pipeline_start()、video_device_pipeline_stop() 等接口把节点转换到对应的媒体 pad,再转入媒体管线框架。

这组包装接口要求节点实体具有符合要求的单个 pad。开始函数不满足条件时返回错误,停止或查询路径也会检查条件。

它们用于媒体管线状态和关联管理,不能把函数名中的 start 理解成直接启动某个摄像头或 DMA。实际硬件流控制仍由相应驱动链路完成。


二十、回到最初的问题:一个 /dev/videoX 到底由什么组成

从文件系统看,它是一个字符设备文件;从字符设备层看,它是一个设备号到操作表的映射;从 V4L2 核心看,它是一个 video_device 节点对象;从设备模型看,它又是一个带有设备名称、父对象和引用计数的 struct device。

这些不是四个独立的“视频设备”,而是同一个节点在不同内核层次中的表示。

__video_register_device() 把这些表示连接起来:先建立有效初始状态,再预留编号,生成标准接口集合,注册 cdev 和设备模型对象,连接上层生命期与媒体信息,最后开放访问。

v4l2_fops 沿这些关系把文件操作送到当前节点的驱动实现;video_unregister_device() 先关闭访问入口;引用管理则保证旧文件仍能完成清理;最终释放函数再拆除公共关联并交还存储所有权。

整个文件最核心的设计,不是如何搬运一帧图像,而是如何让“设备身份、访问入口和对象生命期”始终保持一致。

只要这三者的关系清楚,后续的 ioctl 分发、videobuf2 队列与硬件采集实现,就有了明确的入口与边界。

相关学习资料