夜雨聆风学习资料网

ARTICLE · 1299556

V4L2 源码深度解析(十八):一个 v4l2_subdev 怎样变成 /dev/v4l-subdevX?

V4L2 源码深度解析(十八):一个 v4l2_subdev 怎样变成 /dev/v4l-subdevX?

上一篇已经能从 v4l2_subdev 找到 Sensor、Debayer、Scaler 的操作表。桥接驱动拿着 sd,便可以调用格式、选区或流控制回调。但应用没有这个内核指针。它只能打开一个设备文件,再通过接口表达“读取这个模块某个 pad 的格式”。

/dev/v4l-subdevX 就是这样一个可选入口。它没有把 Sensor 变成另一台摄像头,也没有复制一份子设备;框架为已有的 sd 配置一个字符设备节点,把应用的操作送回同一份驱动实现。

这一篇沿 VIMC 的节点发布过程继续:先看对象怎样接到设备文件,再跟踪一次打开和格式查询,最后处理注册失败、关闭与注销。主线是启用 Media Controller 和子设备 API 的这份实现,不展开异步匹配,也不重复 VB2 的缓冲区流程。

1. 子设备已经存在,为什么还需要一个节点?

1.1 内核能调用,不代表应用有入口

一个 Sensor 驱动可以已经完成初始化,登记到 v4l2_device,并由接收端通过 v4l2_subdev_call() 控制。即使系统中没有它的 /dev/v4l-subdevX,这套内核内部调用仍然可以成立。

直接节点解决的是另一个问题:让应用针对这个模块查询能力、访问控制项、协商 pad 格式,或者接收事件。是否暴露这个入口,由子设备的能力声明和上层驱动的节点注册过程共同决定。

以一个可见的 Sensor 节点为例,下面这条命令表达的是“查询 pad 0 的当前格式”,不是取回一帧图像:

structv4l2_subdev_formatfmt = {0};
fmt.pad = 0;
fmt.which = V4L2_SUBDEV_FORMAT_ACTIVE;
int ret = ioctl(fd, VIDIOC_SUBDEV_G_FMT, &fmt);

返回的是宽高、media-bus code、颜色描述等参数,不是像素内存地址。真正的采集缓冲区仍由采集节点及其 VB2 队列处理。

1.2 节点可以看成一个操作入口,但不是另一份硬件

在普通媒体设备中,三个入口经常同时出现:

入口
本篇关注的用途
不应推导的结论
/dev/videoX
采集或输出节点的格式、缓冲区及流操作
名字带 video 不代表整条链路只有一个模块。
/dev/v4l-subdevX
针对某个子设备执行配置、控制和事件操作
不因此获得一套可 QBUF/DQBUF 的图像队列。
/dev/mediaX
发现实体、接口和连接,配置允许变化的链路
不直接返回采集帧。

这里比较的是本篇摄像头场景的职责,不是用文件名概括所有媒体设备的功能。最重要的是:子设备对象可以先存在,节点是在这个对象外面增加的访问方式。

图 1:三列分别表示当前场景中的操作入口与用途;实际的媒体接口关联在图 4 展开。不同节点不是三套图像管线的副本。

实现对照:v4l2_subdev_fops、subdev_do_ioctl();接口含义补充见文末参考 A。

2. VIMC 什么时候决定把子设备暴露出来?

2.1 标志声明与实际创建分开执行

vimc_ent_sd_register() 先调用 v4l2_subdev_init(),随后填写名称、操作表关联和实体信息。它还设置:

sd->flags |= V4L2_SUBDEV_FL_HAS_DEVNODE;

这个标志的意思是“这个子设备需要直接节点”。它不分配 video_device,不选择设备号,也没有在这里创建 /dev 文件。

完成 pad 初始化以后,VIMC 调用 v4l2_device_register_subdev()。成功时,sd 才进入所属 v4l2_device 的 subdevs 链表。后面批量注册节点,遍历的正是这条链表。

2.2 上层随后发起批量节点注册

在 vimc_register_devices() 中,相关顺序是:

登记 v4l2_device
    → 创建各个模块与采集节点
    → 建立媒体数据连接
    → 注册 media_device
    → v4l2_device_register_subdev_nodes()

这段顺序属于本实例。不能把“media_device 已注册”写成所有子设备节点创建的唯一前提;通用注册器依据的是已有 v4l2_device、已登记的子设备,以及相应配置。

按 Linux v6.1 的配套头文件核对,两个公开辅助入口最终选择同一个实现:

辅助入口
子设备 API 启用时传入的参数
v4l2_device_register_subdev_nodes()__v4l2_device_register_subdev_nodes(v4l2_dev, false)
v4l2_device_register_ro_subdev_nodes()__v4l2_device_register_subdev_nodes(v4l2_dev, true)

第二个布尔参数决定新建节点是否设置框架的只读标志。该头文件还定义了一个容易忽略的配置分支:没有启用 CONFIG_VIDEO_V4L2_SUBDEV_API 时,这两个公开包装直接返回 0,而不执行节点创建。因而“函数返回成功”不能脱离编译配置,解释为“一定出现了新节点”。

本篇主体以目录中的函数实现为准;这里及后面的内联接口、类型声明使用固定版本头文件补充核对,不据此推定整套源码的完整版本。见文末参考 B。

3. 注册器首先筛选什么对象?

__v4l2_device_register_subdev_nodes() 并不接受某一个 sd,而是遍历整组子设备:

list_for_each_entry(sd, &v4l2_dev->subdevs, list) {
if (!(sd->flags & V4L2_SUBDEV_FL_HAS_DEVNODE))
continue;

if (sd->devnode)
continue;

只有带 HAS_DEVNODE 且尚未关联 devnode 的对象,才继续申请节点。

这两个判断带来三个实际结论。第一,没有挂入 v4l2_dev->subdevs 的对象,不会被这次遍历发现。第二,只设置标志而没有执行批量注册,不会自动出现节点。第三,已有 sd->devnode 的对象会被跳过;再次用只读包装调用,并不会将既有可写节点重新配置成只读。

sd->devnode != NULL 也不是任意时刻都等于“当前允许打开”:构造期间该指针会提前设置,注销后它又可能保留到最后引用退出。因此要把关联指针、注册状态和设备文件是否存在分别观察。

这个遍历没有在函数内部持有 v4l2_dev->lock,而且后续会进行可睡眠分配及设备注册。调用方应在设备组织关系稳定的阶段执行;不能一边无序修改子设备链表,一边期待此函数自动串行化全部变化,也不能简单加一把自旋锁包住所有分配。

4. 一个 sd 怎样关联到新建的 video_device?

4.1 为节点单独分配一个对象

筛选通过后,框架执行 kzalloc(sizeof(*vdev), GFP_KERNEL),再建立下面这些关联。这里是一段连续源码:

video_set_drvdata(vdev, sd);
strscpy(vdev->name, sd->name, sizeof(vdev->name));
vdev->dev_parent = sd->dev;
vdev->v4l2_dev = v4l2_dev;
vdev->fops = &v4l2_subdev_fops;
vdev->release = v4l2_device_release_subdev_node;
vdev->ctrl_handler = sd->ctrl_handler;
if (read_only)
    set_bit(V4L2_FL_SUBDEV_RO_DEVNODE, &vdev->flags);
sd->devnode = vdev;

sd 没有被转换成另一个结构体。真正发生的是:分配一个新的 video_device,用它承载字符设备身份和文件操作,再通过驱动数据槽指回已有 sd。

图 2:sd->devnode 与 video_get_drvdata(vdev) 建立相互查找;sd->dev_priv 另行指向驱动自己的关联对象。三者不是同一个字段。

4.2 几个赋值决定了后面的路径

赋值
后续作用
video_set_drvdata(vdev, sd)
从节点找回子设备;不能与 v4l2_set_subdevdata(sd, ...) 混淆。
vdev->name
保存子设备描述名称;它不是 /dev 路径字符串。
vdev->dev_parent = sd->dev
为节点提供设备模型父对象;为空时,通用注册代码回退到 v4l2_dev->dev。
vdev->v4l2_dev
指向整个 V4L2 设备的组织对象。
vdev->fops = &v4l2_subdev_fops
决定应用操作进入子设备 open/ioctl/close 路径。
vdev->release
指定节点对象最后释放时的清理函数,不是每次文件关闭的回调。
vdev->ctrl_handler
优先关联子设备的控制处理器;为空时,通用节点注册还有组织对象 handler 的继承逻辑。
sd->devnode = vdev
记录当前节点关联;不等于注册已经完成。

按补充头文件,vdev_to_v4l2_subdev(vdev) 从节点驱动数据取得 sd。这里并没有把 video_device 内嵌到 v4l2_subdev,不应画成对这个节点执行 container_of() 就能还原子设备。

也没有配置 vdev->queue 或普通节点使用的 ioctl_ops。所以,借用 video_device 的注册设施,不等于借用普通采集节点的全部接口。

实现对照:__v4l2_device_register_subdev_nodes()、__video_register_device();辅助宏见参考 C。

5. 名称、字符设备和 /dev 文件怎样接起来?

5.1 类型决定名称前缀,编号由框架分配

节点构造后,调用的是:

err = __video_register_device(vdev, VFL_TYPE_SUBDEV, -1, 1,
                              sd->owner);

VFL_TYPE_SUBDEV 选择子设备节点类别;-1 表示自动选择可用节点编号。在通用注册函数里,该类别对应名称前缀 v4l-subdev,随后通过 dev_set_name() 形成 v4l-subdev0 等设备模型名称。

sd->name 可以是 Sensor A,设备文件仍可能叫 /dev/v4l-subdev2。前者描述模块,后者是动态节点编号。节点号不是 I²C 地址、media entity ID,也不是 pad 编号。

还要区分 vdev->num 与 vdev->minor:前者参与名称,后者组成设备号并索引核心表;是否存在固定范围映射取决于配置,不能假设路径末尾的 X 总等于次设备号。

5.2 共用字符设备核心,不共用所有业务操作

通用注册过程中的关键连接是:

分配名称编号和 minor
    → video_devices[minor] = vdev
    → cdev->ops = 核心 v4l2_fops
    → cdev_add()
    → device_register()
    → 建立媒体接口对象
    → 设置 V4L2_FL_REGISTERED

cdev 接的是核心 struct file_operations,而不是直接接 v4l2_subdev_fops。请求先进入公共包装,再转发到 vdev->fops 指定的子设备操作。两层操作表的类型也不同。

设备号使用 VIDEO_MAJOR,此接口的主设备号为 81。device_register() 把带有名称、类别和设备号的对象交给设备模型;通常由 devtmpfs 和系统设备管理机制使对应节点在 /dev 可见。它不是 v4l2-device.c 直接打开目录、创建普通文件。通用设备文件机制是本篇之外的背景,补充对照参考 D。

5.3 接口出现与整组发布不是一个原子动作

每个 __video_register_device() 成功后,就已经设置自己的注册标志。批量函数随后才补建该子设备的媒体接口关联,再继续下一个对象。因此,不能把整个批量过程理解成“所有子设备都准备完,才同时让应用看到”。

底层注册函数本身也有“设备模型已经发布、注册标志尚未激活”的短暂阶段;核心 open 会检查注册状态。启动、注销与外部访问之间的完整同步,不能只靠一个 sd->devnode 指针保证。

图 3:一项节点按阶段建立;发生错误后进入后文分析的清理分支。图中不是把整组注册画成一次不可分割事务。

6. 为什么还要给节点建立一个 Media Controller 接口?

6.1 sd->entity 已经代表模块,不需要重复注册一份

子设备登记到 v4l2_device 时,在相应配置下,sd->entity 已经进入媒体设备的实体集合。创建直接节点时,核心另外建立类型为 MEDIA_INTF_T_V4L_SUBDEV 的接口对象,用设备号表示这个入口。

在 VFL_TYPE_SUBDEV 分支中,通用注册没有把新 vdev->entity 当成一份新的 Sensor 实体注册。模块实体仍是原来的 sd->entity,节点接口是 vdev->intf_devnode。

6.2 接口 Link 不是图像数据 Link

批量注册器在节点成功后,把原实体与新接口关联起来:

link = media_create_intf_link(&sd->entity,
                  &vdev->intf_devnode->intf,
                  MEDIA_LNK_FL_ENABLED |
                  MEDIA_LNK_FL_IMMUTABLE);

这里的 media_create_intf_link() 表示“这个接口可以访问该实体”,不是把 Sensor 输出 pad 连到 ISP 输入 pad。数据连接使用 pad link;接口连接不代表像素流向。

图 4:横向是模块之间的数据连接,纵向虚线是实体与直接访问接口的关联。建立接口 Link 不会启动 Sensor,也不会自动配置相邻格式。

实体中的兼容设备号字段与新的接口对象,共同帮助应用从拓扑识别对应节点。实际程序应根据拓扑与节点信息定位设备,而不是假定 Sensor 永远是编号 0。

还有一个必须保留的实现边界:所读 __video_register_device() 把 video_register_media_controller() 的返回值存到 ret,随后仍设置注册位并返回 0,没有在这一处根据负值回滚。外层又假定接口对象已经建立。所以“底层返回 0”不足以证明该实现所有媒体接口异常都被妥善处理。 这是源码中的静态审计点,不应在流程图中补画一条实际不存在的错误出口,也不在这里宣称已经通过硬件故障注入复现。

7. 注册到一半失败,究竟会撤销哪些节点?

7.1 当前对象要按已经到达的阶段处理

如果 kzalloc() 失败,还没有新的节点对象。如果 __video_register_device() 返回负错误,当前分支清空 sd->devnode,直接释放这次申请的 vdev,再进入公共清理。

如果字符节点已经注册,只是后面的接口 Link 分配失败,就不能直接 kfree(vdev):它已经进入设备模型,需要走 video_unregister_device() 及最终引用释放。

注册失败不等于子设备对象已经注销。sd 的组织关系、媒体实体和驱动私有资源,还有各自的外层回滚;批量节点函数不包办这些任务。

7.2 公共清理实际使用 break,而不是 continue

当前实现的清理代码为:

clean_up:
    list_for_each_entry(sd, &v4l2_dev->subdevs, list) {
if (!sd->devnode)
break;
        video_unregister_device(sd->devnode);
    }

return err;

这段循环遇到第一个 sd->devnode == NULL 就结束。它既没有记录“本次新注册的节点集合”,也没有跳过中间无节点对象继续扫描。

假设按链表顺序有 A、B、C、D:A 和 C 需要节点,B 不需要,D 在申请节点时失败。A 和 C 在此之前都已注册。清理到 A 会注销 A;下一项 B 没有节点,循环随即停止,C 不会在这轮清理中被访问。外层后续可能继续注销子设备,但不能把这个函数自身描述成已经撤销全部节点。

图 5:演示的是对当前循环的条件推演。B 不暴露节点是合法配置;图中并未假定所有驱动都会采用这一排列。

即使链表中没有这种空洞,公共清理也可能注销调用前就已存在的节点,因为它不区分旧节点和本次新增节点。因此,重复批量注册时也不能认为失败必然只撤销本次变化。

这不是建议把原文悄悄改成 continue。若要修复或重构,需要同时审计既有节点归属、最终释放、模块引用和并发访问。本文保留实际代码,用来建立正确的失败路径认识。

8. open 以后,file->private_data 为什么不是 sd?

8.1 一份节点,多份打开上下文

节点关联的是一个长期存在的 sd。应用可以独立打开它多次,每次操作却可能有不同的 TRY 格式、事件订阅和等待状态。这些打开级数据不能全部塞进同一个 sd。

因此,这份实现使用 v4l2_subdev_fh。按补充头文件,其关键成员是内嵌的 v4l2_fh vfh、模块指针 owner,以及子设备 API 配置下的 v4l2_subdev_state *state。vfh 是内嵌成员,不是另一次分配。

文件保存的是:

file->private_data = &subdev_fh->vfh;

查找子设备,则从当前节点的驱动数据取得;两条查找链不同:

file → video_devdata(file) → video_get_drvdata(vdev) → sd
file → private_data → v4l2_fh → 外层 v4l2_subdev_fh → state

8.2 核心先取得节点引用,再调用子设备 open

公共 v4l2_open() 在全局节点锁下查表、检查注册状态并取得 video_device 的设备引用,然后释放全局锁,转入 vdev->fops->open()。

对当前节点,这个回调是 subdev_open()。其实现如下:

staticintsubdev_open(struct file *file)
{
structvideo_device *vdev = video_devdata(file);
structv4l2_subdev *sd = vdev_to_v4l2_subdev(vdev);
structv4l2_subdev_fh *subdev_fh;
int ret;

    subdev_fh = kzalloc(sizeof(*subdev_fh), GFP_KERNEL);
if (subdev_fh == NULL)
return -ENOMEM;

    ret = subdev_fh_init(subdev_fh, sd);
if (ret) {
        kfree(subdev_fh);
return ret;
    }

    v4l2_fh_init(&subdev_fh->vfh, vdev);
    v4l2_fh_add(&subdev_fh->vfh);
    file->private_data = &subdev_fh->vfh;

if (sd->v4l2_dev->mdev && sd->entity.graph_obj.mdev->dev) {
structmodule *owner;

        owner = sd->entity.graph_obj.mdev->dev->driver->owner;
if (!try_module_get(owner)) {
            ret = -EBUSY;
goto err;
        }
        subdev_fh->owner = owner;
    }

if (sd->internal_ops && sd->internal_ops->open) {
        ret = sd->internal_ops->open(sd, subdev_fh);
if (ret < 0)
goto err;
    }

return0;

err:
    module_put(subdev_fh->owner);
    v4l2_fh_del(&subdev_fh->vfh);
    v4l2_fh_exit(&subdev_fh->vfh);
    subdev_fh_free(subdev_fh);
    kfree(subdev_fh);

return ret;
}

成功顺序是:分配打开对象,创建 state,初始化并挂接标准 v4l2_fh,保存文件私有指针,按条件取得媒体设备所属驱动模块的引用,最后调用可选的 internal_ops->open()。

这里没有统一执行 sd->ops->video->s_stream()。某个驱动的内部 open 可以做额外电源或资源准备,但不能据此把所有子设备 open 都解释成“开流”。

8.3 state 的分配会调用 init_cfg

subdev_fh_init() 调用 __v4l2_subdev_state_alloc()。后者分配 state;有 pads 时分配相应的 pad 配置;选择 sd->state_lock 或自己的内部锁;持锁调用可选 pad.init_cfg()。

缺失 init_cfg 导致的 -ENOIOCTLCMD 在这里可被接受,其他负错误则触发释放。VIMC Sensor 提供自己的初始化回调,将 TRY 格式设置为其默认值。因此新打开的 TRY 状态不应被普遍描述为“自动拷贝当前 ACTIVE 状态”。

图 6:独立 open 分配不同的打开对象与 TRY 存储;dup 仍引用原文件对象。相同的 sd 可以服务多个打开上下文,ACTIVE 的保存位置由该驱动实现决定。

8.4 open 失败不会由未来的 close 自动补救

state 创建失败时释放刚分配的打开对象;走到标准 fh 已挂接以后失败,则按 module_put → fh_del → fh_exit → state_free → kfree 清理,返回外层归还节点引用。

注意两个实际细节:这个错误分支没有将 file->private_data 再赋为 NULL;internal_ops->open() 返回错误以后也不会自动调用其 close 回调。不能把失败的文件对象继续当作成功打开的句柄使用,内部 open 自己取得的半初始化资源也应自己回滚。

若 CONFIG_VIDEO_V4L2_SUBDEV_API 未启用,subdev_open() 本身还有返回 -ENODEV 的替代实现;这与前面公开注册包装的配置分支需要一起看。

实现对照:v4l2_open()、subdev_open()、subdev_fh_init()、__v4l2_subdev_state_alloc()、vimc_sensor_init_cfg();打开对象声明见参考 C。

9. 一次 G_FMT 怎样到达 Sensor 回调?

9.1 子设备有自己的文件操作表

conststructv4l2_file_operationsv4l2_subdev_fops = {
    .owner = THIS_MODULE,
    .open = subdev_open,
    .unlocked_ioctl = subdev_ioctl,
#ifdef CONFIG_COMPAT
    .compat_ioctl32 = subdev_compat_ioctl32,
#endif
    .release = subdev_close,
    .poll = subdev_poll,
};

这里没有 read、write、mmap,也没有一套自动连接 VB2 的缓冲区回调。poll 的作用稍后单独分析。表中 .release = subdev_close 与节点注册时设置的 vdev->release 更不是同一个成员。

应用执行 ioctl 后的主线为:

核心 v4l2_ioctl()
    → vdev->fops->unlocked_ioctl = subdev_ioctl
    → video_usercopy(..., subdev_do_ioctl_lock)
        → 参数进入内核临时对象
        → subdev_do_ioctl_lock()
            → 选 state、按条件加锁
            → subdev_do_ioctl()
            → 解锁
        → 结果回写与临时参数清理

subdev_ioctl() 只有一条组合调用:

staticlongsubdev_ioctl(struct file *file, unsignedint cmd,
unsignedlong arg)

{
return video_usercopy(file, cmd, arg, subdev_do_ioctl_lock);
}

它复用 video_usercopy() 的参数处理,却不经过普通节点的 video_ioctl2() → __video_do_ioctl() 标准分发表。因此,不能给子设备节点随意套上那条路径中的 valid_ioctls、操作优先级和队列选锁规则。

9.2 按命令取出 which,再选择状态

subdev_ioctl_get_state() 识别格式、选区及相关枚举命令,从参数结构取出 which:TRY 选择当前 subdev_fh->state;其他值先走 ACTIVE 状态取得路径。没有对应状态需求的命令返回 NULL。

这里是在选择状态,不是完整的合法性验证。无效的 which 后面还要被相应检查拒绝;不能把“选到了 ACTIVE 分支”当成合法 ACTIVE 请求。

VIMC Sensor 没有在这条接入路径上使用核心管理的 ACTIVE state,而是保存自己的 mbus_format。因此 ACTIVE 分支取得 NULL 并不自动等于错误。TRY 则必须具备有效的 state 及所需 pad 存储。

9.3 具体分支清理保留字段,再执行 pad.get_fmt

subdev_do_ioctl() 中的分支是:

case VIDIOC_SUBDEV_G_FMT: {
structv4l2_subdev_format *format = arg;

memset(format->reserved, 0, sizeof(format->reserved));
memset(format->format.reserved, 0, sizeof(format->format.reserved));
return v4l2_subdev_call(sd, pad, get_fmt, state, format);
}

所读辅助层实际先进入 call_get_fmt_state(),再调用 call_get_fmt()。前者由 DEFINE_STATE_WRAPPER 生成:传入 state 非空时直接沿用;传入 NULL 时尝试取得并锁住核心 ACTIVE state,结束后配对解锁。后者依次检查格式参数、which、pad 和 TRY 状态,再调用具体的 sd->ops->pad->get_fmt()。VIMC 没有核心 ACTIVE state 时,这条补偿路径也不会凭空创建它。v4l2_subdev_call() 对操作表存在性和包装层的选择,补充按固定版本头文件核对。

最后到达 VIMC Sensor:

staticintvimc_sensor_get_fmt(struct v4l2_subdev *sd,
                   struct v4l2_subdev_state *sd_state,
                   struct v4l2_subdev_format *fmt)

{
structvimc_sensor_device *vsensor =
container_of(sd, structvimc_sensor_device, sd);


    fmt->format = fmt->which == V4L2_SUBDEV_FORMAT_TRY ?
              *v4l2_subdev_get_try_format(sd, sd_state, fmt->pad) :
              vsensor->mbus_format;

return0;
}

TRY 从当前打开的 pad 配置读取;ACTIVE 从 vsensor->mbus_format 读取。驱动填的是已经准备好的内核参数副本,video_usercopy() 随后才把结果回写到应用。

图 7:输入复制和输出回写包围分发函数。驱动返回以后先解开实际取得的锁,再回到参数层;这不是一条只向下、不返回的调用链。

缺少具体回调时,调用辅助宏可以返回 -ENOIOCTLCMD;外层 video_usercopy() 会把这类“不支持”结果统一成 -ENOTTY。而参数错误、节点注销或驱动自有错误来自不同层,不应只凭一个最终 errno 判断失败位置。

这里展示本机标准 ioctl 主线。compat_ioctl32 另有子设备的转发入口,不能把这条图直接当作所有 32 位兼容调用的完整验证。

10. 锁在哪里,哪些并发没有被自动保护?

完整的锁包装函数如下:

staticlongsubdev_do_ioctl_lock(struct file *file, unsignedint cmd, void *arg)
{
structvideo_device *vdev = video_devdata(file);
structmutex *lock = vdev->lock;
long ret = -ENODEV;

if (lock && mutex_lock_interruptible(lock))
return -ERESTARTSYS;

if (video_is_registered(vdev)) {
structv4l2_subdev *sd = vdev_to_v4l2_subdev(vdev);
structv4l2_fh *vfh = file->private_data;
structv4l2_subdev_fh *subdev_fh = to_v4l2_subdev_fh(vfh);
structv4l2_subdev_state *state;

        state = subdev_ioctl_get_state(sd, subdev_fh, cmd, arg);

if (state)
            v4l2_subdev_lock_state(state);

        ret = subdev_do_ioctl(file, cmd, arg, state);

if (state)
            v4l2_subdev_unlock_state(state);
    }

if (lock)
        mutex_unlock(lock);
return ret;
}

外层首先按需取得 vdev->lock,再检查节点是否仍注册;选出非空 state 后,按需取得 state 锁。退出时先解 state 锁,再解节点锁。输入复制和最终结果回写在这些锁之外。

要特别留意,前面的通用子设备节点构造代码并未给 vdev->lock 赋值。它来自清零对象,因此在没有后续设置的路径中为 NULL。不能照搬普通采集节点的队列锁配置,声称所有子设备 ioctl 自动共用一把操作锁。

state 锁也不是“所有驱动状态都被保护”的证明。TRY 有它自己的配置对象;如果驱动设置了 sd->state_lock,多个 state 可以共用该锁。ACTIVE 使用驱动私有格式且核心 active state 为 NULL 时,本函数不会凭空再找一把锁保护那份私有格式。

所以,框架包装的锁、控制处理器的锁、驱动的运行状态锁,以及注销时序,要结合实际接入一起审计。驱动若把外层节点锁和 state 锁设成同一把非递归互斥锁,也不能忽略这里的两次加锁。

这份代码在取得操作锁后重新检查注册状态,是为了拒绝已经失效的节点访问;它不是对所有热拔插和硬件销毁问题的完整解决方案。

11. “只读子设备”为什么仍然可以执行 S_FMT?

11.1 只读是一个框架策略位,不等于 O_RDONLY

创建只读节点时设置 V4L2_FL_SUBDEV_RO_DEVNODE。VIDIOC_SUBDEV_QUERYCAP 会据此返回 V4L2_SUBDEV_CAP_RO_SUBDEV,相应 ioctl 分支再决定拒绝哪些操作。

这与设备文件权限、应用使用 O_RDONLY 还是 O_RDWR 是不同层次。它不是把所有写方向 ioctl 一律关掉,也不是让内核中的 v4l2_subdev_call() 全部变成只读。

11.2 TRY 修改的是试算状态

格式设置分支保留了这个条件:

case VIDIOC_SUBDEV_S_FMT: {
structv4l2_subdev_format *format = arg;

if (format->which != V4L2_SUBDEV_FORMAT_TRY && ro_subdev)
return -EPERM;

memset(format->reserved, 0, sizeof(format->reserved));
memset(format->format.reserved, 0, sizeof(format->format.reserved));
return v4l2_subdev_call(sd, pad, set_fmt, state, format);
}

只读节点拒绝非 TRY 的格式修改,但允许 TRY 试算。因为 TRY 不提交当前有效配置;返回调整后的候选格式,正是协商接口的用途。

图 8:图中画的是当前 S_FMT 分支的策略判断,后面仍有参数检查和驱动能力检查。只读策略不是所有命令的总开关。

11.3 不要把实现范围写大

所读实现对 ACTIVE 格式、ACTIVE 选区/裁剪、帧间隔设置以及部分时序/制式设置有显式只读检查。但 VIDIOC_S_CTRL、VIDIOC_S_EXT_CTRLS、VIDIOC_S_EDID 及默认私有 ioctl 分支,并没有在这里统一检查 ro_subdev。

因此不能把这个标志当作“任何设备状态都绝不可能被修改”的安全保证。其他层可能还有自己的权限、控制标志和驱动检查;分析特定命令时必须继续追踪,而不能仅依据节点叫只读就结束判断。

重复调用只读注册包装也不会改造已经存在的节点,因为注册循环会跳过非空的 sd->devnode。

12. poll 等待的是事件,不是图像帧

subdev_poll() 先检查 V4L2_SUBDEV_FL_HAS_EVENTS。没有事件能力时返回 EPOLLERR;支持时把当前 fh 的事件等待队列登记到 poll,并在存在待取事件时返回 EPOLLPRI。

这条路径没有检查 VB2 的完成列表。应用收到事件就绪以后,使用事件接口取出事件描述,而不是通过 DQBUF 从子设备节点取得一帧。

标志存在还不够:订阅回调、事件产生路径及事件类型也要真实支持。只设置 HAS_EVENTS 不会自动生成事件。控制命令同样按 fh 关联的 handler 进入控制框架,并非全部经过 core_ops.ioctl。

最后也不要从 sd->ops->video->s_stream 的存在推导“应用可以对这个节点直接 STREAMON”。本 subdev_do_ioctl() 没有普通 VIDIOC_STREAMON 到 s_stream 的标准转换分支;未知命令会尝试驱动的 core.ioctl。驱动私有扩展需要单独分析,不能作为通用节点能力。

实现对照:subdev_poll()、subdev_do_ioctl()、v4l2_subdev_fops。

13. close、注销和最后释放为什么不是同一次清理?

13.1 普通关闭清理的是一次打开

subdev_close() 的完整过程为:

staticintsubdev_close(struct file *file)
{
structvideo_device *vdev = video_devdata(file);
structv4l2_subdev *sd = vdev_to_v4l2_subdev(vdev);
structv4l2_fh *vfh = file->private_data;
structv4l2_subdev_fh *subdev_fh = to_v4l2_subdev_fh(vfh);

if (sd->internal_ops && sd->internal_ops->close)
        sd->internal_ops->close(sd, subdev_fh);
    module_put(subdev_fh->owner);
    v4l2_fh_del(vfh);
    v4l2_fh_exit(vfh);
    subdev_fh_free(subdev_fh);
    kfree(subdev_fh);
    file->private_data = NULL;

return0;
}

先调用可选的内部 close,再归还这次打开持有的模块引用,摘除并退出标准 fh,释放 TRY state 和打开对象,最后清空 file->private_data。

这里没有 kfree(sd),也不因本次关闭就把长期节点销毁。该函数清理的是本次打开。外层 v4l2_release() 最后会归还打开时取得的节点引用;支持 Media Request 的组织对象还可能在外层取得请求串行化锁,但这不改变打开对象与节点对象的区别。

按通用文件机制,触发最终文件 release 的边界是 struct file 的最后引用退出,不是任意一个别名 fd 被 close。dup() 不建立新的 subdev fh;关闭其中一个 fd,也不会独立调用一次 subdev_close()。通用文件引用机制补充对照参考 E。

13.2 注销先撤销访问资格,不是强制释放所有打开对象

v4l2_device_unregister_subdev() 先从组织链表摘除 sd,调用可选 unregistered,清空 sd->v4l2_dev,并在适用时注销实体。存在 sd->devnode 时再调用 video_unregister_device()。

后者在核心锁保护下清除 V4L2_FL_REGISTERED,唤醒相应事件等待者,随后注销设备模型对象。已有文件可能仍持有节点引用;正常路径不会在这里强行释放这些文件的 subdev_fh。

这时新 open 可能因为路径已经消失得到 ENOENT,或在节点状态检查处得到 ENODEV;已有文件再次 ioctl 也会在注册检查处被拒绝。具体返回取决于到达的层次。单纯唤醒事件等待者,不等于所有等待函数都有了完整的断开退出条件。

13.3 最后的节点引用退出,才进入节点释放回调

v4l2-dev.c 中的设备释放函数会删除核心表项与字符设备、回收编号、处理媒体接口,再执行 vdev->release(vdev)。对子设备节点,回调是:

staticvoidv4l2_device_release_subdev_node(struct video_device *vdev)
{
    v4l2_subdev_release(video_get_drvdata(vdev));
    kfree(vdev);
}

这里取回 sd,完成子设备释放通知及模块引用归还,再释放节点自身的 video_device。核心随后仅在相应 v4l2_device->release 回调存在的路径上归还组织对象引用,不能遗漏这个条件。

图 9:假设有一个成功打开文件,且驱动已经为断开做好同步。没有打开引用时,最终节点释放可能在注销过程中就发生,不需要等待一次未来 close。

13.4 五个同名 close/release 不要混在一起

入口
对应层次
sd->internal_ops->close
驱动对一次子设备节点打开的收尾。
v4l2_subdev_fops.release = subdev_close
清理本次 subdev fh 和 TRY state。
核心 v4l2_release
文件释放包装,归还节点设备引用。
vdev->release = v4l2_device_release_subdev_node
节点对象最后释放。
v4l2_dev->release
组织对象的最终释放通知,例如 VIMC 在这里释放各私有模块。

此外还有 sd->internal_ops->release,它位于子设备释放路径,不是每次打开的 close。所读 v4l2_subdev_release() 在调用它以后还执行 sd->devnode = NULL:

staticvoidv4l2_subdev_release(struct v4l2_subdev *sd)
{
structmodule *owner = !sd->owner_v4l2_dev ? sd->owner :NULL;

if (sd->internal_ops && sd->internal_ops->release)
        sd->internal_ops->release(sd);
    sd->devnode = NULL;
    module_put(owner);
}

这份代码的顺序要求回调返回后 sd 仍可访问。 不能直接把另一版本“在 internal release 中释放包含 sd 的整个对象”的范例套进来;那会使后续赋值访问已经释放的存储。这是对当前实现的安全性审计,不是宣称 VIMC 已发生该故障。VIMC 的 Sensor 等私有对象由更外层的 vimc_v4l2_dev_release() 统一释放,不能与这个回调混为一谈。

无论哪条路径,节点引用只能说明相应对象仍被持有,不能保证 Sensor 已供电、硬件仍在线或者异步处理已经停止。驱动必须另外同步实际访问和私有资源生命周期。

14. 做一个不启动采集的节点观察实验

14.1 先找实际节点,不猜编号

在测试系统上先读取现有拓扑,并查看节点描述:

media-ctl -d /dev/media0 -p
for n in /sys/class/video4linux/v4l-subdev*; do
    [ -d "$n" ] || continue
printf'%s: '"$(basename "$n")"
    cat "$n/name"
done

命令中的 media 编号是示例,应换成实际设备。只查看对应节点的格式,不要把捕获节点误当成 subdev,也不要从名称猜 pad 编号;pad 应来自拓扑或驱动定义。

14.2 程序观察什么

配套 examples/subdev_node_probe.c 做两次独立 open,再对第一次打开执行一次 dup。默认只查询子设备能力以及指定 pad 的 ACTIVE/TRY 格式;加 --try WIDTH HEIGHT 才对第一次打开执行 TRY 格式试算。

cc -std=c11 -Wall -Wextra -Werror -O2 \
   subdev_node_probe.c -o subdev_node_probe

./subdev_node_probe /dev/v4l-subdev0 0
./subdev_node_probe /dev/v4l-subdev0 0 --try 800 600

核心试算片段是:

trial = active;
trial.which = V4L2_SUBDEV_FORMAT_TRY;
trial.format.width = width;
trial.format.height = height;
if (xioctl(fd_a, VIDIOC_SUBDEV_S_FMT, &trial) < 0)
goto out;
/* 再分别查询 A、dup(A)、独立打开 B 的 TRY,以及 ACTIVE。 */

按接口约定和 VIMC 的实现,A 与其 dup 共享同一份 TRY 状态,B 有自己的状态;试算不提交 ACTIVE 配置。实际驱动可以调整尺寸,两个独立状态也可能恰好返回相同值,所以“数值相同”不能直接证明两者共用对象。

程序不设置 ACTIVE,不改变控制项,不调用 STREAMON,也不读取像素。仍应在空闲测试节点使用:open 可以触发驱动自己的准备动作;TRY 结果还可能受当前控制值与连接条件影响。实验输出不预设固定尺寸和节点号。

14.3 有价值的源码观察点

注册时观察 sd->name、sd->devnode、vdev->num、vdev->minor 和实际 fops;打开时观察两个 file 是否获得不同的 subdev_fh->state;查询时观察 which、选中的 state 和最终驱动回调;关闭时观察文件关闭与节点释放是否在不同时间发生。

不要在生产链路上直接用卸载驱动来验证引用关系。先完成资源停止与断开设计,再在专门测试环境中分析并发注销。

15. 从现象反推哪一阶段没有成立

现象
优先核对的位置
Sensor 已被接收端使用,却没有 subdev 节点
是否声明 HAS_DEVNODE、是否进入组织链表、是否调用节点注册、子设备 API 配置是否启用。
节点注册包装返回 0,却没有新增节点
配置存根、无候选对象、已有 devnode 被跳过,以及系统 /dev 挂载与设备管理。
/dev/v4l-subdevX
 与预想编号不同
自动编号、其他设备占用;用实体关联和节点描述定位,不猜序号。
对节点调用普通 QUERYCAP 失败
本接口使用 VIDIOC_SUBDEV_QUERYCAP;普通视频命令不等于子设备命令。
G_FMT 返回 EINVAL
pad、which、TRY state 和具体驱动校验。不能一律归因“Sensor 不支持”。
G_FMT 返回 ENOTTY
操作表或具体回调缺失、错误命令、默认 core ioctl 分支及外层错误转换。
只读节点 TRY 设置成功,ACTIVE 设置失败
先核对只读策略位及实际分支;这可以是预期行为。
独立 open 的 TRY 互相影响
是否实际上使用 dup/fork 共享文件;驱动是否错误地把 TRY 写入共享配置。
poll 不返回图像就绪
该节点 poll 面向事件,不是 VB2 帧完成。
注册到一半失败仍留下其他节点
检查 clean_up 的 break、链表中的无节点项、既有节点及外层回滚。
注销后出现旧句柄访问异常
sd 和外层私有对象的寿命、internal close/release 的访问、操作同步及真实硬件停止。

本篇的完整闭环是:已有子设备声明需要入口,上层为它注册节点;打开建立独立上下文,ioctl 找回同一子设备执行操作;关闭退出本次上下文,注销与最终引用归还再结束节点生命周期。

节点解决了“怎样找到并操作这个模块”。下一阶段再进入 Media Controller 的实体、pad 与 link,才能把多个模块之间的连接与配置关系完整串起来。

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

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

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

相关学习资料