夜雨聆风学习资料网

ARTICLE · 1355014

V4L2 源码深度解析(十九):从 media_device 到 /dev/mediaX——媒体设备怎样对外提供拓扑接口?

V4L2 源码深度解析(十九):从 media_device 到 /dev/mediaX——媒体设备怎样对外提供拓扑接口?
、

上一篇已经把一个子设备变成了 /dev/v4l-subdevX。应用打开这个节点,可以查询 Sensor 的格式,也可以配置某个处理模块的选区。但只知道几个节点的名字,还不够:哪个 Sensor 接着哪个 Debayer,Scaler 选用了哪路输入,最后又从哪个视频节点取得图像?

/dev/mediaX 提供的正是这份设备内部的关系图。它不是另一条图像输出通道,而是让应用发现功能模块、找到访问接口、读取连接关系,并在允许的范围内改变连接状态。

本篇继续沿 VIMC 往下读。先认识这张图里保存什么,再看 media_device 怎样发布为字符设备,随后跟踪一次完整的拓扑查询,最后把注销和引用释放接起来。主线是启用 CONFIG_MEDIA_CONTROLLER 的实现;子设备节点还依赖相应的 Subdev API 配置。缓冲区、逐帧请求和完整的管线启动过程不在这里重新展开。

1. 已经有视频节点,为什么还需要媒体节点?

1.1 节点名字不能说明它们怎样连接

在一套摄像头系统里,Sensor 负责产生图像,后续模块可能进行去马赛克、缩放和颜色处理,采集节点则负责把结果送进缓冲区。一个模块可能有多个出口,一个处理模块也可能在几路输入中选择一路。

仅仅列出 /dev/video0、/dev/video1 和几个子设备节点,无法回答这些问题。视频节点的数字由注册过程分配,不是图像路径上的顺序编号;子设备节点的存在,也不说明它现在接到了哪一个采集出口。

以这份 VIMC 配置的一条支路为例:

Sensor A → Debayer A → Scaler → RGB/YUV Capture

这表示功能模块之间有相应的数据连接。它不是四个文件之间的数据复制过程,也不是承诺各段的当前格式已经匹配。实际开始处理之前,还要完成格式配置、缓冲区提交和流启动。

同一份配置中,Sensor A 还连接 Raw Capture 0,并且关联 Lens A。前者是图像数据路径,后者是镜头与 Sensor 的附属关系。它们不应该都画成“像素从这里流过去”。

图 1:选取 VIMC 的一部分连接。实线表示数据连接,虚线表示附属关系;下方的节点说明访问职责,不构成另一条像素管线。

1.2 应用需要的是一张可查询的图

Media Controller 把设备内部的功能块和连接组织成图。应用读图时,主要关心四类信息。

Entity 表示功能实体。 Sensor、Debayer、Scaler 和采集出口可以各有一个实体。实体不一定是独立芯片,也不一定有自己的设备文件。

Pad 表示实体的数据端口。 source pad 表示数据出口,sink pad 表示数据入口。这里的 pad 既不是芯片封装引脚,也不是 VB2 图像内存的 plane。

Interface 表示访问接口。 对本文的 V4L2 节点,接口描述中包含节点类型以及主、次设备号。它帮助应用从图中的功能实体,找到实际可打开的接口。

Link 表示对象之间的关联。 本实现支持 pad 到 pad 的数据连接、interface 到 entity 的接口关联,以及 entity 到 entity 的附属关联。应用必须先看 link 类型,才能解释它两端的 ID。

这些概念服务于“发现和配置”,不代替模块本身的算法、寄存器配置或 DMA 操作。后面的查询接口返回的是对象描述和连接,不是图像内容。

1.3 三类节点分别回答什么问题

到这里再比较节点,含义就清楚了:

入口
在本文场景中回答的问题
不应混入的操作
/dev/mediaX
设备里有什么模块,怎样连接,哪些入口可以访问它们?
不用它执行图像 QBUF/DQBUF。
/dev/v4l-subdevX
某个模块的 pad 格式、选区和控制项是什么?
不因节点存在就获得一条 VB2 图像队列。
/dev/videoX
怎样配置该视频接口、交接缓冲区并取得结果?
节点编号本身不描述整张上游拓扑。

接口职责可以组合,但不能互相替代。本文的默认 media_device_fops 没有提供读写像素的 read/write 回调;媒体节点的主要工作在 ioctl 中。

实现对照:vimc-core.c 的 ent_config[]、data_links[]、ancillary_links[];mc-entity.c 的三类建链入口;mc-device.c 的 media_device_fops。

2. media_device 保存图,media_devnode 提供入口

2.1 整套媒体设备由谁分配

VIMC 将 struct media_device mdev 内嵌在自己的设备对象中,同时保存 struct v4l2_device v4l2_dev。两者通过下面这行关联:

vimc->v4l2_dev.mdev = &vimc->mdev;

v4l2_device 组织 V4L2 子设备及相关公共状态,media_device 则组织媒体图。它们可以属于同一个外层驱动对象,但不是同一个结构,也不共享一个可以随意互换的注册函数。

对 media_device,本篇关心的成员可以按实际用途读:

成员
在后续流程中的作用
dev
关联父 struct device,用于设备关系、日志及信息填充。
devnode
指向本次发布的媒体字符设备节点对象。
model
、driver_name、serial、bus_info
为设备信息查询提供身份描述。
entities
、interfaces、pads、links
保存已经登记的图对象集合。
id
、topology_version
分配图对象标识,记录结构增删形成的版本变化。
entity_internal_idx
给图遍历使用的内部索引分配器,不是对外设备号。
graph_mutex
在遵守该约定的路径中,串行化图的查询与修改。
ops
提供可选的链路通知、Request 验证与提交等回调。
req_queue_mutex
Request 排队与相关流操作的串行化基础,不是普通拓扑查询锁。

成员名称与用法来自正文所读函数;结构类型的补充对照见文末接口 A。初始化函数并不替驱动创建外层对象。

2.2 media_devnode 是另外分配的节点对象

__media_device_register() 会单独分配 struct media_devnode。它包含嵌入的 struct device、struct cdev、节点编号、注册标志和媒体文件操作表。

成功路径中建立两个方向的关系:

mdev->devnode       → media_devnode
media_devnode.media_dev → 原来的 media_device

前一个方向便于注销节点,后一个方向让文件操作从已打开的节点找到媒体图。指针关系不等于自动获得对方的引用,生命周期必须另看注册和释放路径。

2.3 media_intf_devnode 不是同一个结构

还会遇到一个名字非常接近的结构:struct media_intf_devnode。它是图中的设备接口描述,记录接口类型及 major/minor,不是用来实现 /dev/mediaX 的 media_devnode。

media_devnode
    → 带 device 和 cdev,发布媒体控制入口

media_intf_devnode
    → 带 media_interface,描述图内某个可访问节点

因此,media_devnode_register() 和 media_devnode_create() 也不能按名字当成相邻的两个注册步骤。前者登记实际媒体字符设备;后者在图中创建接口对象,V4L2 节点注册代码会用它描述视频或子设备接口。

图 2:实线是成员指针或对象内嵌关系。Interface 中保存的设备号用于识别另一个设备接口,不是 mdev 本身的字符设备对象。

实现对照:__media_device_register()、media_devnode_register()、media_devnode_create();配套类型见接口 A、B。

3. 沿 VIMC 看整张图什么时候建立

3.1 先初始化,再创建实体和连接

vimc_probe() 先清零分配外层对象,设置模型名称、总线描述和父设备,再调用 media_device_init()。

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);

上面是同一初始化段的节选。此时还没有因为 media_device_init() 自动出现 /dev/mediaX。

接下来,vimc_register_devices() 的成功路径依次完成:

v4l2_device_register()
    → 分配 ent_devs 数组
    → vimc_add_subdevs()
    → vimc_create_links()
    → media_device_register()
    → v4l2_device_register_subdev_nodes()

vimc_add_subdevs() 按配置调用各模块的创建函数,里面不只有 Sensor,也包含 Capture。不能只看辅助函数名字,就认为这一阶段完全没有视频节点。

3.2 实体可以先于 /dev/mediaX 存在

子设备登记通过 v4l2_device_register_subdev() 将 sd->entity 注册到媒体设备。普通采集节点的注册路径也会登记相应的实体和接口。媒体图因此可以在媒体控制节点发布之前逐步形成。

media_device_register() 做的关键工作是发布访问入口,而不是扫描所有 Sensor 并自动重建一张完整图。实体和 link 来自前面具体的创建、注册函数。

3.3 节点出现后,图还可能继续增加接口

本例把子设备节点的批量创建放在媒体节点发布之后。后续为子设备建立 /dev/v4l-subdevX 时,会创建接口对象,再将接口关联到已有 sd->entity。

所以,在这段注册尚未全部结束时,不能把第一次看到 /dev/mediaX 理解成“所有子设备接口已经同时可见”。它也说明拓扑查询需要处理结构变化,而不能只在程序启动时记死一次数量。

图 3:按当前 VIMC 成功路径排列。实体登记与接口发布分阶段发生;这不是所有桥接驱动唯一合法的注册模板。

本例还有 Sensor 到 Lens 的 ancillary link。后面返回的 links 数量不只是图像管线中的箭头数量,还包括接口关联等其他正向连接。

实现对照:vimc_probe()、vimc_register_devices()、video_register_media_controller()、__v4l2_device_register_subdev_nodes()。

4. media_device_init:先建立图的管理条件

下面是本实现的完整函数:

voidmedia_device_init(struct media_device *mdev)
{
 INIT_LIST_HEAD(&mdev->entities);
 INIT_LIST_HEAD(&mdev->interfaces);
 INIT_LIST_HEAD(&mdev->pads);
 INIT_LIST_HEAD(&mdev->links);
 INIT_LIST_HEAD(&mdev->entity_notify);

 mutex_init(&mdev->req_queue_mutex);
 mutex_init(&mdev->graph_mutex);
 ida_init(&mdev->entity_internal_idx);

 atomic_set(&mdev->request_id, 0);

if (!*mdev->bus_info)
  media_set_bus_info(mdev->bus_info, sizeof(mdev->bus_info),
       mdev->dev);

 dev_dbg(mdev->dev, "Media device initialized\n");
}

4.1 链表头不是一张已经填好的拓扑

初始化五条链表,表示它们已经可以安全接入成员,不表示实体、pad 或连接已经创建。实体登记时,media_gobj_create() 才将对象挂到所属类型的全局链表中。

entity_notify 是实体登记通知集合,不是供应用 DQEVENT 使用的 V4L2 事件队列。request_id 同样属于请求对象的编号管理,不是 Sensor 编号或缓冲区索引。

4.2 两把互斥锁保护的范围不同

普通图查询和链路修改主要涉及 graph_mutex。Request 的排队及与启停协作涉及 req_queue_mutex。不能看到媒体对象里有两把锁,就认为任何 ioctl 都同时取得它们。

是否持锁,要在命令表和具体入口中判断。后面会看到:MEDIA_IOC_G_TOPOLOGY 有图锁标志,MEDIA_IOC_REQUEST_ALLOC 没有。

4.3 初始化不是完整清零,也不是释放存储的承诺

函数没有 memset(mdev, 0, sizeof(*mdev))。本例通过外层 kzalloc() 保证未显式赋值成员具有正确初值。将一段未初始化内存交给这个函数,不等于所有字段都自动有效。

media_device_cleanup() 则销毁内部 ID 分配器、图遍历辅助状态和互斥锁。它不负责 kfree(mdev),也不代替实际节点注销。三件事应配对管理:外层存储分配、框架初始化、对外节点发布。

5. 从 __media_device_register 到字符设备发布

5.1 上层函数先为节点填好依赖

通常通过 media_device_register() 宏进入 __media_device_register(mdev, owner);宏的模块参数规则以配套头文件为准。下面直接读实现主体:

int __must_check __media_device_register(struct media_device *mdev,
      struct module *owner)
{
structmedia_devnode *devnode;
int ret;

 devnode = kzalloc(sizeof(*devnode), GFP_KERNEL);
if (!devnode)
return -ENOMEM;

/* Register the device node. */
 mdev->devnode = devnode;
 devnode->fops = &media_device_fops;
 devnode->parent = mdev->dev;
 devnode->release = media_device_release;

/* Set version 0 to indicate user-space that the graph is static */
 mdev->topology_version = 0;

 ret = media_devnode_register(mdev, devnode, owner);
if (ret < 0) {
/* devnode free is handled in media_devnode_*() */
  mdev->devnode = NULL;
return ret;
 }

 ret = device_create_file(&devnode->dev, &dev_attr_model);
if (ret < 0) {
/* devnode free is handled in media_devnode_*() */
  mdev->devnode = NULL;
  media_devnode_unregister_prepare(devnode);
  media_devnode_unregister(devnode);
return ret;
 }

 dev_dbg(mdev->dev, "Media device registered\n");

return0;
}

它把 media_device_fops、父设备和节点释放钩子都准备好,再交给 media_devnode_register()。底层失败时,节点的内存回收已由底层相应分支负责,上层把 mdev->devnode 清空,不能再无条件 kfree(devnode)。

成功注册后再创建只读 sysfs 属性 model。这里的“只读”指该属性,没有把整个媒体控制接口设成只读节点。

5.2 动态主设备号与独立的 media 总线

媒体节点基础设施在 media_devnode_init() 中执行:

ret = alloc_chrdev_region(&media_dev_t, 0, MEDIA_NUM_DEVICES,
                         MEDIA_NAME);

随后注册名为 media 的 media_bus_type。在本实现里,预留范围是 256 个次设备号,主设备号由内核分配;不能把视频节点使用的 VIDEO_MAJOR 直接套到 /dev/mediaX。

这个软件总线把媒体节点纳入设备模型,并不表示像素经过一条名为 media 的物理总线。源码走的是 devnode->dev.bus = &media_bus_type,不是仿照视频节点注册一个 video4linux class。

5.3 为当前节点分配 minor

media_devnode_register() 在全局 media_devnode_lock 下查找空闲位并占用,随后记录:

devnode->minor = minor;
devnode->media_dev = mdev;
devnode->dev.bus = &media_bus_type;
devnode->dev.devt = MKDEV(MAJOR(media_dev_t), devnode->minor);
devnode->dev.release = media_devnode_release;

设备名由 dev_set_name(..., "media%d", minor) 形成。这里的 X 是本类节点的动态编号,不是媒体 entity ID,不是物理相机端口号,也不应跨重启作为唯一身份依据。

5.4 device 与 cdev 为什么要一起发布

device_initialize() 准备设备模型对象;cdev_init() 连接文件操作。当前实现接着执行:

set_bit(MEDIA_FLAG_REGISTERED, &devnode->flags);
ret = cdev_device_add(&devnode->cdev, &devnode->dev);

注册标志在发布动作之前设置,不能画成“字符设备完全可访问之后才把注册位置一”。标志使文件入口知道节点是否接受访问;cdev/device 的注册才让入口真正进入设备模型与字符设备查找关系。

补充对照 Linux v6.1 的通用字符设备实现,cdev_device_add() 先处理 cdev 与设备对象的关联,再发布 device;后续失败会撤销已经发布的字符入口。辅助接口并不承诺调用期间没有任何短暂可见窗口,因此所有文件回调依赖都应提前准备好。这里不把它展开成新的设备模型专题。

图 4:基础设施初始化和每台媒体设备的注册是两层。设备文件通常由 devtmpfs/设备管理服务配合呈现,函数并不是自己打开 /dev 目录写一个普通文件。

5.5 失败时撤销的是哪一层

没有空闲 minor 时,底层释放尚未发布的 devnode,返回 -ENFILE。发布失败时,清注册位、撤销编号和关联,再通过设备引用路径收尾。sysfs model 属性创建失败时,上层执行 prepare-unregister 与实际 unregister,不能只删属性而留下可访问的 cdev。

正常成功注册的 minor 要到最终 media_devnode_release() 才归还;注册失败分支还存在自己的提前撤销处理。不要把两类路径合并成“每次注销都会立刻复用编号”。

实现对照:media_devnode_init()、media_devnode_register();通用字符设备与设备发布机制为文末补充接口 C。

6. open:从 inode 找到 media_devnode

6.1 不是通过 video_devices[] 查找

媒体节点有自己的 cdev。打开后,media_open() 从 inode->i_cdev 还原其外层对象:

devnode = container_of(inode->i_cdev, struct media_devnode, cdev);

随后在 media_devnode_lock 保护下检查注册位并取得设备引用。锁保护的是“检查仍注册”和“增加节点引用”之间的交接,防止这两步之间节点被注销释放。

下面是这个阶段的连续源码节选:

if (!media_devnode_is_registered(devnode)) {
    mutex_unlock(&media_devnode_lock);
return -ENXIO;
}
get_device(&devnode->dev);
mutex_unlock(&media_devnode_lock);

filp->private_data = devnode;

file->private_data 在这里保存 media_devnode,不是 v4l2_fh、video_device 或 media_device。ioctl 再通过 devnode->media_dev 走到媒体图。

6.2 默认 open 没有创建独立的图副本

上层 media_device_open() 只返回 0;默认 close 也只返回 0。每次打开并不复制一张独立拓扑,两个打开文件会面对同一个 mdev 维护的图。

打开回调失败时,外层归还刚取得的节点引用并清空 private_data。正常最终文件释放时,media_release() 调用可选 close,清空文件关联,再 put_device()。这与前面的打开取得引用相配对。

节点设备引用保护的是 media_devnode 的寿命。它本身没有调用 v4l2_device_get(),也没有自动为外层 mdev 建立一套通用 kref 协议。 外层对象何时能被清理,仍需看具体驱动与退出路径的同步。

7. ioctl:两层文件操作把命令送到媒体图

7.1 先把两张表接起来

实际字符设备使用 struct file_operations media_devnode_fops。其 unlocked_ioctl 指向 media_ioctl()。媒体语义层则使用 struct media_file_operations media_device_fops,其中 .ioctl = media_device_ioctl。

应用 ioctl(fd, MEDIA_IOC_..., arg)
    → media_ioctl()
    → __media_ioctl():检查入口与注册状态
    → devnode->fops->ioctl
    → media_device_ioctl()
    → 命令表中的处理函数

__media_ioctl() 接到的是待调用函数指针。它不负责拓扑数组复制,也不选 graph mutex;这些工作在更内层完成。这条路径也不经过 video_ioctl2() 或 video_usercopy(),不能把普通视频节点的分发规则原样复制过来。

图 5:左侧是打开时建立的对象关系,右侧是后续命令路径。锁只画在实际取得的位置,不把整个文件操作都放进同一把锁。

7.2 命令编号只是索引,完整命令值还要再检查

media_device_ioctl() 先执行:

if (_IOC_NR(cmd) >= ARRAY_SIZE(ioctl_info)
    || ioctl_info[_IOC_NR(cmd)].cmd != cmd)
return -ENOIOCTLCMD;

因此,不是只要编号落在表内就可以调用。编码中的命令组、方向和参数大小也体现在完整 cmd 中。内部的 -ENOIOCTLCMD 不应不加区分地写成应用最终必然直接得到的 errno;外部结果还涉及通用 ioctl 层。

7.3 参数复制、图锁与结果回写的真实顺序

staticlongmedia_device_ioctl(struct file *filp, unsignedint cmd,
unsignedlong __arg)

{
structmedia_devnode *devnode = media_devnode_data(filp);
structmedia_device *dev = devnode->media_dev;
conststructmedia_ioctl_info *info;
void __user *arg = (void __user *)__arg;
char __karg[256], *karg = __karg;
long ret;

if (_IOC_NR(cmd) >= ARRAY_SIZE(ioctl_info)
     || ioctl_info[_IOC_NR(cmd)].cmd != cmd)
return -ENOIOCTLCMD;

 info = &ioctl_info[_IOC_NR(cmd)];

if (_IOC_SIZE(info->cmd) > sizeof(__karg)) {
  karg = kmalloc(_IOC_SIZE(info->cmd), GFP_KERNEL);
if (!karg)
return -ENOMEM;
 }

if (info->arg_from_user) {
  ret = info->arg_from_user(karg, arg, cmd);
if (ret)
goto out_free;
 }

if (info->flags & MEDIA_IOC_FL_GRAPH_MUTEX)
  mutex_lock(&dev->graph_mutex);

 ret = info->fn(dev, karg);

if (info->flags & MEDIA_IOC_FL_GRAPH_MUTEX)
  mutex_unlock(&dev->graph_mutex);

if (!ret && info->arg_to_user)
  ret = info->arg_to_user(arg, karg, cmd);

out_free:
if (karg != __karg)
  kfree(karg);

return ret;
}

首先为主参数准备临时空间。小结构使用栈上 256 字节区域,大结构才 kmalloc()。输入复制按 _IOC_WRITE 判断,输出复制按 _IOC_READ 判断。方向是以应用读写数据的视角编码的。

然后根据命令标志决定是否取得 graph_mutex。当前表中,设备信息、旧枚举、链路设置和 G_TOPOLOGY 都带图锁标志,REQUEST_ALLOC 不带。

最重要的顺序是:复制主参数 → 加图锁 → 处理命令 → 解图锁 → 成功时回写主参数 → 释放临时空间。 锁并不覆盖最后的外层主参数回写。另一方面,G_TOPOLOGY 内部还会复制数组,那些复制发生在处理函数内,因而在这次图锁范围内。

7.4 失败后的输出不能按成功结果使用

代码只在 !ret 时执行 arg_to_user。这意味着处理函数内部修改了主参数副本,最终返回错误时,这些修改未必回到应用。

但处理函数已经直接写入的数组内容不会因为错误自动回滚;一次查询可能写完若干元素以后才失败。系统调用失败不代表全部输出内存都保持原样,成功回写也不是数据结构的永久锁定。 这两点决定了后面程序的重试方式。

8. DEVICE_INFO:先确认打开的是哪一台媒体设备

应用可以从最简单的一条命令开始:

structmedia_device_infoinfo = {0};
if (ioctl(fd, MEDIA_IOC_DEVICE_INFO, &info) < 0) {
/* 处理节点失效、参数地址错误或其他实际返回。 */
}

处理函数先清零输出,优先使用 mdev->driver_name;为空时,从父设备关联的驱动取得名称。再复制 model、serial、bus_info 和硬件修订信息。

本实现还明确写入:

info->media_version = LINUX_VERSION_CODE;
info->driver_version = info->media_version;
info->hw_revision = dev->hw_revision;

所以,不应把这里的 driver_version 当成某颗 Sensor 的固件版本,或当成应用构建号。hw_revision 的含义由设备驱动决定。

DEVICE_INFO 不返回实体数量,也不自动告诉应用哪个 /dev/videoX 才是目标采集口。它先回答“当前媒体设备是什么”;具体组成要继续查询拓扑。

接口文档的简述不能替代完整系统调用的错误分析:本处理函数通常返回 0,外层仍可能因为节点状态、输入输出地址等原因失败。

实现对照:media_device_get_info()、__media_ioctl()、copy_arg_from_user()、copy_arg_to_user();字段契约补充见接口 D。

9. G_TOPOLOGY:把链表中的图导出成四组数组

9.1 为什么不是返回一个内核指针

内核中的实体和 link 通过指针相连,这些指针不能直接交给应用作为对象地址。查询接口为每类对象填写固定布局的描述,以 ID 重建关联。

struct media_v2_topology 不是整张图的实体存储,而是一个查询头。它提供拓扑版本、四个数量,以及四个数组地址槽:

查询头成员
对应数组或含义
topology_version
这次观察的结构版本。
num_entities
 / ptr_entities
entity 数量与描述数组。
num_interfaces
 / ptr_interfaces
interface 数量与描述数组。
num_pads
 / ptr_pads
pad 数量与描述数组。
num_links
 / ptr_links
非 backlink 的连接数量与描述数组。

地址槽使用固定宽度的 __u64 表示应用地址。它不是内核分配的图对象地址,也不是 DMA 地址。C 程序通常将已经分配的指针先转为 uintptr_t,再存入 64 位字段。不要对整个内核对象使用 memcpy() 作为接口返回。

9.2 四个循环怎样读取同一张图

media_device_get_topology() 先把当前 mdev->topology_version 写入临时查询头,随后依次遍历 entities、interfaces、pads 和 links。

entity 循环的主体如下,其余数组采用类似的数量与复制规则:

i = 0;
uentity = media_get_uptr(topo->ptr_entities);
media_device_for_each_entity(entity, mdev) {
    i++;
if (ret || !uentity)
continue;

if (i > topo->num_entities) {
        ret = -ENOSPC;
continue;
    }

memset(&kentity, 0, sizeof(kentity));
    kentity.id = entity->graph_obj.id;
    kentity.function = entity->function;
    kentity.flags = entity->flags;
    strscpy(kentity.name, entity->name, sizeof(kentity.name));

if (copy_to_user(uentity, &kentity, sizeof(kentity)))
        ret = -EFAULT;
    uentity++;
}
topo->num_entities = i;
topo->reserved1 = 0;

数组地址为零时,只统计数量,不复制对象。地址非零时,输入的数量表示该数组可容纳的元素数;对象超过这个容量,记录 -ENOSPC。

发生第一个错误以后,后续循环仍会统计对象,但不继续复制。这里得到的最终计数先保存在内核临时查询头里;是否能回到应用,还要经过上一节的成功回写判断。

9.3 描述中怎样保持对象关系

entity 数组保存 ID、名称、功能和标志。interface 数组保存 ID、接口类型及设备号。pad 数组保存自己的 ID、所属 entity ID、端口 index 和方向标志。

link 则填入:

klink.id = link->graph_obj.id;
klink.source_id = link->gobj0->id;
klink.sink_id = link->gobj1->id;
klink.flags = link->flags;

注意这里使用通用 gobj0/gobj1,而不是把每条连接都按两个 pad 输出。三类 link 的端点含义是:

连接类型
source_id 指向
sink_id 指向
DATA_LINK
source pad
sink pad
INTERFACE_LINK
interface
entity
ANCILLARY_LINK
primary entity
ancillary entity

应用通过 flags & MEDIA_LNK_FL_LINK_TYPE 选择解释方式。碰到未知类型,应保留或报告,不能强行拿它的端点 ID 去查 pad 数组。

图 6:图中使用 E、P、I 作为示意标签,不是硬编码的实际 ID。连接数组中的 ID 需要回到对应类型的对象数组解析。

9.4 反向记录为什么不重复导出

数据连接在内核中可以有供两端遍历使用的反向记录。G_TOPOLOGY 遇到 link->is_backlink 就跳过,避免把同一数据连接对外显示成两条独立线路。

这个跳过发生在计数递增之前。因此 num_links 不是 mdev->links 所有内部成员数的机械复制,也不等于只有 DATA_LINK 的数量。

9.5 ID、pad index 和设备号不要互换

图对象 ID 在所属媒体图中识别对象,不应假定连续,更不应拿它当数组下标。一个 pad 的 index 只在所属 entity 内有意义;接口的 major/minor 才用于查找字符设备。三个编号分别服务于图对象、模块端口和设备文件。

内部 entity->internal_idx 供图遍历辅助结构使用,也不是应用应该保存的 entity ID。对象删除、重新注册后,旧 ID 不再能用来证明仍是同一个实例。

实现对照:media_device_get_topology()、media_gobj_create()、media_create_intf_link()、media_create_ancillary_link();接口布局补充见 E。

10. 为什么查询通常分两次,什么时候需要重试

10.1 第一次只问数量

先把查询头清零,四个数组地址均为零:

structmedia_v2_topologyfirst = {0};
if (ioctl(fd, MEDIA_IOC_G_TOPOLOGY, &first) < 0)
/* 没有拿到可用数量,停止本次读取。 */;

成功后,按四个数量分别分配数组。应用必须检查乘法溢出、内存分配失败,并对异常规模设置自己能够承受的上限。数量为零的数组可保留空指针。

10.2 第二次传回容量和数组地址

第二次查询头保留第一次数量,将数组指针写入对应字段。成功后使用第二次返回的实际数量,而不是继续按旧数量打印未填充尾部。

两次 ioctl 之间没有跨调用的 graph mutex。第一次查询后,驱动可能增删对象,所以第二次可能需要更多容量,也可能成功返回不同版本。

图 7:空间不足或结构版本变化时,丢弃这次数组,重新从数量查询开始。参数错误、设备失效等不能全部用无限重试掩盖。

10.3 当前实现没有比较应用传回的版本

函数进入后就执行 topo->topology_version = mdev->topology_version。它没有先比较输入版本,再拒绝不一致的快照。版本一致性应由应用根据前后两次成功结果判断。

这与“版本变化时内核完全不写任何数组”的理解不同。数组可能已经被填写;应用选择重试,是为了重新获取一组按相同结构版本规划和取得的数据。

10.4 ENOSPC 后不能依赖头里的数量已经更新

结合两层源码,过程是:

G_TOPOLOGY 内部发现容量不足
    → ret = -ENOSPC
    → 内部临时查询头继续记录实际数量
    → 返回 media_device_ioctl()
    → !ret 不成立,不调用 arg_to_user
    → 应用手中的查询头不保证包含这些新计数

因此示例程序遇到 ENOSPC 后,会释放本次数组,从一个新的零查询头重新取数量。不能简单使用“失败返回里肯定已经告诉我正确大小”作为下一次分配依据。

EFAULT 也可能出现在某个数组元素已经复制之后。本文的程序不消费失败查询留下的部分数据。

10.5 topology_version 不是所有配置的变化次数

media_gobj_create() 和 media_gobj_destroy() 会递增版本。__media_device_register() 又将它设为零,作为发布时的基线。VIMC 后面继续创建子设备接口,所以版本仍会继续改变;源码注释里的 static 不能解释成永久冻结图。

当前 link setup 只更新 link 和 reverse 的 flags,没有在该路径增加 topology_version。因此版本相同不代表连接启用状态、pad 格式、控制值等全都没有变化。

一次成功 G_TOPOLOGY 在图锁下得到的是一次结构观察。它不是将整条采集管线锁给这个进程,也不是对后续 SETUP_LINK、S_FMT、STREAMON 的事务保证。

11. 从一个 entity 找到可打开的节点

应用已经取得四组数组,下一步要把它们连起来。查找 Sensor 节点时,可以先按实体名称及功能找到目标 entity,再查指向该 entity 的 INTERFACE_LINK,找到相应 interface,读取类型和 major/minor。

目标 entity 的 ID
    ← INTERFACE_LINK 的 sink_id
    ← 同一 link 的 source_id
    ← interface.id
    → interface.devnode.major / minor
    → 实际设备节点

不能把 entity 的 ID 直接拼成 /dev/v4l-subdev<ID>。查询头也没有给出任意系统都固定的绝对路径字符串。

Linux 上可用 /sys/dev/char/<major>:<minor> 及其 uevent 信息协助解析实际节点;也可以由设备枚举库完成。它属于设备模型提供的路径发现方式,而不是 Media Controller 返回了一个文件路径。读取之后仍要检查节点是否存在、权限是否满足,以及设备是否已变化。

并非每个实体都有 interface。没有独立设备文件的内部模块仍然可以存在于媒体图中;反过来,接口与实体也不应被写死成所有设备都一对一的关系。

本文的只读程序会打印接口设备号和它关联的实体。它不自动打开找到的子设备,更不会更改格式或开始采集。

12. 旧式枚举与 32 位兼容:只解释必要的边界

12.1 ENUM_ENTITIES 按 ID 选择,不是按数组下标

media_device_enum_entities() 使用 find_entity()。不带 MEDIA_ENT_ID_FLAG_NEXT 时寻找指定 ID;带 NEXT 时,从登记列表中找 ID 大于指定值的下一项。

实际遍历应使用返回的 ID 继续请求,而不是假定实体依次编号为 0、1、2。当前实现的 NEXT 逻辑是沿列表找第一个满足条件的对象,不是每次把整个列表重新排序。

旧式 entity 描述的 type 还有历史兼容映射。新格式中的 function 更直接表达实体功能,不能把旧字段在所有场景下都当成未经修改的现代 function 值。

12.2 ENUM_LINKS 的输出空间由调用方准备

旧式流程先枚举某个实体的 pads 和 outbound links 数量,再为 media_links_enum.pads/links 准备数组。参数没有像 v2 那样为每个数组提供输入容量字段,因此不能期待它替错误的数组长度做同等检查。

这个接口按 pad 连接组织信息,不提供独立的 interface 数组。本实现还沿实体 links 以 source/sink 的 pad 布局读取,没有在该循环中显式按三类 link 分派。对包含 ancillary link 的扩展图,不能把它未经核对地当成 G_TOPOLOGY 的完整替代。 本篇观察程序只采用 v2 拓扑接口,不以旧枚举路径验证附属连接。

这是源码适用范围上的审计提醒,不是声称本文已在内核中触发并定位所有混合连接场景。

12.3 为什么 compat 对旧枚举单独转换

media_links_enum 含有原生指针。32 位应用与 64 位内核之间,该结构的指针宽度和命令大小可能不同。当前实现为 MEDIA_IOC_ENUM_LINKS32 单独读取指针字段,用 compat_ptr() 转换,再调用公共枚举函数。

其他命令通过默认分支接回 media_device_ioctl()。v2 的数组地址字段采用固定 64 位表示,为统一布局提供了条件;仍然不能由此宣称所有私有 ioctl 天然兼容。本文没有进行目标架构的实际 compat 运行测试。

13. SETUP_LINK 与 REQUEST_ALLOC:拓扑入口还能做什么

13.1 SETUP_LINK 只修改已有连接

media_device_setup_link() 从应用描述取得两端 entity ID 和 pad index,找到实体、检查端口范围,再找两端已经存在的 link。任何一步找不到,都不能凭空创建新连接。

随后在当前图锁上下文中调用 __media_entity_setup_link()。此实现允许修改的掩码只有 MEDIA_LNK_FL_ENABLED。不可修改位不能被顺便清掉;IMMUTABLE 连接只能保持原状态;处于流中的非 DYNAMIC 连接不能随意切换。

图 8:仅画 pad 数据连接的设置流程。修改连接不等于协商格式,也没有在这里替应用 QBUF 或调用整条管线的 STREAMON。

通过初步检查后,还可能执行媒体设备的 PRE 通知和两端实体的 link_setup。sink 回调失败时,当前实现尝试用旧 flags 通知 source 恢复;这种回调级回退不应被扩写成所有硬件动作都有不可失败的事务回滚。

成功时,正向和反向记录的 flags 一起更新。本篇只交代它从媒体 ioctl 到实体操作的边界,完整建链及切换放在后续 Entity/Pad/Link 专题中展开。

13.2 不要把打开权限解释成驱动自动增加的只读策略

本篇工具以 O_RDONLY 打开并只发送查询命令。但本实现 media_device_ioctl() 没有按 file->f_mode 统一禁止 SETUP_LINK;“我的程序只查询”与“这个 fd 在内核被强制禁止配置”是不同结论。

权限设计需要结合设备文件权限、安全策略和驱动实际检查,不能凭 read-only 这个名字推断所有 ioctl 都被同样限制。

13.3 REQUEST_ALLOC 返回另一个 fd

当 Request API 配置和 req_validate/req_queue 等条件满足时,媒体节点可以创建请求 fd。请求 fd 不是 /dev/mediaX 的别名,也不是像素缓冲区 fd。

默认 VIMC 例子不应被当成完整 Request 流程的验证设备。请求绑定、提交和完成已经在对应专题展开,这里只保留媒体节点创建入口及能力检查。

13.4 默认 poll 不是拓扑变化通知

media_device_fops 没有提供专用 poll 回调。外层 media_poll() 在节点有效但没有该回调时返回 DEFAULT_POLLMASK;节点已经失效则返回错误与挂断标志。不能将一次可读/可写的 poll 结果解释成“媒体图刚刚变化了”,也不能在媒体 fd 上照搬 V4L2 的 DQEVENT。

14. 注销与最终释放:先撤销入口,再按引用收尾

14.1 三个函数不能合并成一个 free

media_device_unregister() 撤销已发布的媒体设备及图关联。media_device_cleanup() 清理内部管理设施。外层 mdev 存储由驱动按其真实所有权释放。

实际 media_devnode 则由设备引用机制收尾,最终进入 media_devnode_release()。即使 mdev 内嵌在驱动对象里,节点也仍是单独的分配块。

14.2 media_device_unregister 的主要顺序

在图锁下检查节点是否仍登记,先用 media_devnode_unregister_prepare() 清注册位,阻止后来的 open/操作继续按正常入口进入。然后撤销实体及相关连接、pad、实体通知,解除接口对象的图关联。

这里对 interface 的注释很重要:框架解除图登记和接口链接,但不统一释放创建者持有的接口对象。由创建该对象的模块继续完成释放,避免把“从列表摘除”写成“所有内存已经 kfree”。

解开图锁以后,删除 model 属性,注销 devnode,最后清空 mdev->devnode。

14.3 字符设备删除后,已有文件仍需要 release

media_devnode_unregister() 删除 cdev/device,清空 devnode->media_dev,再归还注册阶段的设备引用。已有文件最终关闭时仍会执行 media_release(),并归还打开时增加的节点引用。

当设备引用归零时:

staticvoidmedia_devnode_release(struct device *cd)
{
structmedia_devnode *devnode = to_media_devnode(cd);

 mutex_lock(&media_devnode_lock);
/* Mark device node number as free */
 clear_bit(devnode->minor, media_devnode_nums);
 mutex_unlock(&media_devnode_lock);

/* Release media_devnode and perform other cleanups as needed. */
if (devnode->release)
  devnode->release(devnode);

 kfree(devnode);
 pr_debug("%s: Media Devnode Deallocated\n", __func__);
}

正常路径中,此时才释放 minor,占用的节点存储也在这里结束。devnode->release 是媒体层的清理钩子;当前 media_device_release() 只记录日志,没有替驱动释放 mdev。

图 9:上半部分撤销图和对外入口;下半部分归还打开引用。外层 mdev 的 cleanup/free 还要满足驱动的同步条件,不由一条裸指针自动保护。

14.4 注册位不是对在途 ioctl 的完整同步证明

__media_ioctl() 的注册位检查发生在进入 media_device_ioctl() 之前,普通文件引用增加的是 devnode 的 struct device。不能据此直接证明所有已经越过检查的 ioctl 都已经退出,也不能认为 graph mutex 会自动延长外层驱动私有对象的寿命。

具体驱动必须结合设备解绑顺序、在途操作和回调依赖保证 mdev 仍有效。本篇核对的是源码中确实存在的保护与撤销位置,没有把这段静态分析当成并发热拔插验证。

VIMC 的正常退出在撤销各模块和媒体设备以后,再退出 V4L2 组织关系并归还引用;vimc_v4l2_dev_release() 中才执行媒体设备 cleanup 和外层存储释放。这个实例的整体并发安全仍需目标内核运行验证,不能只靠一张引用示意图下结论。

15. 只读实验:取得一份可以解析的拓扑

15.1 先确认目标,不假定一定是 media0

设备号可能改变。先根据系统中实际存在的节点查询模型和 bus_info,选择正确的媒体设备,再取得拓扑。

完整示例程序 只发送 DEVICE_INFO 与 G_TOPOLOGY。它以只读方式打开,不设置 link、不设置格式、不申请图像缓冲区、不启动流,也不创建 Request。使用时仍需确认测试权限和目标设备。

cc -std=c11 -Wall -Wextra -Werror -O2 \
   code/media_topology_probe.c -o media_topology_probe

./media_topology_probe /dev/media0

最后一个参数按实际节点替换。若目标内核没有 G_TOPOLOGY,程序报告错误并退出,不未经检查退回旧的混合连接枚举路径。

15.2 程序的核心不是“调用两次就一定成功”

最多尝试 8 轮
    → 清零查询头,查询版本与数量
    → 检查数量与分配大小,分配四组数组
    → 传回容量和地址,获取对象
    → ENOSPC:释放数组,重新查询数量
    → 其他错误:保留错误,退出
    → 成功但版本/数量变化:释放数组,重新开始
    → 成功且检查一致:解析对象与连接

程序打印 entity 名称、pad 归属、interface 设备号和 link 类型。数据连接按 pad ID 解析,接口关联按 interface/entity 解析,附属连接按 entity/entity 解析,未知类型不擅自解引用成另一种结构。

输出中的 ID、设备号、数量和版本来自当次实际查询;文中示意图不充当一次伪造的运行日志。

15.3 重试之外仍要遵守的限制

成功结果只是当时的快照。拿到设备号后,目标节点还可能被移除;读到 enabled 标志后,其他执行方仍可能修改连接。需要长期使用时,应用应在每个实际操作处继续处理错误,不能把启动时的一次查询当成永远有效的授权。

结构稳定也不等于图像配置正确。得到完整拓扑后,还要通过相应子设备和视频节点完成配置,才能进入前面已经学过的采集流程。

16. 从现象反推失败发生在哪一层

现象
应先检查什么
有 video 节点,却找不到 media 节点
Media Controller 配置、mdev 初始化和注册是否执行;不要只看 Sensor probe。
media 节点出现,但子设备接口不全
各实体与接口的注册阶段、HAS_DEVNODE、批量子设备节点注册返回值。
open 失败
节点路径与权限、实际是否仍注册;当前媒体 open 的失效分支返回 ENXIO。
ioctl 没进入处理函数
完整命令编码、外层注册检查、主参数复制,以及入口操作表。
G_TOPOLOGY 返回 ENOSPC
两次查询之间的结构增长或传入容量不足;从数量查询重新开始。
G_TOPOLOGY 返回 EFAULT
主参数或数组地址、容量与真实分配是否一致;不要消费部分输出。
数量没变,结果对象却不一样
比较版本和实际 ID,数量相等不能证明对象没有被替换。
版本相同,enabled 状态改变
当前版本计数主要追踪结构增删,不覆盖本实现的全部 flags/格式修改。
找到 entity,却无法拼出设备路径
entity ID 不是节点序号;检查 INTERFACE_LINK、接口类型和设备号。
SETUP_LINK 返回 EINVAL/EBUSY
两端标识、已有连接、不可变位、流状态及实体回调约束。
注销后对象没有立刻释放
区分图登记、注册引用、打开引用和外层驱动存储;也检查是否遗漏引用配对。

错误码可以来自多个层次。定位时先确认进入哪个函数、是否已经取得图锁、是否已经发生输出复制或状态修改,再查具体字段,不要用一条错误日志推断整套设备的状态。

17. 把这条路径接回整个 V4L2 系列

media_device 保存一张由驱动建立的图;media_devnode 把它接到字符设备。应用打开节点后,文件关联到这个 devnode;媒体 ioctl 再按命令表进入查询或配置函数。

拓扑查询把指针关联转换成 ID 和固定布局描述。应用根据这些关系找到模块和访问接口,而不是从媒体节点直接取得像素。查询完成以后,格式、控制、缓冲区和启动仍由各自接口负责。

本篇读到这里,应该能独立解释三件事:

一个媒体节点为什么能描述多个视频和子设备接口;一次查询为什么会涉及四组数组和两次调用;注销入口为什么不等于立即释放所有相关存储。

接下来深入 Entity、Pad 与 Link 时,就可以从已经取得的图回到它们的创建、连接和遍历过程,而不必再猜 /dev/mediaX 究竟提供了什么。

回首页,搜索你想要的文档

在首页搜索框输入关键词,快速定位资料

文库首页搜索指引 立即搜索

相关学习资料