ARTICLE · 1290799
V4L2 源码深度解析(十六):从一个采集节点到整套设备——v4l2_device.c 深度解析
开篇:/dev/videoX 背后不只有一个 video_device
前面已经沿着 v4l2-dev.c 追过视频节点注册,也沿着 VB2 走过图像缓冲区从申请、排队到完成和释放的过程。现在把视角往上移一层:一个摄像头系统通常不只有采集节点,还可能包含 Sensor、图像处理子模块、多个视频节点,以及描述这些模块连接关系的媒体设备。
这些对象不是靠名字相似就自动组成一套设备。驱动需要建立明确的对象关联,并规定它们怎样注册、怎样摘除,以及什么时候才可以释放承载它们的内存。
v4l2_device.c 负责 V4L2 设备组织中的一部分基础工作:初始化 struct v4l2_device、维护其子设备列表、登记和注销子设备,并通过引用计数与释放回调协调外层对象的寿命。它不是/dev/videoX 的创建实现,也不负责自动建立整张媒体拓扑。
本文以 v4l2-device.c 为主线,使用 VIMC 的 vimc-core.c、vimc-common.c 和 vimc-capture.c 观察实际连接。先把对象关系讲清,再沿注册、子设备登记、节点发布和退出路径逐步展开。

图 1:v4l2_device 是组织对象,不是新的设备节点。video_device 和 v4l2_subdev 的职责不同;两者都可能关联媒体实体,但不是同一种对象。
1. 先把四个对象分开
1.1 struct video_device:一个对外的视频节点
video_device 是 V4L2 视频节点的核心对象。前面已经分析过它怎样连接 fops、ioctl_ops、queue,并经 video_register_device() 注册成 /dev/videoX。
一个设备系统可以有多个 video_device:例如不同采集输出、不同处理结果,或输入与输出队列。它描述的是一个可通过视频节点操作的接口,不代表整套摄像头硬件。
1.2 struct v4l2_device:组织 V4L2 对象的公共锚点
v4l2_device 常由桥接驱动或整套媒体设备的外层对象持有。它包含子设备链表、用于保护链表的锁、优先级状态、引用计数,以及与父 struct device、控制处理器和 media_device 的关联。
可以把它理解为一处组织关系的锚点:子设备注册时,框架会把子设备挂到它的 subdevs 列表;视频节点则通过 vdev->v4l2_dev 指向所属的 V4L2 设备。
但它不是“把所有东西都放在里面”的容器。实际的 video_device、子设备、媒体实体和像素缓冲区,仍由各自的对象和生命周期规则管理。
1.3 struct v4l2_subdev:一个可被桥接驱动调用的子模块
子设备可以代表 Sensor、解码器、Debayer、Scaler 或其他媒体处理模块。它通过 ops 向框架提供操作入口,也保存所属 v4l2_device 的关联。
注册一个 subdev 不一定会创建 /dev/v4l-subdevX。只有驱动选择暴露节点、设置相应标志并调用节点注册辅助函数时,才会创建独立子设备节点。即使没有节点,桥接驱动仍然可以在内核里通过子设备操作接口调用它。
1.4 struct media_device:媒体拓扑与媒体接口
media_device 属于 Media Controller 体系,负责媒体设备层面的拓扑和关联对象。它与 v4l2_device 可以通过指针关联,但二者不是同一个对象,也不能互相替代。
video_device | ||
v4l2_device | /dev/videoX | |
v4l2_subdev | /dev/v4l-subdevX | |
media_device |
关键点:这些对象之间是显式关联,不是包含一个对象就自动创建另一个对象。
2. v4l2_device_register() 实际初始化了什么
打开 v4l2-device.c,首先看到的入口是:
intv4l2_device_register(struct device *dev,
struct v4l2_device *v4l2_dev)
{
if (v4l2_dev == NULL)
return -EINVAL;
INIT_LIST_HEAD(&v4l2_dev->subdevs);
spin_lock_init(&v4l2_dev->lock);
v4l2_prio_init(&v4l2_dev->prio);
kref_init(&v4l2_dev->ref);
get_device(dev);
v4l2_dev->dev = dev;
...
}
这段代码主要完成初始化和关联,不会遍历设备树去寻找所有 Sensor,也不会自动创建媒体链路。
2.1 初始化子设备链表
INIT_LIST_HEAD(&v4l2_dev->subdevs);
这会把 subdevs 初始化为空链表。后续 v4l2_device_register_subdev() 成功时,才把对应的 sd->list 加入链表。
所以调用 v4l2_device_register() 之后,准确的结论是“具备登记子设备的基础结构”,不是“子设备已经全部注册”。
2.2 初始化锁和优先级状态
spin_lock_init(&v4l2_dev->lock);
v4l2_prio_init(&v4l2_dev->prio);
lock 保护子设备列表的并发访问;prio 为 V4L2 优先级管理提供状态。不要把这个自旋锁说成保护整条图像管线或所有控制操作的总锁。具体字段由具体操作路径决定。
2.3 初始化 V4L2 对象自己的引用计数
kref_init(&v4l2_dev->ref);
v4l2_device 使用自己的 kref。它与父 struct device 的设备引用计数是两套不同的计数。
2.4 持有父设备引用,并建立驱动数据关联
get_device(dev);
v4l2_dev->dev = dev;
if (!dev_get_drvdata(dev))
dev_set_drvdata(dev, v4l2_dev);
当 dev 非空时,框架取得父设备引用,并把父设备保存到 v4l2_dev->dev。如果父设备的 driver data 尚未被占用,才会把 v4l2_dev 放进去;如果驱动已经使用 drvdata 保存别的对象,这里不会覆盖它。
还有一个特殊分支:dev == NULL 时,调用方必须事先填写 v4l2_dev->name,函数不会替它从父设备名称生成名字。
若 dev == NULL 且名称为空,函数会返回 -EINVAL。此时虽然此前的初始化语句已经执行,但注册并未成功;驱动应按这条失败路径的对象所有权处理,不能将它当成一个成功注册后再走通用 remove 的实例。
2.5 设备名称怎样生成
若 v4l2_dev->name 为空且父设备有效,框架使用驱动名和设备名拼出一个默认名称。这个名称用于标识 V4L2 设备对象,不等于 /dev/videoX 的节点编号。
因此,下面三个名称来源要分开理解:
v4l2_device的名称:标识一组 V4L2 对象。video_device的名称与编号:用于视频节点发布。media_device的model、bus_info等字段:用于描述媒体设备信息。
3. v4l2_device_register_subdev() 怎样登记一个子设备
VIMC 的公共辅助函数 vimc_ent_sd_register() 会先初始化 subdev、设置名称和私有数据,再初始化 media entity,最后调用 v4l2_device_register_subdev()。
v4l2_subdev_init(sd, sd_ops)
↓
设置 entity.function、entity.ops、owner、name
↓
v4l2_set_subdevdata(sd, ved)
↓
media_entity_pads_init(...)
↓
v4l2_device_register_subdev(v4l2_dev, sd)
注意 media_entity_pads_init() 在子设备注册之前。它准备 entity 的 pad 关系;真正登记 entity 到 media_device,则发生在 v4l2_device_register_subdev() 的相应分支中,前提是 v4l2_dev->mdev 非空且编译启用了 Media Controller。

3.1 入口检查与模块所有者保护
v4l2_device_register_subdev() 首先检查:v4l2_dev、sd 是否有效,sd->v4l2_dev 是否已经设置,以及子设备名称是否为空。
随后处理 sd->owner_v4l2_dev。如果子设备模块与所属父设备驱动属于同一个模块所有者,框架不再对该子设备模块额外执行 try_module_get();否则尝试取得模块引用,避免子设备仍登记在媒体设备上时,其实现模块被卸载。
这段逻辑保护的是模块代码的寿命。它不是设备节点引用,也不是 v4l2_device 的 kref。
3.2 关联所属设备并合并控制处理器
sd->v4l2_dev = v4l2_dev;
err = v4l2_ctrl_add_handler(v4l2_dev->ctrl_handler,
sd->ctrl_handler, NULL, true);
子设备通过 sd->v4l2_dev 记录所属设备。若双方有控制处理器,框架会尝试把子设备的控制项加入上层控制处理器。
这不意味着控制处理器被复制了一份,也不意味着每个子设备的控制都自动在应用层以同样方式暴露。合并失败会进入错误路径,清理刚建立的关联并释放需要归还的模块引用。
3.3 Media Controller 开启时登记 entity
当配置支持 Media Controller,且 v4l2_dev->mdev 非空时,框架调用:
media_device_register_entity(v4l2_dev->mdev, &sd->entity);
这是将子设备的媒体实体登记到媒体设备的动作。它与后续创建 pad link 不同:登记 entity 不会自动把它连到 Sensor 上游或 Capture 下游。 Link 需要驱动或其他拓扑配置代码显式创建。
3.4 调用 internal_ops->registered,最后加入列表
如果子设备实现了 internal_ops->registered,框架会调用它。只有该回调成功后,才在 v4l2_dev->lock 保护下执行 list_add_tail(&sd->list, &v4l2_dev->subdevs)。
这个顺序有实际意义:回调失败时,子设备不会被当作成功登记的列表成员;如果此前已经登记了 media entity,错误路径会撤销该 entity 登记,再清空 sd->v4l2_dev 并处理模块引用。
可以把成功注册理解为多个动作全部完成后的结果,而不是 sd->v4l2_dev = v4l2_dev 这一行执行后就已经完成。
4. 子设备登记成功后,是否一定有 /dev/v4l-subdevX?
不一定。
v4l2_device_register_subdev() 主要登记内核中的子设备对象。如果要让应用通过独立的子设备节点访问它,还需要调用子设备节点注册辅助函数,并且该 subdev 需要设置 V4L2_SUBDEV_FL_HAS_DEVNODE 标志。
__v4l2_device_register_subdev_nodes() 会遍历 v4l2_dev->subdevs,跳过没有 HAS_DEVNODE 标志的对象,为需要暴露的 subdev 分配一个 video_device 作为节点包装对象,设置其 fops 为 v4l2_subdev_fops,然后通过 __video_register_device() 注册为 VFL_TYPE_SUBDEV。
它还会将这个节点对应的设备号信息写入 entity,并在存在 media_device 时创建启用且不可变的 interface link。
这里有三点容易误读:
内核子设备对象与子设备节点不是同一个对象。 节点包装对象会保存对应的 v4l2_subdev指针。并非每个 subdev 都需要独立节点。 桥接驱动可以只在内核中调用它的操作。 子设备节点也复用了 video_device设备模型的注册机制,但其文件操作是 subdev 专用操作。 不应因此把它当成普通 CAPTURE 节点。
这一层和前面已经分析过的 v4l2-dev.c 有直接联系:__video_register_device() 负责视频设备节点注册;v4l2-device.c 决定哪些子设备需要包装成节点并触发注册。
5. 用 VIMC 看一遍真实的注册顺序
VIMC 适合观察完整对象如何组合,因为它在同一个驱动里建立 Sensor、Debayer、Scaler、Capture 等实体。这里要特别注意:VIMC 中 vimc_add_subdevs() 这个函数名有一定历史原因,它遍历配置中的各类 entity;其中既有通过 v4l2_device_register_subdev() 登记的 subdev,也有通过 video_register_device() 发布的 Capture 视频节点。
5.1 vimc_probe() 先建立外层对象和 media device
在 vimc_probe() 中,驱动分配 struct vimc_device,设置管线配置,并先建立两个对象之间的指针关系:
vimc->v4l2_dev.mdev = &vimc->mdev;
strscpy(vimc->mdev.model, VIMC_MDEV_MODEL_NAME,
sizeof(vimc->mdev.model));
snprintf(vimc->mdev.bus_info, sizeof(vimc->mdev.bus_info),
"platform:%s", VIMC_PDEV_NAME);
vimc->mdev.dev = &pdev->dev;
media_device_init(&vimc->mdev);
设置 v4l2_dev.mdev 只是建立指针关联;media_device_init() 初始化媒体设备对象,也不等同于立即发布 /dev/mediaX。发布动作稍后由 media_device_register() 完成。
5.2 vimc_register_devices() 的实际顺序
在该实现中,注册过程可以概括为:
vimc_probe()
├─ 分配 vimc_device
├─ 关联并初始化 media_device
└─ vimc_register_devices()
├─ v4l2_device_register()
├─ 分配 ent_devs 数组
├─ vimc_add_subdevs() 创建配置中的各类实体
├─ vimc_create_links() 建立 data / ancillary links
├─ media_device_register() 发布媒体设备
└─ v4l2_device_register_subdev_nodes()
发布需要独立节点的 subdev

图 3:这是当前 VIMC 实例的执行顺序,不是所有 V4L2 驱动都必须照搬的唯一模板。
这里有一个重要区别:VIMC 的 Capture 节点在其实体创建函数中直接调用 video_register_device();子设备节点则在媒体设备注册后,通过 v4l2_device_register_subdev_nodes() 统一处理。不能把这两类节点说成完全相同的注册路径。
5.3 为什么要先建立实体和连接,再发布媒体设备?
VIMC 在发布 media_device 之前,已经创建了实体和 link。这样,当媒体设备接口对外可见时,内核中的拓扑对象已经按该实例的配置准备好。
但“拓扑对象已经建立”仍不等于每条链路都在所有设备上可随意切换,也不意味着格式已经自动协商完成。link 的标志来自 vimc-core.c 中的配置表;格式协商和管线验证需要单独分析。
5.4 注册失败时不能只写一个 kfree(vimc)
vimc_register_devices() 根据失败发生的位置进入不同清理标签:可能需要移除已建 links、注销并释放已经创建的实体、释放 ent_devs 数组,以及注销 v4l2_device。如果 media_device_register() 已成功而后续 subdev 节点注册失败,还要先注销 media device。
VIMC 将 v4l2_dev.release 回调放在 vimc_register_devices() 成功返回后才设置。因此注册失败时,由 vimc_probe() 直接回滚各个已经分配的对象;成功后则使用引用计数和 release 回调管理外层对象。这是该实例的明确设计,不应推广成所有驱动都必须在完全相同的位置设置回调。
6. disconnect()、unregister() 与 put() 有什么区别?
这三个操作经常被笼统叫成“释放设备”,但它们处理的关系不同。
6.1 v4l2_device_disconnect():断开父设备关联
voidv4l2_device_disconnect(struct v4l2_device *v4l2_dev)
{
if (v4l2_dev->dev == NULL)
return;
if (dev_get_drvdata(v4l2_dev->dev) == v4l2_dev)
dev_set_drvdata(v4l2_dev->dev, NULL);
put_device(v4l2_dev->dev);
v4l2_dev->dev = NULL;
}
它会在 drvdata 仍指向当前 v4l2_device 时清空该关联,归还先前取得的父设备引用,并将 v4l2_dev->dev 置空。
它不会在这里遍历并注销全部 subdev,也不会直接释放 v4l2_device 的内存。
6.2 v4l2_device_unregister():撤销组织关系
该函数先检查指针和名称,避免对空对象或已经注销的对象重复执行主体操作。随后调用 v4l2_device_disconnect(),再遍历 subdevs 列表,逐个调用 v4l2_device_unregister_subdev();对于标记为 I²C 或 SPI 子设备的对象,还会调用对应总线辅助注销路径。最后将名称首字符清零,作为已经注销的标记。
子设备注销本身会从 subdevs 列表摘除对象,调用 internal_ops->unregistered(如果存在),清除 sd->v4l2_dev,并在 Media Controller 开启时注销 entity。若存在独立 devnode,则通过 video_unregister_device() 撤销节点;否则调用子设备释放辅助路径。
6.3 v4l2_device_put():归还 V4L2 对象引用
staticvoidv4l2_device_release(struct kref *ref)
{
structv4l2_device *v4l2_dev =
container_of(ref, structv4l2_device, ref);
if (v4l2_dev->release)
v4l2_dev->release(v4l2_dev);
}
intv4l2_device_put(struct v4l2_device *v4l2_dev)
{
return kref_put(&v4l2_dev->ref, v4l2_device_release);
}
put() 减少 v4l2_device 自己的 kref。只有引用计数降到零时,才会调用上面的释放分发函数;而释放分发函数只有在 v4l2_dev->release 非空时才调用驱动提供的回调。
unregister() 不等于 put(),put() 也不负责注销子设备。 前者撤销组织关系,后者归还引用。驱动必须按自己的分配方式和引用配对安排二者。
7. 两套引用计数:父 device 与 v4l2_device 的寿命
这一点决定了 remove 之后对象能否安全存活一段时间。
get_device(dev)/put_device(dev)操作的是 Linux 设备模型中的父struct device引用。v4l2_device_get()/v4l2_device_put()操作的是struct v4l2_device的kref。try_module_get()/module_put()保护的是子设备实现所在模块的代码寿命。
它们保护不同的资源,不能互相替代。
在当前 v4l2-dev.c 的注册路径中,__video_register_device() 会对所属 v4l2_device 增加引用;视频节点最终释放时,再按相应条件归还引用。因此驱动注销视频节点后,节点的最终 release 可能因为仍有引用而延后执行。驱动必须保证 v4l2_device 及其外层对象在引用归零前仍然有效。

7.1 VIMC 怎样使用 release 回调
VIMC 的 vimc_v4l2_dev_release() 使用 container_of() 从 v4l2_device 找回外层 vimc_device,然后释放子设备私有对象、清理 media device、释放 ent_devs 数组,最后释放外层 vimc_device。
这解释了为什么它要在成功注册之后才设置 v4l2_dev.release:在注册失败的路径里,VIMC 直接回滚各个已经分配的对象;在成功运行并进入 remove 的路径里,则通过 unregister、put 和最终 release 配合清理。
不要把这个 release 回调误认为 v4l2_device_unregister() 的必然同步调用。它是 kref 归零时的释放入口,具体何时执行取决于所有相关引用何时归还。
8. 从 VIMC 的 remove 看退出顺序
VIMC 的 remove 路径大致是:
vimc_unregister_subdevs(vimc);
media_device_unregister(&vimc->mdev);
v4l2_device_unregister(&vimc->v4l2_dev);
v4l2_device_put(&vimc->v4l2_dev);
对应关系如下:
vimc_unregister_subdevs()调用各实体类型自己的注销函数。Capture 类型会注销其video_device,子设备类型会注销对应 subdev。media_device_unregister()撤销媒体设备对外发布。v4l2_device_unregister()断开父设备关联,并清理仍在 V4L2 子设备列表中的对象。v4l2_device_put()归还 VIMC 持有的基础引用;如果引用归零,触发vimc_v4l2_dev_release()。

要注意,VIMC 的实体注销回调会先注销它们自己的节点或子设备,所以调用 v4l2_device_unregister() 时,列表中可能已经没有这些 subdev。v4l2_device_unregister() 自身仍实现了遍历子设备的兜底逻辑,但不能把这理解成驱动可以任意重复释放同一对象。
节点注销和最终释放也不是一个时刻:设备模型的引用可能让 video_device 的 release 延后执行;这也是 v4l2_device 引用计数必须与节点寿命协同的原因。
9. 失败回滚为什么必须按已完成阶段处理
注册过程不是一次不可分割的操作。不同阶段失败时,已建立的资源集合不同,回滚路径也不能完全相同。

以当前 VIMC 的实现为例:
如果 v4l2_device_register()失败,尚未开始创建实体,外层按 probe 失败路径返回。如果创建某个实体失败, vimc_add_subdevs()会先注销并释放已创建的实体,再把错误返回给上层。如果创建 link 失败,需要移除此前已建立的 links,再注销、释放实体。 如果 media_device_register()成功,但子设备节点注册失败,需要先注销已经发布的 media device,再回滚实体和 V4L2 设备关联。成功注册后进入 remove,则使用正常注销与引用归还路径,而不是复用任意一个 probe 错误标签。
读这类代码时,建议为每一步列出“此时已经成功创建了什么”,再对照错误标签确认对应资源是否有撤销动作。仅仅搜索 kfree() 不足以判断回滚是否完整;媒体实体、link、视频节点、父设备引用和模块引用都可能有各自的释放入口。
10. v4l2_device 与 Media Controller:有关联,但分工不同
VIMC 在 vimc_probe() 中把 vimc->v4l2_dev.mdev 指向 vimc->mdev。之后子设备注册时,如果 mdev 存在,框架会登记子设备的 media_entity。另一方面,Capture 节点拥有自己的 video_device.entity,并在视频节点注册时接入媒体框架。
这说明一套设备里至少有两种关系:
V4L2 组织关系: v4l2_device维护子设备列表,并关联视频节点和子设备。媒体拓扑关系: media_device组织实体、接口、pad 和 link,表达数据连接及其他媒体关系。
子设备登记到 v4l2_device 并不自动建立 Sensor 到 Capture 的 data link。VIMC 在 vimc_create_links() 中根据配置表显式调用 media_create_pad_link() 和 media_create_ancillary_link()。
因此,后续学习 mc-entity.c 时,需要从 entity/pad/link 继续往下追;不要在 v4l2-device.c 这一篇把拓扑机制提前全部展开。
11. 用源码做一次自查:遇到问题应该从哪里看
情况一:v4l2_device_register_subdev() 返回错误
按源码顺序检查:
v4l2_dev和sd是否有效,sd->name是否已设置。sd->v4l2_dev是否已经非空,是否重复注册。子设备模块引用是否取得成功。 v4l2_ctrl_add_handler()是否失败。若启用 MC, media_device_register_entity()是否失败。internal_ops->registered是否返回错误。
不要只看最终的 -EINVAL 或 -ENODEV,要定位它来自哪个检查分支。
情况二:子设备已注册,却没有 /dev/v4l-subdevX
检查子设备是否设置 V4L2_SUBDEV_FL_HAS_DEVNODE,以及驱动是否调用 v4l2_device_register_subdev_nodes()。子设备对象注册与节点发布是两步。
情况三:注销后外层对象没有立刻释放
检查 v4l2_device_unregister() 是否已经撤销组织关系,再检查 v4l2_device_put() 是否归还基础引用,以及是否仍有已注册或延迟释放的 video_device 持有引用。还要确认驱动有没有设置 release 回调,以及回调是否负责释放正确的外层对象。
情况四:drvdata 不是 v4l2_device
这是允许的。v4l2_device_register() 只有在 dev_get_drvdata(dev) 为空时才设置它。驱动若已经用 drvdata 保存外层私有对象,不能假定调用后 dev_get_drvdata() 必然返回 v4l2_device。
12. 本篇结论
读完 v4l2-device.c,应当能把以下四件事分开解释:
v4l2_device_register()初始化 V4L2 设备组织对象,并关联父设备;它不自动发现或注册所有子设备。v4l2_device_register_subdev()负责子设备的所属关系、控制处理器合并、可选 entity 登记、内部注册回调和列表挂接。v4l2_device_unregister()撤销设备关联并清理子设备组织关系;它不等同于最终释放外层内存。v4l2_device_put()归还kref;只有引用计数归零时,才可能调用驱动的release回调。
VIMC 把这些动作串在同一个实例中:先建立 v4l2_device 和 media_device,再创建各类实体、建立 links、发布媒体设备,并在需要时注册子设备节点。退出时则按实体、节点、媒体设备、V4L2 组织对象和引用计数的关系清理。
下一篇建议继续写 v4l2_subdev.c:Sensor、Debayer、Scaler 怎样共用一套操作接口。本篇已经建立了 subdev 的组织和生命周期背景,后续可以把重点转向 v4l2_subdev_ops、v4l2_subdev_call() 以及实际驱动回调。















































































