ARTICLE · 1114543
V4L2 源码深度解析(二):一条 ioctl 命令如何穿过框架,到达驱动?
structv4l2_capabilitycap = {0};int ret = ioctl(fd, VIDIOC_QUERYCAP, &cap);这次调用看起来只是“查询设备能力”,背后却需要解决一组问题:fd 如何对应到当前节点?VIDIOC_QUERYCAP 怎样选中处理函数?应用传入的地址能否直接交给驱动?回调执行时由哪把锁保护?驱动返回成功后,为什么应用仍可能收到错误?
v4l2-dev.c 已经建立了文件操作进入 V4L2 的入口。继续往下,v4l2-ioctl.c 负责把一个标准 ioctl 请求,转换成能够在内核中执行的命令,再把结果送回应用。
这不是一张巨大的“命令到函数”对照表,而是一套同时管理参数边界、命令能力、操作串行化和返回语义的执行框架。
本文围绕 media/v4l2-core/v4l2-ioctl.c 的实现展开,配合相同源码树中的入口层、VIMC 与 VB2 文件衔接调用关系。头文件接口采用 Linux v6.1 的对应声明辅助说明,不据此推定整棵源码树的版本。主线限定为接入 video_ioctl2() 的标准视频节点;具体平台的寄存器、图像搬运与 Sensor 时序不在本篇展开。
1. 先建立全景:一次 ioctl 分成“进去、执行、出来”三段

图 1:命令进入适配器后,无论由驱动还是框架完成,都先返回标准分发层,按实际持有情况释放锁,再回到外层参数处理。图中展示已经进入分发后的主干,早退与回写条件在后文展开。
完整主线可以压缩成:
应用 ioctl(fd, cmd, arg) → VFS 的文件操作入口 → v4l2-dev.c::v4l2_ioctl() → vdev->fops->unlocked_ioctl() → video_ioctl2() → video_usercopy() ├─ 转换命令、准备内核参数、处理嵌套数组 ├─ __video_do_ioctl() │ ├─ 选锁、检查状态与命令能力 │ └─ v4l_* 适配器 → vidioc_* 回调或框架处理 └─ 决定是否回写、恢复指针、释放临时内存 → 返回应用这里的嵌套关系很重要:video_usercopy() 不是调用结束之后才轮到 __video_do_ioctl();**它先完成输入处理,在中间调用 __video_do_ioctl(),再完成输出处理。
可以把职责分为三层:
v4l2_ioctl() | ||
video_usercopy()__video_do_ioctl() | ||
v4l_querycap()v4l_s_fmt()、vidioc_* |
第三层不一定直接抵达硬件。vidioc_qbuf 可以连接 VB2,控制命令可以由控制框架承接,优先级命令还可以由核心自己完成。**“命令已分发”与“硬件已执行某个动作”不能画等号。
2. 入口为什么会来到 video_ioctl2()?
2.1 核心操作表与驱动操作表并不是同一张表
v4l2-dev.c 中的 v4l2_ioctl() 负责转发。关键实现是:
staticlongv4l2_ioctl(struct file *filp, unsignedint cmd, unsignedlong arg){structvideo_device *vdev = video_devdata(filp);int ret = -ENODEV;if (vdev->fops->unlocked_ioctl) {if (video_is_registered(vdev)) ret = vdev->fops->unlocked_ioctl(filp, cmd, arg); } else ret = -ENOTTY;return ret;}这里没有展开所有 VIDIOC_*,而是把请求交给 vdev->fops->unlocked_ioctl。
采用标准命令分发时,驱动需要显式建立下面的连接:
.unlocked_ioctl = video_ioctl2,例如,同一源码树中的 VIMC 捕获节点提供了这张文件操作表:
staticconststructv4l2_file_operationsvimc_capture_fops = { .owner = THIS_MODULE, .open = v4l2_fh_open, .release = vb2_fop_release, .read = vb2_fop_read, .poll = vb2_fop_poll, .unlocked_ioctl = video_ioctl2, .mmap = vb2_fop_mmap,};因此,进入 v4l2-ioctl.c 的前提不是“节点名字叫 video”,而是驱动的 ioctl 入口接上了标准分发函数。自定义入口可以完全自行处理,也可以选择性调用通用辅助函数,不能无条件套用本文的整条路径。
2.2 先把参与者放到同一张图中

图 2:全局命令描述表提供处理规则,节点操作表提供具体实现,文件对象提供当前上下文。
struct file | private_data | |
struct video_device | ||
struct v4l2_file_operations | unlocked_ioctl 接到 video_ioctl2() | |
struct v4l2_ioctl_ops | vidioc_querycap、格式操作、缓冲区操作等 | |
struct v4l2_ioctl_info | ||
struct v4l2_fh |
v4l2_ioctl_ops 是“这个节点交出了哪些实现”,v4l2_ioctl_info 是“框架怎样处理这一类命令”。前者由驱动选择,后者构成核心的全局命令表
2.3 fh 和 vfh 为什么同时存在?
__video_do_ioctl() 先取得:
void *fh = file->private_data;structv4l2_fh *vfh = NULL;只有节点带有 V4L2_FL_USES_V4L2_FH 标志时,才进一步把 file->private_data 解释为 struct v4l2_fh *。
两者并不是两次不同的分配:fh 是要传给后续回调的不透明上下文指针;vfh 则表示核心可以按照标准结构读取其中的字段。
这个区别避免了核心对任意 private_data 都按 v4l2_fh 解引用。它也说明:**设置 ioctl_ops 不会自动创建每次打开的上下文;对象的构造必须在正确的打开路径中完成。
3. cmd 不是一个普通序号:先读懂 ioctl 的编码
3.1 一个命令同时携带四类信息
Linux 使用 _IO、_IOR、_IOW、_IOWR 等宏编码 ioctl 命令。对于 V4L2,常见定义形式如下:
#define VIDIOC_QUERYCAP _IOR('V', 0, struct v4l2_capability)#define VIDIOC_S_FMT _IOWR('V', 5, struct v4l2_format)#define VIDIOC_STREAMON _IOW('V', 18, int)理解这些定义时,重点不是背十六进制数值,而是知道它们包含哪些信息。[^uapi][^ioctl-abi]
_IOC_TYPE(cmd) | 'V' | v4l2_format.type |
_IOC_NR(cmd) | ||
_IOC_DIR(cmd) | open() 的访问模式 | |
_IOC_SIZE(cmd) |
方向从应用角度理解:_IOW 表示应用向内核提交参数,_IOR 表示应用从内核接收参数,_IOWR 表示双向传递。这里讨论的是 ioctl 参数,不是 read() 与 write() 的图像数据通路。
3.2 为什么查询命令也可能需要双向复制?
VIDIOC_QUERYCAP 不需要应用先指定一个查询索引,因此可以先在内核构造一个清零结果,再回传设备能力。
但枚举类接口通常需要先输入 index,或者输入 type、像素格式等选择条件,再输出对应条目。“查询”描述功能,“读写方向”描述参数如何传递,两者不是一回事。
同理,VIDIOC_S_FMT 虽然是“设置”,仍然需要把驱动实际接受的格式返回。因此,应用提交的宽高、像素格式不一定与返回内容完全相同。
3.3 STREAMON 为什么传地址,而不是直接传整数?
典型用法是:
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;int ret = ioctl(fd, VIDIOC_STREAMON, &type);这条命令具有写入方向,框架会把参数复制到内核临时存储中。随后 v4l_streamon() 才从那个内核地址中取出缓冲区类型,再以值的形式传给驱动回调:
staticintv4l_streamon(const struct v4l2_ioctl_ops *ops, struct file *file, void *fh, void *arg){return ops->vidioc_streamon(file, fh, *(unsignedint *)arg);}所以应区分:应用接口接收参数地址,命令适配器收到内核副本地址,而 vidioc_streamon 回调最终接收缓冲区类型值。
4. 命令描述表:struct v4l2_ioctl_info 如何驱动分发?
4.1 结构体不是只有一个函数指针
structv4l2_ioctl_info {unsignedint ioctl; u32 flags;constchar * const name;int (*func)(const struct v4l2_ioctl_ops *ops, struct file *file,void *fh, void *p);void (*debug)(constvoid *arg, bool write_only);};逐个看成员:
ioctl | |
flags | |
name | |
func | vidioc_* 指针 |
debug |
其中 func 的统一签名把四种信息汇到一起:节点操作表 ops、当前文件 file、打开上下文 fh、已经准备好的参数 p。由此,外层分发不必为每一种参数结构单独写一个入口。
4.2 IOCTL_INFO 宏建立的是带指定下标的数组
#define IOCTL_INFO(_ioctl, _func, _debug, _flags) \ [_IOC_NR(_ioctl)] = { \ .ioctl = _ioctl, \ .flags = _flags, \ .name = #_ioctl, \ .func = _func, \ .debug = _debug, \ }命令表的典型条目如下:
IOCTL_INFO(VIDIOC_QUERYCAP, v4l_querycap, v4l_print_querycap, 0),IOCTL_INFO(VIDIOC_S_FMT, v4l_s_fmt, v4l_print_format, INFO_FL_PRIO),IOCTL_INFO(VIDIOC_REQBUFS, v4l_reqbufs, v4l_print_requestbuffers, INFO_FL_PRIO | INFO_FL_QUEUE),IOCTL_INFO(VIDIOC_QUERYBUF, v4l_querybuf, v4l_print_buffer, INFO_FL_QUEUE | INFO_FL_CLEAR(v4l2_buffer, length)),IOCTL_INFO(VIDIOC_QBUF, v4l_qbuf, v4l_print_buffer, INFO_FL_QUEUE),IOCTL_INFO(VIDIOC_DQBUF, v4l_dqbuf, v4l_print_buffer, INFO_FL_QUEUE),IOCTL_INFO(VIDIOC_STREAMON, v4l_streamon, v4l_print_buftype, INFO_FL_PRIO | INFO_FL_QUEUE),IOCTL_INFO(VIDIOC_STREAMOFF, v4l_streamoff, v4l_print_buftype, INFO_FL_PRIO | INFO_FL_QUEUE),[_IOC_NR(_ioctl)] 是指定下标初始化。框架先用编号定位候选项,再判断完整命令值是否匹配;不是在运行时顺序遍历所有命令。
4.3 下标命中不等于命令合法
staticboolv4l2_is_known_ioctl(unsignedint cmd){if (_IOC_NR(cmd) >= V4L2_IOCTLS)returnfalse;return v4l2_ioctls[_IOC_NR(cmd)].ioctl == cmd;}这里先做数组边界检查,再比较完整命令。即使 _IOC_NR() 一样,只要命令组、方向或编码大小不同,也不能因此当成同一个标准 ioctl。
还有一种情况:指定下标形成的数组可能有空洞。完整值比较也可以避免把某个没有实际命令条目的槽位误当成有效接口。
但这不意味着任何不匹配的命令都会立刻被彻底拒绝。标准表外还有 vidioc_default 路径;自定义命令的合法性需要由相应处理器继续检查。
4.4 四个标志和一个“带数值的策略”
INFO_FL_PRIO | __video_do_ioctl() | |
INFO_FL_CTRL | ||
INFO_FL_QUEUE | v4l2_ioctl_get_lock() | |
INFO_FL_ALWAYS_COPY | ||
INFO_FL_CLEAR(...) | video_get_user() |
INFO_FL_CLEAR 与前四项不同,它不是一个简单开关:字段末尾相对结构起点的偏移被放到 flags 的高位中。
例如 INFO_FL_CLEAR(v4l2_control, id) 表达:输入只需要保留 id 及其之前的区域,后续内容先清零,再由命令处理器填写。它不是“把 id 清零”。
另一个要分清的宏是 CLEAR_AFTER_FIELD(p, field):它直接对现有结构体的尾部执行清零;INFO_FL_CLEAR 则把规则写进命令元数据,由参数输入阶段解释执行。两者目的相近,生效时机不同。
5. video_ioctl2() 很短,真正的工作在它连接的两端
longvideo_ioctl2(struct file *file,unsignedint cmd, unsignedlong arg){return video_usercopy(file, cmd, arg, __video_do_ioctl);}这个函数把 __video_do_ioctl 作为回调传给 video_usercopy()。
可以把它理解成一种固定组合:
通用参数编组框架:video_usercopy +标准视频命令处理器:__video_do_ioctl =标准视频节点 ioctl 入口:video_ioctl2video_usercopy() 负责参数如何进去、出来;传入的 func 决定参数准备好以后究竟怎样执行命令。函数指针把两类职责分开,也使参数编组能力可以供其他合适的路径复用。
6. video_usercopy() 前半段:把应用参数变成可处理的内核对象

图 3:参数编组的主要阶段。标准有方向命令与无方向命令具有不同的入口语义。
6.1 先理清临时变量
orig_cmd | |
cmd | video_translate_cmd() 转换后,用于内核标准处理的命令 |
sbuf[128] | |
mbuf | kmalloc() 分配的主结构体存储 |
parg | sbuf 或 mbuf |
array_buf | |
user_ptr | |
kernel_ptr | |
always_copy |
这里最重要的区别是:**应用地址、内核临时对象地址、结构体内部指针字段的地址,是三种不同的东西。
6.2 主结构体什么时候用栈,什么时候用堆?
if (_IOC_DIR(cmd) != _IOC_NONE) {if (ioc_size <= sizeof(sbuf)) { parg = sbuf; } else {/* too big to allocate from stack */ mbuf = kmalloc(ioc_size, GFP_KERNEL);if (NULL == mbuf)return -ENOMEM; parg = mbuf; } err = video_get_user((void __user *)arg, parg, cmd, orig_cmd, &always_copy);if (err)goto out;}源码以标准化后的 _IOC_SIZE(cmd) 决定主参数大小。小于或等于 128 字节,使用 sbuf;超过这个边界,使用 kmalloc(..., GFP_KERNEL)。
这个大小是参数结构体大小,不是图像分辨率乘像素字节数。即使设备采集的是大尺寸图像,VIDIOC_QBUF 在这里编组的仍是缓冲区描述,不会把整帧像素塞进这个小数组。
GFP_KERNEL 对应可以睡眠的常规分配场景;这条 ioctl 路径处于进程上下文,并不是要求立即完成的硬中断回调。分配失败时返回 -ENOMEM,不会继续调用驱动。
6.3 _IOC_NONE 是一个单独分支
命令没有编码参数方向时,前面的临时结构体分配与 video_get_user() 都会被跳过,parg 保留由 arg 转换来的值。
因此,“所有 ioctl 的 arg 都会自动复制为结构体”是不成立的。对标准有方向的命令,后续收到内核副本;对 _IOC_NONE,具体含义必须按该命令约定解释,不能无条件解引用。
6.4 video_get_user() 不只是 copy_from_user() 的包装
输入处理至少包含三种情况:
第一种:没有输入方向。
以原始命令方向判断,不含 _IOC_WRITE 时,先清零内核参数对象,不从应用复制主结构体。VIDIOC_QUERYCAP 就会走到这类处理。
第二种:包含输入方向,且原始命令与标准命令相同。
实际条件是 cmd == real_cmd,而不是“调用来自本机 ABI”。这时使用 copy_from_user();对于带 INFO_FL_CLEAR 的已知命令,只复制需要的前缀,其余部分清零。兼容进程发出的命令如果在两种 ABI 下布局与编码相同,也可以走这一分支。
第三种:包含输入方向,而且原始命令需要翻译。
只有在 cmd != real_cmd 时,才继续选择布局转换路径:处于兼容系统调用中时,调用 v4l2_compat_get_user();在特定的原生 32 位内核配置下,旧时间字段布局则由 CONFIG_COMPAT_32BIT_TIME 对应分支处理。
因此,“32 位应用访问 64 位内核”不等于“每个主参数都要调用专门的结构转换函数”。是否转换要看当前命令的编码与布局;已知嵌套数组还另有自己的兼容处理,不能由主结构体是否直接复制一概推导。
主线中的关键片段是:
unsignedint n = _IOC_SIZE(real_cmd);int err = 0;if (!(_IOC_DIR(cmd) & _IOC_WRITE)) {/* read-only ioctl */memset(parg, 0, n);return0;}/* * In some cases, only a few fields are used as input, * i.e. when the app sets "index" and then the driver * fills in the rest of the structure for the thing * with that index. We only need to copy up the first * non-input field. */if (v4l2_is_known_ioctl(real_cmd)) { u32 flags = v4l2_ioctls[_IOC_NR(real_cmd)].flags;if (flags & INFO_FL_CLEAR_MASK) n = (flags & INFO_FL_CLEAR_MASK) >> 16; *always_copy = flags & INFO_FL_ALWAYS_COPY;}最后还有一段统一处理:
if (!err && n < _IOC_SIZE(real_cmd))memset((u8 *)parg + n, 0, _IOC_SIZE(real_cmd) - n);这样做既减少无意义的输入字段对驱动的影响,也保证后续没有填写的输出区域不是未初始化的临时内容。
6.5 这里的参数名字容易读反
外层函数与辅助函数采用了不同命名:
video_usercopy() | video_get_user()video_put_user() 中 | |
|---|---|---|
orig_cmd | cmd | |
cmd | real_cmd |
因此,阅读 video_get_user() 时看到 _IOC_DIR(cmd),它检查的是原始 ABI 的方向;看到 _IOC_SIZE(real_cmd),它考虑的是标准结构所需空间。把两个名字的语义对齐,兼容路径就不会越读越乱。
7. 嵌套数组:为什么只复制一个 v4l2_buffer 还不够?
7.1 结构体中的指针,只是一个地址数值
多平面缓冲区接口中,主结构体包含 m.planes,它指向另一个 struct v4l2_plane[] 数组。
如果只复制外层 struct v4l2_buffer,复制过去的 m.planes 仍然是应用地址。驱动不能因为“外层结构体在内核中”,就把所有内部指针都当成已经转换好的内核对象。
这就是 check_array_args() 和 array_buf 存在的原因。

图 4:这里转换的是平面描述数组;图像存储本身仍由缓冲区与内存机制管理。
7.2 check_array_args() 在做什么?
多平面相关的分支是:
case VIDIOC_PREPARE_BUF:case VIDIOC_QUERYBUF:case VIDIOC_QBUF:case VIDIOC_DQBUF: {structv4l2_buffer *buf = parg;if (V4L2_TYPE_IS_MULTIPLANAR(buf->type) && buf->length > 0) {if (buf->length > VIDEO_MAX_PLANES) { ret = -EINVAL;break; } *user_ptr = (void __user *)buf->m.planes; *kernel_ptr = (void **)&buf->m.planes; *array_size = sizeof(struct v4l2_plane) * buf->length; ret = 1; }break;}注意三次赋值的不同含义:
*user_ptr 保存 buf->m.planes 原来的应用地址*kernel_ptr 保存“buf->m.planes 这个指针字段”的地址*array_size 计算需要复制多少个平面描述结构体kernel_ptr 是一个指向指针的指针,不是图像缓冲区本身。外层随后执行 *kernel_ptr = array_buf,真正修改的是内核主结构体中的那个指针字段。
check_array_args() 本身不分配数组,也不复制字节或替换指针。 它返回负错误码、0 或 1,分别表示检查失败、没有需要本层处理的数组、存在需要本层处理的数组;同时把数组大小、原始地址和指针字段位置交给外层。真正的分配、复制及指针替换都在 video_usercopy() 中执行。
buf->length 在这个分支中表示平面描述数组的元素数量。源码会拒绝超过 VIDEO_MAX_PLANES 的数量;数量为零则不会在这里建立数组副本,但仍可能在后续命令处理或 VB2 校验中被拒绝。这里的成功不代表缓冲区所有参数都已经合法。
7.3 已知数组不是只有平面描述
PREPARE_BUFQUERYBUF、QBUF、DQBUF 的多平面形式 | v4l2_plane[] | VIDEO_MAX_PLANES |
G_EDIDS_EDID | ||
G_EXT_CTRLSS_EXT_CTRLS、TRY_EXT_CTRLS | v4l2_ext_control[] | V4L2_CID_MAX_CTRLS |
v4l2_clip[] |
这些都是本实现显式识别的情况。**它不是任意 C 结构体的递归深拷贝工具,也不会自动理解自定义 ioctl 内部的所有指针。
对扩展控制而言,复制控制数组也不意味着每个条目中可能存在的字符串、复合类型载荷都已在这一层复制完毕;进一步的内容解释属于控制框架或相应处理路径。
7.4 数组什么时候改成内核地址?
数组存储使用 kvmalloc(),原始内容复制成功后,再把主结构体里的指针改为内核数组地址:
if (has_array_args) { array_buf = kvmalloc(array_size, GFP_KERNEL); err = -ENOMEM;if (array_buf == NULL)goto out;if (in_compat_syscall()) err = v4l2_compat_get_array_args(file, array_buf, user_ptr, array_size, orig_cmd, parg);else err = copy_from_user(array_buf, user_ptr, array_size) ? -EFAULT : 0;if (err)goto out; *kernel_ptr = array_buf;}这样,标准处理器看到的主结构体和已知描述数组都已经进入内核临时存储。
但这里复制的是平面描述。某个平面描述中的缓冲区偏移、DMA-BUF 文件描述符或应用缓冲区地址仍然各有自己的语义,需要缓冲区层按内存模式解释;不能把这些值都视为可直接访问的像素指针。
7.5 回写前为什么必须还原指针?
当本次调用进入结果回写分支时,外层会把 m.planes 等指针字段恢复为最初的应用地址,然后回写数组和主结构体。输入准备失败、不支持命令或不允许回写的错误会直接清理临时对象,不会无条件执行这一步。
原因不是“驱动需要旧地址”,而是:返回给应用的结构体不应该夹带只在本次 ioctl 内有效的内核临时地址。 如果把替换后的指针直接回写,应用拿到的值没有正确的接口语义,也会破坏地址空间边界。
数组与主结构体都属于本次调用的临时对象。回调不能把 parg、array_buf 或其中临时描述项的地址保存起来,留待异步工作或下一帧中断继续使用;需要保留的状态必须转存到具有正确生命周期的驱动对象中
8. __video_do_ioctl():把参数变成一次受检查的操作
参数准备好后,video_usercopy() 执行:
err = func(file, cmd, parg);在 video_ioctl2() 建立的组合里,这里的 func 就是 __video_do_ioctl()。

图 5:标准分发主干。资源未取得时的失败直接退出;已取得的锁按持有情况释放。
8.1 先取当前节点,不从命令反推设备
函数通过 video_devdata(file) 找到当前 video_device,再取它的 ioctl_ops、调试标志和打开上下文。
这个顺序说明:VIDIOC_QUERYCAP 决定的是“执行什么操作”,file 决定的是“对哪个节点执行”。不同节点收到同一个命令,最终可以调用各自不同的驱动函数。
如果 vfd->ioctl_ops == NULL,标准分发没有可用的操作集合,会告警并返回 -ENOTTY。因此,只把 .unlocked_ioctl 设为 video_ioctl2,却没有配置 ioctl_ops,并不能形成一个完整的标准 ioctl 实现。
8.2 加锁以后,再检查节点是否仍注册
函数先处理可选的 Request 串行化锁,再选择并获取当前 ioctl 使用的锁。取得锁以后,重新检查 video_is_registered(vfd)。
为什么入口层已经检查过,这里还要再查?因为参数复制、内存分配、等待锁都可能消耗时间。进入外层时有效的节点,到真正准备执行操作时,状态可能已经变化。
但这个检查不能单独解决所有热拔插问题:检查之后仍需由驱动的注销与操作路径采用一致的同步方案。节点对象的引用计数保证内存存活,状态位表示是否允许访问,而硬件是否仍可访问,需要驱动正确协调。
8.3 已知命令,还要检查当前节点是否支持
if (v4l2_is_known_ioctl(cmd)) { info = &v4l2_ioctls[_IOC_NR(cmd)];if (!test_bit(_IOC_NR(cmd), vfd->valid_ioctls) && !((info->flags & INFO_FL_CTRL) && vfh && vfh->ctrl_handler))goto done;if (vfh && (info->flags & INFO_FL_PRIO)) { ret = v4l2_prio_check(vfd->prio, vfh->prio);if (ret)goto done; }} else { default_info.ioctl = cmd; default_info.flags = 0; default_info.debug = v4l_print_default; info = &default_info;}这里区分了两件事:
v4l2_is_known_ioctl(cmd) → 框架认识这个标准命令vfd->valid_ioctls 对应位 → 当前节点的注册配置允许这条命令例如,核心能够识别 VIDIOC_S_FMT,并不代表每个节点都实现了合法的设置格式路径。
valid_ioctls 在 v4l2-dev.c 的注册阶段由 determine_valid_ioctls() 计算。它综合考虑操作回调、节点类别、方向、能力和显式屏蔽策略。运行时通常只需要测试对应位,而不必重新扫描全部回调。
这是一种“注册阶段准备,运行阶段快速判断”的设计。相应地,驱动也不应把注册后的操作表随意修改,再期待旧位图自动重新计算。
8.4 控制命令为什么存在一个例外?
核心检查中的例外是:
(info->flags & INFO_FL_CTRL) && vfh && vfh->ctrl_handler节点注册时,还没有所有未来的打开上下文。某次打开可能单独关联一个控制处理器,因此,即使节点位图中没有开放该控制命令,运行时仍可能由文件句柄上的处理器完成它。
这也说明 valid_ioctls 不是适用于所有命令、所有上下文的最终判决。对普通标准命令,位图是重要限制;对这类控制命令,还需要结合当前文件句柄。
8.5 INFO_FL_PRIO 检查的不是 CPU 调度优先级
存在 vfh 且命令带 INFO_FL_PRIO 时,执行:
ret = v4l2_prio_check(vfd->prio, vfh->prio);同一源码树中的检查逻辑是:
intv4l2_prio_check(struct v4l2_prio_state *global, enum v4l2_priority local){return (local < v4l2_prio_max(global)) ? -EBUSY : 0;}它比较的是 V4L2 打开上下文之间的操作优先级,而不是线程的 nice 值,也不是进程调度策略。
例如,某个更高优先级的打开上下文正在使用节点,另一个低优先级上下文执行受保护的设置类命令,就可能在抵达驱动回调之前收到 -EBUSY。
注意条件:**不是所有 ioctl 都做这项检查,也不是不存在标准文件句柄时核心仍能凭空取得一个优先级。
8.6 标准命令与表外命令是两条分支
write_only = _IOC_DIR(cmd) == _IOC_WRITE;if (info != &default_info) { ret = info->func(ops, file, fh, arg);} elseif (!ops->vidioc_default) { ret = -ENOTTY;} else { ret = ops->vidioc_default(file, fh, vfh ? v4l2_prio_check(vfd->prio, vfh->prio) >= 0 : 0, cmd, arg);}已知标准命令进入 info->func。它通常是一个 v4l_* 适配器,用于完成该命令共用的检查和参数整理。
表外命令则尝试 ops->vidioc_default;没有默认回调就返回 -ENOTTY。
这里有两个重要细节。
第一,已知但不支持的标准命令,不会自动掉到 vidioc_default。 它已经在位图检查处退出。默认回调不是补救所有缺失标准回调的兜底入口。
第二,默认回调收到一个优先级检查结果布尔值,但核心没有在这条分支中统一替它拒绝全部低优先级请求。 具体自定义命令需要根据自身语义使用这个值,并完成完整命令值、参数和权限检查。
8.7 回调不是直接从 v4l2_ioctls[] 中拿到驱动地址
对 VIDIOC_QUERYCAP:
v4l2_ioctls[命令编号].func → v4l_querycap → ops->vidioc_querycap对 VIDIOC_S_FMT:
v4l2_ioctls[命令编号].func → v4l_s_fmt → 按参数中的 type 选择具体格式回调中间的适配层让框架能够先处理通用规则,再把差异化行为交给驱动。部分简单命令甚至通过 DEFINE_V4L_STUB_FUNC 宏生成一层直接转发包装;复杂命令则需要明显更多的校验、兼容和结果整理。
9. 锁究竟在哪里:unlocked_ioctl 并不表示“不加锁”
9.1 不要把 videodev_lock 与 ioctl 锁混在一起
上一篇中的 videodev_lock 是入口层管理全局表、编号与部分生命周期交接的锁。它不是本次标准命令执行时统一持有的操作锁。
v4l2_ioctl() 本身也没有自动获取 vdev->lock。这里的选锁与加锁发生在 __video_do_ioctl() 中。**函数成员叫 unlocked_ioctl,不等于驱动可以省掉并发控制。
9.2 v4l2_ioctl_get_lock() 怎样选择锁?
static struct mutex *v4l2_ioctl_get_lock(struct video_device *vdev, struct v4l2_fh *vfh, unsignedint cmd,void *arg){if (_IOC_NR(cmd) >= V4L2_IOCTLS)return vdev->lock;if (vfh && vfh->m2m_ctx && (v4l2_ioctls[_IOC_NR(cmd)].flags & INFO_FL_QUEUE)) {if (vfh->m2m_ctx->q_lock)return vfh->m2m_ctx->q_lock; }if (vdev->queue && vdev->queue->lock && (v4l2_ioctls[_IOC_NR(cmd)].flags & INFO_FL_QUEUE))return vdev->queue->lock;return vdev->lock;}对于正常的已知标准命令,可以这样理解优先顺序:
q_lock 非空 | vfh->m2m_ctx->q_lock |
queue->lock 非空 | vdev->queue->lock |
vdev->lock | |
NULL |
这里选择的是一把 ioctl 锁,不是每次都依次把 M2M 锁、队列锁、节点锁全部锁上。
一个容易忽略的细节是:这个选锁函数按 _IOC_NR(cmd) 查看候选条目的队列标志,没有在这里执行完整命令匹配。它的职责是选锁,不能把它当作命令合法性校验;完整匹配发生在后面的分发判断中。
9.3 为什么队列命令常用另一把锁?
假设一个设置类命令需要较长时间,而 DQBUF 只是从完成队列取出已到达的帧。如果它们使用同一把大锁,设置操作就可能拖住缓冲区周转。
把队列操作交给 queue->lock,能够减少这类无关等待。但分锁也带来新的责任:格式、队列状态、控制参数之间若有共享约束,驱动仍需保证访问规则一致,不能因为“框架支持两把锁”就认为任意操作都可以并行
9.4 Request 锁为什么先于 ioctl 锁?
当设备支持 Media Request,且命令是 VIDIOC_STREAMON 或 VIDIOC_STREAMOFF 时,源码会先获取:
&vfd->v4l2_dev->mdev->req_queue_mutex目的是让流启动、停止与新的 Request 入队串行化。随后再取得本次 ioctl 的锁,结束时按相反顺序释放。
这个 Request 指的是媒体请求对象机制,不是把所有普通 QBUF 都叫作“请求”。不满足上述条件时,本次调用不会因为经过标准 ioctl 层就自动拿到 Request 锁
9.5 加锁失败怎么退出?
两次加锁采用 mutex_lock_interruptible()。等待被中断时,内核路径返回 -ERESTARTSYS。如果 Request 锁已经成功获取,而后续 ioctl 锁获取失败,就先释放前者再退出。
-ERESTARTSYS 是内核中的系统调用重启语义,不应该直接当成应用必然看到的 errno 数字。应用侧的表现需要结合信号处理和重启规则。
9.6 参数复制并不在这把锁里面

图 6:参数编组与结果回写处于 ioctl 串行化锁之外;阻塞等待还需要正确的锁协作。
正常过程是:
输入复制、结构转换、数组准备 → 获取锁 → 状态与命令检查 → 命令适配器与驱动回调 → 释放锁 → 结果回写、临时内存释放这个结构说明两件事。
首先,串行化锁保护的是当前命令的状态检查与执行阶段,不是从应用进入系统调用到最终返回的全部时间。
其次,一次 S_FMT 返回给应用后,再发起 REQBUFS,两者之间仍可能有其他线程执行操作。**单次 ioctl 的加锁,不构成跨多个系统调用的原子事务。
9.7 阻塞 DQBUF 会一直占着队列锁吗?
不应该把“调用时持锁”理解为“睡眠等待期间永远持锁”。同一源码树的 VB2 等待路径在合适的位置调用 wait_prepare 与 wait_finish,由驱动配置的回调完成等待前解锁、醒来后重新加锁。
使用标准辅助函数时,对应实现为:
voidvb2_ops_wait_prepare(struct vb2_queue *vq){ mutex_unlock(vq->lock);}voidvb2_ops_wait_finish(struct vb2_queue *vq){ mutex_lock(vq->lock);}使用这两个辅助函数,不仅要求 vq->lock 非空,还要求它正是当前线程在进入等待前已经持有、并且需要暂时释放的那把互斥锁。辅助函数不会重新调用 v4l2_ioctl_get_lock(),也不会自动发现“实际取得了另一把锁”。
例如,若外层实际取得的是 vdev->lock,而 vq->lock 为空或指向另一把锁,就不能机械地挂接这两个辅助函数。M2M 路径也必须核对上下文的 q_lock 与队列等待回调所操作锁的关系。需要释放其他锁时,应实现与实际加锁约定配套的等待回调。
VIMC 把节点锁和队列锁都关联到捕获对象的互斥锁,并配置了上述等待回调,因而这一组调用能够配对。等待回调操作的互斥锁,也不是 VB2 完成队列所使用的 done_lock 自旋锁。
这样,等待图像的线程可以暂时让出操作锁,让其他操作有机会推进队列状态或停止等待。醒来重新加锁后,VB2 还会再次检查队列条件;被唤醒本身并不保证一定有可取出的帧。
10. 完整实例一:VIDIOC_QUERYCAP 如何把设备信息交回来?

图 7:QUERYCAP 的正常返回路径;能力字段由核心与驱动按职责共同构造。
10.1 第一步:建立一个清零的内核能力结构
VIDIOC_QUERYCAP 是 _IOR 命令,主结构体不需要从应用提供查询条件。因此 video_get_user() 先把内核参数区域清零。
这意味着:应用中 cap 的初值不是驱动要读取的查询输入。不过,把应用侧结构初始化为零仍然是清晰、稳健的编码习惯;对于其他同时包含输入字段的命令,更需要按接口要求初始化。
10.2 第二步:命令描述指向 v4l_querycap()
标准表中保存:
IOCTL_INFO(VIDIOC_QUERYCAP, v4l_querycap, v4l_print_querycap, 0),本条没有设置 INFO_FL_PRIO、INFO_FL_QUEUE 或 INFO_FL_ALWAYS_COPY。它仍然会经过通用的节点状态和命令能力检查,并且在 vdev->lock 非空时使用相应锁;“没有特殊标志”不等于完全绕过通用处理。
10.3 第三步:核心先填公共字段,再让驱动补充
staticintv4l_querycap(const struct v4l2_ioctl_ops *ops, struct file *file, void *fh, void *arg){structv4l2_capability *cap = (structv4l2_capability *)arg;structvideo_device *vfd = video_devdata(file);int ret; cap->version = LINUX_VERSION_CODE; cap->device_caps = vfd->device_caps; cap->capabilities = vfd->device_caps | V4L2_CAP_DEVICE_CAPS; media_set_bus_info(cap->bus_info, sizeof(cap->bus_info), vfd->dev_parent); ret = ops->vidioc_querycap(file, fh, cap);/* * Drivers must not change device_caps, so check for this and * warn if this happened. */ WARN_ON(cap->device_caps != vfd->device_caps);/* * Check that capabilities is a superset of * vfd->device_caps | V4L2_CAP_DEVICE_CAPS */ WARN_ON((cap->capabilities & (vfd->device_caps | V4L2_CAP_DEVICE_CAPS)) != (vfd->device_caps | V4L2_CAP_DEVICE_CAPS)); cap->capabilities |= V4L2_CAP_EXT_PIX_FORMAT; cap->device_caps |= V4L2_CAP_EXT_PIX_FORMAT;return ret;}可以把这个函数分成三个时段:
version、device_caps、capabilities,尝试准备 bus_info | ||
ops->vidioc_querycap(file, fh, cap) | ||
V4L2_CAP_EXT_PIX_FORMAT |
这里的 version 取自编译时的 LINUX_VERSION_CODE,不是芯片固件版本。
capabilities 与 device_caps 也有区别:前者表达设备能力集合,后者表达当前打开节点的能力;应用在看到 V4L2_CAP_DEVICE_CAPS 时,应按接口约定使用 device_caps 判断当前节点。
10.4 WARN_ON 是告警,不是自动修复
函数检查驱动是否改动了 device_caps,以及 capabilities 是否仍包含要求的能力集合。
但这些 WARN_ON 不会把错误字段自动恢复成原值,也没有把驱动的返回码统一改成失败。源码最后仍然返回 ret。因此,不能把“核心有检查”写成“核心已经替错误驱动纠正了能力信息”。[^querycap]
随后补充 V4L2_CAP_EXT_PIX_FORMAT 是框架的接口能力声明,不表示硬件支持所有像素格式;具体格式仍应通过相应枚举和格式协商接口确认。
10.5 接到真实回调:VIMC 怎样填写?
staticintvimc_capture_querycap(struct file *file, void *priv, struct v4l2_capability *cap){ strscpy(cap->driver, VIMC_PDEV_NAME, sizeof(cap->driver)); strscpy(cap->card, KBUILD_MODNAME, sizeof(cap->card));snprintf(cap->bus_info, sizeof(cap->bus_info),"platform:%s", VIMC_PDEV_NAME);return0;}这个例子很适合观察职责分工:驱动只填写它需要补充的信息,不必在每个驱动里重复核心已经完成的公共赋值。
完整链条是:
video_ioctl2 → video_usercopy:准备 cap → __video_do_ioctl:检查与选路 → v4l_querycap:公共字段 → vimc_capture_querycap:设备描述 → v4l_querycap:能力校验与补充 → __video_do_ioctl:解锁 → video_usercopy:video_put_user 回写 capvideo_put_user() 成功后,应用才能从自己的 cap 对象中读取最终结果。驱动改的是内核副本,不是直接拿着应用地址填写。
11. 完整实例二:VIDIOC_S_FMT 为什么会选中不同的回调?
11.1 先分清三个“类型”
一次格式请求里至少会遇到三个不同层次的标识:
_IOC_TYPE(cmd) == 'V' | ||
vdev->vfl_type == VFL_TYPE_VIDEO | ||
p->type == V4L2_BUF_TYPE_VIDEO_CAPTURE |
不能用其中一个代替另一个。
VIDIOC_S_FMT 本身只有一个标准命令,但 struct v4l2_format 中的 type 会决定如何解释 fmt 联合体,进而选择单平面、多平面、输出、元数据等不同的回调分支。
11.2 框架并不是直接调用 vidioc_s_fmt_vid_cap

图 8:S_FMT 的回调分支由参数 type 与回调配置决定;前置步骤失败就返回对应错误。成功路径才返回协商结果,失败路径按外层回写规则退出,且 S_FMT 不等同于启动图像流。
v4l_s_fmt() 的主要前置处理为:
structv4l2_format *p = arg;structvideo_device *vfd = video_devdata(file);int ret = check_fmt(file, p->type);unsignedint i;if (ret)return ret;ret = v4l_enable_media_source(vfd);if (ret)return ret;v4l_sanitize_format(p);执行顺序是:先检查格式类型,再处理媒体源启用逻辑,然后规范化参数,最后按 p->type 选具体回调。
其中,v4l_enable_media_source() 在适用的媒体设备上调用相应源启用钩子;没有关联媒体设备或没有该钩子时可以不执行实质动作。它不是所有系统统一启动 Sensor 或 DMA 的命令,也不等同于 STREAMON
11.3 check_fmt() 检查的不只是枚举值范围
该函数综合观察节点类型、device_caps、方向与格式回调。对于输入节点,需要相应采集方向成立;对于输出节点,则需要相应输出方向成立。
一个容易漏读的细节是:它用相应的 vidioc_g_fmt_* 回调是否存在,参与判断格式类型是否受支持,而不是只查看本次要调用的 s_fmt。 随后 v4l_s_fmt() 再检查本次分支需要的设置回调。
因此,只有 vidioc_s_fmt_vid_cap 却缺少合法的获取格式路径,并不能据此认为格式接口已经配置完整。
更细一点,单平面 capture 类型在 check_fmt() 中可能因为存在多平面 G_FMT 回调而先通过一层检查;但后面的具体单平面 S_FMT 分支仍要求单平面设置回调。这说明通过前置共性检查,不代表所有后续分支都必然成功。
11.4 参数“规范化”与硬件能力校验有什么区别?
v4l_sanitize_format() 的职责主要包括:限制多平面的 num_planes 上界;处理单平面扩展字段的 priv 标记;把不合法的颜色空间、编码、量化和传递函数值替换成默认值。
这些是接口层的共性整理。它没有一张所有摄像头硬件的分辨率支持表,也不会替具体设备判断任意尺寸能否被实现。
因此应区分:
框架规范化 → 参数布局、扩展字段和通用枚举语义可被后续安全解释驱动校验 / 调整 → 当前设备究竟接受哪些尺寸、格式、对齐与资源状态把多平面 num_planes 限制到 VIDEO_MAX_PLANES,只是避免不合理的上界;实际应有几个平面、每个平面多大,还需要后续实现决定。
11.5 单平面与多平面分支的关键差异
case V4L2_BUF_TYPE_VIDEO_CAPTURE:if (unlikely(!ops->vidioc_s_fmt_vid_cap))break; CLEAR_AFTER_FIELD(p, fmt.pix); ret = ops->vidioc_s_fmt_vid_cap(file, fh, arg);/* just in case the driver zeroed it again */ p->fmt.pix.priv = V4L2_PIX_FMT_PRIV_MAGIC;if (vfd->vfl_type == VFL_TYPE_TOUCH) v4l_pix_format_touch(&p->fmt.pix);return ret;case V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE:if (unlikely(!ops->vidioc_s_fmt_vid_cap_mplane))break; CLEAR_AFTER_FIELD(p, fmt.pix_mp.xfer_func);for (i = 0; i < p->fmt.pix_mp.num_planes; i++) CLEAR_AFTER_FIELD(&p->fmt.pix_mp.plane_fmt[i], bytesperline);return ops->vidioc_s_fmt_vid_cap_mplane(file, fh, arg);单平面分支处理 fmt.pix;多平面分支处理 fmt.pix_mp,并清理其中相关保留区域。
这里的“多平面 API”描述的是缓冲区接口形式,不等于“图像一定只有 YUV 才能用”,也不能仅凭像素格式名称猜测需要填哪一个联合体分支。应用应先确认节点实际支持的缓冲区类型,再使用对应参数布局。
11.6 VIMC 的设置流程:先检查资源,再调整,再提交
同一源码树中的 vimc_capture_s_fmt_vid_cap() 先调用 vb2_is_busy(&vcapture->queue),繁忙时返回 -EBUSY;随后调用其 try_fmt 实现整理格式,最后把结果写入 vcapture->format。
其 try_fmt 会限制宽高范围与偶数对齐,处理不支持的像素格式,并计算 bytesperline、sizeimage。
这条路径说明:S_FMT 返回成功表示驱动接受了回写结果所描述的配置,不是保证应用最初提交的每个字段都原样保留。接口中可能出现调整,应用必须读取返回结果
队列繁忙也不能只理解成“已经执行 STREAMON”。在用于接口核对的头文件中,vb2_is_busy() 判断的是 q->num_buffers > 0;这和 q->streaming 不是同一个字段。具体判定应看驱动使用的资源检查,而不是只看是否正在出图。
11.7 G_FMT、TRY_FMT、S_FMT 怎样形成闭环?
G_FMT | v4l_g_fmt() | |
TRY_FMT | v4l_try_fmt() | |
S_FMT | v4l_s_fmt() |
TRY_FMT 与 S_FMT 可以复用驱动内部的调整逻辑,但不能因此变成同一行为:前者不应提交设备当前状态,后者才执行设置。
格式协商也不是启动图像传输。典型采集仍要继续建立缓冲区、入队并启动流;不能因为 S_FMT 成功,就去寻找“为什么帧还没有回来”。
12. 走到缓冲区入口:QBUF 怎样接入 VB2?
12.1 v4l2_ioctl_ops 将标准命令连接到不同实现
同一张操作表可以同时保存驱动自有函数和通用辅助函数。VIMC 中的相关条目是:
staticconststructv4l2_ioctl_opsvimc_capture_ioctl_ops = { .vidioc_querycap = vimc_capture_querycap, .vidioc_g_fmt_vid_cap = vimc_capture_g_fmt_vid_cap, .vidioc_s_fmt_vid_cap = vimc_capture_s_fmt_vid_cap, .vidioc_try_fmt_vid_cap = vimc_capture_try_fmt_vid_cap, .vidioc_enum_fmt_vid_cap = vimc_capture_enum_fmt_vid_cap, .vidioc_enum_framesizes = vimc_capture_enum_framesizes, .vidioc_reqbufs = vb2_ioctl_reqbufs, .vidioc_create_bufs = vb2_ioctl_create_bufs, .vidioc_prepare_buf = vb2_ioctl_prepare_buf, .vidioc_querybuf = vb2_ioctl_querybuf, .vidioc_qbuf = vb2_ioctl_qbuf, .vidioc_dqbuf = vb2_ioctl_dqbuf, .vidioc_expbuf = vb2_ioctl_expbuf, .vidioc_streamon = vb2_ioctl_streamon, .vidioc_streamoff = vb2_ioctl_streamoff,};按照职责归类:
vidioc_querycapvidioc_enum_fmt_vid_cap | ||
vidioc_g_fmt_vid_capvidioc_try_fmt_vid_cap、vidioc_s_fmt_vid_cap | ||
vidioc_reqbufsvidioc_create_bufs、vidioc_querybuf | ||
vidioc_qbufvidioc_dqbuf | ||
vidioc_streamonvidioc_streamoff |
这种组织方式把协议层的命令名与实现函数解耦。同名标准命令可以由不同设备实现;不同驱动也可以把相同成员指向 VB2 通用函数。**函数表的赋值才是连接成立的证据,不能仅凭函数名字推断自动调用关系。
12.2 v4l_qbuf() 只做入口适配
staticintv4l_qbuf(const struct v4l2_ioctl_ops *ops, struct file *file, void *fh, void *arg){structv4l2_buffer *p = arg;int ret = check_fmt(file, p->type);return ret ? ret : ops->vidioc_qbuf(file, fh, p);}它先检查缓冲区类型,再调用节点提供的 vidioc_qbuf。这里看不到图像像素复制,也没有直接操作 DMA 寄存器。
当驱动把 vidioc_qbuf 接到 vb2_ioctl_qbuf 时,才继续进入另一层:

图 9:驱动操作表显式连接 VB2;QBUF 的入口适配不是硬件图像搬运。
12.3 队列归属检查在哪里发生?
intvb2_ioctl_qbuf(struct file *file, void *priv, struct v4l2_buffer *p){structvideo_device *vdev = video_devdata(file);if (vb2_queue_is_busy(vdev->queue, file))return -EBUSY;return vb2_qbuf(vdev->queue, vdev->v4l2_dev->mdev, p);}vb2_ioctl_qbuf() 先检查当前文件是否可以操作这个队列,再调用 vb2_qbuf()。因此,QBUF 返回 -EBUSY 也可能来自队列归属,而不是来自 V4L2 优先级检查。
这是两种不同机制:
V4L2 优先级 → 当前打开上下文能否执行受优先级保护的操作VB2 队列归属 → 当前文件是否有权操作已经被某个上下文占用的队列INFO_FL_QUEUE 则主要参与选锁,不会因为设置了这个标志就自动完成上述全部检查。
12.4 DQBUF 的阻塞模式又由哪里传下去?
intvb2_ioctl_dqbuf(struct file *file, void *priv, struct v4l2_buffer *p){structvideo_device *vdev = video_devdata(file);if (vb2_queue_is_busy(vdev->queue, file))return -EBUSY;return vb2_dqbuf(vdev->queue, p, file->f_flags & O_NONBLOCK);}可以看到,O_NONBLOCK 从 file->f_flags 传入 VB2。它不是由 v4l_dqbuf() 单独写出一套等待队列逻辑。
后续 VB2 的等待路径会检查是否正在流传输、队列是否出错、是否已有完成缓冲区,以及当前是否非阻塞。非阻塞且没有完成缓冲区时,可以返回 -EAGAIN;阻塞等待则会结合前面提到的等待回调处理锁。
至此,v4l2-ioctl.c 与后续缓冲区专题的边界已经清楚:本文件负责标准命令的通用入口与适配,VB2 继续处理缓冲区状态、归属、等待与完成。 不能把“进入 QBUF 回调”直接等同于“硬件已经开始写入这块内存”。
13. 返回路径同样重要:驱动返回之后还没结束
13.1 video_usercopy() 后半段的实际顺序
err = func(file, cmd, parg);if (err == -ENOTTY || err == -ENOIOCTLCMD) { err = -ENOTTY;goto out; }if (err == 0) {if (cmd == VIDIOC_DQBUF) trace_v4l2_dqbuf(video_devdata(file)->minor, parg);elseif (cmd == VIDIOC_QBUF) trace_v4l2_qbuf(video_devdata(file)->minor, parg); }/* * Some ioctls can return an error, but still have valid * results that must be returned. */if (err < 0 && !always_copy)goto out;if (has_array_args) { *kernel_ptr = (void __force *)user_ptr;if (in_compat_syscall()) {int put_err; put_err = v4l2_compat_put_array_args(file, user_ptr, array_buf, array_size, orig_cmd, parg);if (put_err) err = put_err; } elseif (copy_to_user(user_ptr, array_buf, array_size)) { err = -EFAULT; } }if (video_put_user((void __user *)arg, parg, cmd, orig_cmd)) err = -EFAULT;out: kvfree(array_buf); kfree(mbuf);return err;这段代码必须按先后顺序理解,尤其不能把 always_copy 简化成“无论发生什么错误都回写”。
13.2 不支持命令的错误会先被统一处理
if (err == -ENOTTY || err == -ENOIOCTLCMD) { err = -ENOTTY;goto out;}这一步在 always_copy 判定之前。遇到这两类返回值,直接进入资源清理,不继续执行后面的结果回写。
因此,即使某类命令有 INFO_FL_ALWAYS_COPY,也不能据此推导“不支持命令时仍会得到正常参数回传”。
13.3 普通错误与允许携带结果的错误
接下来的关键判断是:
if (err < 0 && !always_copy)goto out;可以分成以下几种情况:
always_copy == false | |
always_copy == true | |
扩展控制操作是一个典型场景:操作失败时,error_idx 等字段仍可能有诊断意义,所以不能简单丢弃全部输出。EDID 等命令也会使用这一策略。
这份实现还存在一个值得单独核对的执行顺序细节。video_get_user() 先处理不含 _IOC_WRITE 的命令,清零后直接返回;只有继续向下,才读取命令表并设置 always_copy。
按照标准命令定义,VIDIOC_QUERY_DV_TIMINGS 属于 _IOR,而它在本文件中的表项带有 INFO_FL_ALWAYS_COPY。把两个条件合在一起推演:
always_copy 初值为 false ↓QUERY_DV_TIMINGS 没有输入方向 ↓video_get_user 清零后提前返回,未读取 ALWAYS_COPY 标志 ↓若命令处理器返回负错误 ↓外层按 !always_copy 提前退出,不继续回写因此,不能仅看到表项中的 INFO_FL_ALWAYS_COPY 就断言实际错误路径一定回写。上面是对本文函数顺序与标准命令定义组合得到的静态路径分析,不是对所有版本实现的结论,也不应理解为应用接口不需要返回这些诊断信息。时序查询接口本身允许在 ERANGE 等情况下提供已检测到的信息,分析时必须把接口约定与当前实现的实际控制流分开。
13.4 先恢复数组地址,再回写描述
如果本次命令处理了嵌套数组,先将主结构体中的指针字段恢复成原始应用地址,再将数组内容写回,最后调用 video_put_user() 回写主结构体。
主结构体回写首先统一检查原始命令是否包含 _IOC_READ:这个方向条件同时适用于本机 ABI 和兼容 ABI。没有输出方向就直接返回,不回写主结构体。
需要回写时,若 cmd == real_cmd,就按原始命令大小直接 copy_to_user(),兼容进程也可能走此分支;只有命令不同且 in_compat_syscall() 成立时,才调用 v4l2_compat_put_user()。特定原生 32 位配置下的旧时间布局另外转换。
这里的数组回写与主结构体回写是分开的。某一步失败,不代表其他步骤一定完全没有产生可见效果;不能把两次复制理解为不可分割的事务。
13.5 为什么驱动成功了,应用却得到 EFAULT?
可能发生这样的时序:
应用提交参数地址 ↓输入复制成功 ↓驱动已执行设置,返回 0 ↓结果回写时,目标地址不可写或已失效 ↓video_put_user() 失败 ↓最终返回 -EFAULT此时,驱动的动作可能已经发生。本层不会因为结果复制失败就自动执行反向操作,把硬件或队列恢复到调用前。
这是一条非常实用的排障结论:最终 errno 描述的是整次系统调用的结果,不一定等于驱动回调当时返回的状态。 对会改变状态的命令,不能只因收到错误就盲目重复提交,还需要确认当前设备或队列状态。
13.6 调试日志与 trace 也有自己的时间位置
__video_do_ioctl() 的调试输出发生在结果回写之前。video_usercopy() 里的 trace_v4l2_qbuf()、trace_v4l2_dqbuf(),也在后续参数回写之前,并且只在该阶段 err == 0 时触发。
所以,看到一次成功的 QBUF/DQBUF 核心跟踪事件,并不能单独证明应用最后成功收到了回写结果。反过来,应用看到 EFAULT,也不能据此认定前面从未执行过回调。定位时应把“回调返回”“核心跟踪”“最终返回”三个时间点分开。
13.7 资源释放必须闭合
统一出口执行:
kvfree(array_buf);kfree(mbuf);return err;栈上的 sbuf 随函数返回而结束生命周期,不需要手工释放。通过 kvmalloc() 建立的数组使用 kvfree() 释放,主结构体的 kmalloc() 分配对应 kfree()。
这也是驱动不能保存这些临时地址供异步访问的另一个直接证据:无论最终成功还是大多数失败退出路径,外层都会清理它们。
14. 不是每条标准命令都直达一个同名驱动回调
14.1 控制命令:优先选择控制处理器
以 v4l_s_ctrl() 为例,其前半段的优先顺序是:
文件句柄上的 ctrl_handler ↓ 不存在节点上的 ctrl_handler ↓ 不存在传统 vidioc_s_ctrl ↓ 不存在尝试兼容到 vidioc_s_ext_ctrls ↓ 也不存在返回不支持这解释了为什么某个驱动没有直接提供 vidioc_s_ctrl,控制设置却仍可以工作:它可能通过 v4l2_ctrl_handler 接入统一控制框架。
同样,不能因为没找到一个名叫 vidioc_s_exposure 的成员就认为曝光无法设置。控制 ID、控制对象和具体控制回调的关联属于控制框架,标准 ioctl 层只负责把请求送到适当入口。
14.2 事件出队:核心可直接调用事件框架
staticintv4l_dqevent(const struct v4l2_ioctl_ops *ops, struct file *file, void *fh, void *arg){return v4l2_event_dequeue(fh, arg, file->f_flags & O_NONBLOCK);}VIDIOC_DQEVENT 的执行入口直接连接 v4l2_event_dequeue(),而不是先寻找一个 vidioc_dqevent 驱动回调。
这也再次区分了两条不同路径:DQEVENT 处理事件队列,DQBUF 处理图像或其他流数据缓冲区的完成结果。它们都可能等待,但不是同一个队列,也没有相同的所有权语义。
14.3 部分传统接口通过其他接口适配
裁剪与 selection 相关的包装函数中存在参数转换与兼容处理。这类函数的意义不只是“转发名字”,而是保持已有应用接口与下层较统一实现之间的协作。
因此,深读一个命令时,不应只搜索有没有完全同名的驱动函数。应从命令表中的 func 开始,继续查看这个适配器究竟做了直接转发、回退、参数变换,还是由核心完成。
15. 32 位兼容:统一参数入口并不意味着 ABI 天然一致
15.1 同一个结构名,内存布局可能不同
兼容 ABI 涉及指针宽度、结构布局和时间字段表示等差异。命令编码本身又可能包含结构体大小,因此原始命令值和内核用于处理的标准命令值并不总是相同。
这就是外层同时保存 orig_cmd 与 cmd 的原因:前者用于按原接口解释和返回参数,后者用于标准命令分发。
15.2 这份实现怎样接回标准入口?
在相应兼容配置下,v4l2_compat_ioctl32() 先按命令组和编号范围选择入口:
_IOC_TYPE(cmd) == 'V' 且 _IOC_NR(cmd) < BASE_VIDIOC_PRIVATE → 将 arg 通过 compat_ptr() 转换 → file->f_op->unlocked_ioctl → 核心 v4l2_ioctl → 驱动设置的 unlocked_ioctl(本篇为 video_ioctl2)不满足上述条件 → 存在 vdev->fops->compat_ioctl32 时交给它 → 否则保留不支持的返回结果这一层并没有调用 v4l2_is_known_ioctl()。 因此,“不在标准命令表中”与“交给驱动的兼容回调”不是等价条件。例如,命令组为 'V'、编号仍在上述范围内,但大小编码与标准条目不匹配的命令,依然先进入共同入口;后续是否交给 vidioc_default,由标准分发层另行判断。
接入 video_ioctl2() 后,主结构体按命令是否需要翻译选择直接复制或兼容转换;数组则按数组兼容路径处理。compat_ptr() 只转换参数地址的表示,不会自动转换地址所指结构体的布局,也不会递归处理所有内部指针。
不能假定一个含有指针的自定义 ioctl,在 32 位应用和 64 位内核之间只做一次指针强制转换就能正确工作。
15.3 不要混用其他版本的兼容调用图
有些实现将更多兼容工作集中在专门的转换文件中,而本文分析的实现已在 video_usercopy() 内部显式配合原始命令与标准命令。
判断调用关系时应以当前函数体为准:看到外层保留 orig_cmd,继续检查命令翻译、主结构体转换与数组转换,而不是根据某张旧流程图,把这些步骤凭空删除。本文只说明已出现的协作边界,不把其他版本的内部布局代入当前实现。
16. 错误码怎么定位:先看在哪一层被产生
-ENOTTY | ioctl_ops、命令未开放、默认回调缺失,或下层不支持 | |
-EINVAL | type | |
-EFAULT | ||
-ENOMEM | ||
-EBUSY | ||
-ENODEV | ||
-EAGAIN | ||
-ERESTARTSYS |
表中列的是典型位置,不是每个错误码唯一可能的来源。下层驱动也可以在接口允许的范围内返回这些错误。
16.1 一个有效的定位顺序
第一步:是否进入 v4l2_ioctl? 没有 → 检查文件对象和入口路径第二步:是否进入 video_ioctl2? 没有 → 检查驱动 unlocked_ioctl 的连接第三步:是否调用 __video_do_ioctl? 没有 → 参数、ABI、临时分配或数组准备阶段已失败第四步:本次属于哪一种命令分支? 已知标准命令 → 跟踪 info->func 没有执行 → 核对锁、节点状态、位图与优先级检查 表外命令 → 跟踪 vidioc_default 没有执行 info->func 本身不代表失败 默认回调缺失或拒绝命令时,才按实际返回点排查第五步:是否到达该命令实际需要的实现? 预期驱动回调未执行 → 检查适配器是否拒绝参数或选择了其他分支 框架直接完成的命令 → 没有同名 vidioc_* 回调也可能正常成功第六步:实现层返回值与应用结果是否一致? 不一致 → 继续看适配器返回后处理、数组和主结构体回写这条顺序比在驱动末端不停加打印更有效。它能先确定失败属于“尚未分发”“适配器拒绝”“下层失败”还是“回传失败”,再缩小到具体函数。
16.2 几个值得主动排查的误区
“ioctl_ops 里有回调,所以命令必然有效。” 还需要确认注册期位图、节点类别与方向,以及命令适配器的参数检查。
“回调里直接 copy_from_user() 才安全。” 对本文的标准有方向命令,传给回调的主参数已经是内核对象;再次把它当成应用地址处理,反而破坏约定。内部尚未处理的载荷指针应按各自接口单独判断,不能一概而论。
“加了 vdev->lock,所有文件操作就都安全。” 本文分析的是标准 ioctl 分发的选锁范围,不能顺便覆盖自定义入口、读写、映射和驱动异步路径。
“有成功日志,就证明应用拿到了结果。” 日志可能早于回写,最后还可能失败。
“临时结构体地址在 ioctl 返回以后还能给工作队列用。” 参数编组对象会被释放,应复制需要的状态或建立正确的持有关系。
17. 验证练习:让查询与格式试算经过这条链路
17.1 使用一个不启动采集的探测程序
下面的程序只执行能力查询、读取当前格式和格式试算,不执行 S_FMT、REQBUFS 或 STREAMON。它适合先验证标准分发的入口、格式类型和返回结果。
程序按节点能力选择单平面或多平面采集类型,保留当前像素格式,试算 1280 × 720,再读取当前格式观察是否发生变化。TRY_FMT 按接口约定不应提交当前配置;最终结果仍取决于驱动是否正确实现这一语义。
#define _POSIX_C_SOURCE 200809L#include<errno.h>#include<fcntl.h>#include<inttypes.h>#include<stdint.h>#include<stdio.h>#include<string.h>#include<sys/ioctl.h>#include<time.h>#include<unistd.h>#include<linux/videodev2.h>/* 只重试被信号中断的调用,不自动重试其他错误。 */staticintxioctl(int fd, unsignedlong cmd, void *arg){int ret;do { ret = ioctl(fd, cmd, arg); } while (ret < 0 && errno == EINTR);return ret;}staticvoidshow_command(constchar *name, unsignedlong cmd){printf("%-18s cmd=%#lx nr=%lu dir=%lu size=%lu\n", name, cmd, (unsignedlong)_IOC_NR(cmd), (unsignedlong)_IOC_DIR(cmd), (unsignedlong)_IOC_SIZE(cmd));}staticvoidshow_format(constchar *label, const struct v4l2_format *fmt){uint32_t width, height, fourcc;if (fmt->type == V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE) { width = fmt->fmt.pix_mp.width; height = fmt->fmt.pix_mp.height; fourcc = fmt->fmt.pix_mp.pixelformat; } else { width = fmt->fmt.pix.width; height = fmt->fmt.pix.height; fourcc = fmt->fmt.pix.pixelformat; }printf("%s: type=%u, %ux%u, fourcc=%c%c%c%c (%#x)\n", label, fmt->type, width, height, (char)(fourcc & 0xff), (char)((fourcc >> 8) & 0xff), (char)((fourcc >> 16) & 0xff), (char)((fourcc >> 24) & 0xff), fourcc);}intmain(int argc, char **argv){constchar *path = argc > 1 ? argv[1] : "/dev/video0";structv4l2_capabilitycap = {0};structv4l2_formatcurrent = {0}, trial, after = {0};uint32_t caps;int status = 1;int fd;if (argc > 2) {fprintf(stderr, "Usage: %s [/dev/videoX]\n", argv[0]);return2; } show_command("VIDIOC_QUERYCAP", VIDIOC_QUERYCAP); show_command("VIDIOC_G_FMT", VIDIOC_G_FMT); show_command("VIDIOC_TRY_FMT", VIDIOC_TRY_FMT); fd = open(path, O_RDWR | O_NONBLOCK | O_CLOEXEC);if (fd < 0) {fprintf(stderr, "open %s: %s\n", path, strerror(errno));return1; }if (xioctl(fd, VIDIOC_QUERYCAP, &cap) < 0) { perror("VIDIOC_QUERYCAP");goto out; } caps = (cap.capabilities & V4L2_CAP_DEVICE_CAPS) ? cap.device_caps : cap.capabilities;printf("driver=%.*s card=%.*s caps=%#x\n", (int)sizeof(cap.driver), (constchar *)cap.driver, (int)sizeof(cap.card), (constchar *)cap.card, caps);if (caps & V4L2_CAP_VIDEO_CAPTURE_MPLANE) current.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;elseif (caps & V4L2_CAP_VIDEO_CAPTURE) current.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;else {fprintf(stderr, "This node is not a supported capture type.\n");goto out; }if (xioctl(fd, VIDIOC_G_FMT, ¤t) < 0) { perror("VIDIOC_G_FMT");goto out; } show_format("current", ¤t); trial = current; /* 保留当前像素格式,只修改试算尺寸。 */if (trial.type == V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE) { trial.fmt.pix_mp.width = 1280; trial.fmt.pix_mp.height = 720; } else { trial.fmt.pix.width = 1280; trial.fmt.pix.height = 720; }if (xioctl(fd, VIDIOC_TRY_FMT, &trial) < 0) { perror("VIDIOC_TRY_FMT");goto out; } show_format("trial", &trial); after.type = current.type;if (xioctl(fd, VIDIOC_G_FMT, &after) < 0) { perror("VIDIOC_G_FMT after TRY_FMT");goto out; } show_format("current-after-try", &after); status = 0;out:if (close(fd) < 0) { perror("close"); status = 1; }return status;}编译与运行:
cc -std=c11 -Wall -Wextra -Werror -O2 ioctl_probe.c -o ioctl_probe./ioctl_probe /dev/video0输出由实际设备决定,不预设固定的驱动名称、像素格式或分辨率。未实现试算接口的节点可能返回错误;这时应结合入口路径判断是命令未开放,还是具体参数被拒绝。
示例经过 C11 编译检查;未在目标视频硬件上验证实际运行结果。
17.2 用核心调试开关观察参数与返回值
v4l2-dev.c 为节点提供 dev_debug 属性;标准分发根据该位图决定是否打印命令名和参数。对于这里的查询与格式命令,掩码 3 可以打开命令与参数相关输出。QBUF/DQBUF 的频繁日志还受 streaming 调试位控制,不宜在长时间采集中无节制开启。[^dev][^dispatch][^locking]
在一个终端观察内核日志:
sudo dmesg -w在另一个终端短时间开启调试,运行后恢复原值:
DBG=/sys/class/video4linux/video0/dev_debugold=$(cat "$DBG") || exit 1trap'printf "%s\n" "$old" | sudo tee "$DBG" >/dev/null' EXITprintf'3\n' | sudo tee "$DBG" >/dev/null./ioctl_probe /dev/video0先确认节点名称与目标设备一致,并避免在正在使用的生产链路上随意改变调试状态。这里的日志用于帮助定位适配器执行与返回,并不代表记录了全部系统调用阶段。
17.3 用三个问题检验是否真正读通
问题一:VIDIOC_S_FMT 返回 EBUSY,是否说明驱动已经开始改格式?
不一定。它可能在 V4L2 优先级检查中就退出,也可能在媒体源启用、队列资源检查或具体驱动中被拒绝。先确认是否到达了设置回调。
问题二:多平面 QBUF 已经复制了主结构体,为什么还要复制 planes[]?
因为外层复制只复制了指针数值,平面描述数组仍在另一个地址中。数组副本与指针替换解决的是这一级描述对象的地址空间边界,不是图像像素搬运。
问题三:驱动回调打印返回 0,应用却得到 EFAULT,是否矛盾?
不矛盾。回调之后还有参数回写,后者可以失败;本层也不会自动撤销已经执行的命令。
18. 回看整个文件:不同函数群分别承担什么职责?
打通主线后,再回看其他函数,就可以按职责分类,而不必按文件排列顺序逐个背诵。
video_ioctl2video_usercopy | ||
video_translate_cmdvideo_get_user、video_put_user | ||
check_array_args | ||
v4l2_ioctl_infov4l2_ioctls、__video_do_ioctl | ||
v4l2_ioctl_get_lock | ||
v4l_querycapcheck_fmt、v4l_sanitize_format、v4l_g/s/try_fmt | ||
v4l_reqbufsv4l_qbuf、v4l_dqbuf、v4l_streamon/off | ||
v4l_s_ctrlv4l_s_ext_ctrls、v4l_dqevent、crop/selection 包装 | ||
v4l_print_*v4l_fill_fmtdesc、制式相关辅助函数 |
“v4l_g/s/try_fmt”等写法在这张表中表示一组函数的缩写,不是一个实际可调用的 C 标识符。
这些函数共同形成了标准 ioctl 层,但并不是每次调用都会经过所有函数群。先抓住公共入口,再沿具体命令的 info->func 进入相应分支,才是阅读这个文件最有效的组织方式。
19. 最后把整条链路收束成一个模型
一次标准 V4L2 ioctl,可以用下面的过程完整描述:
file 确定操作哪个节点 ↓cmd 确定执行哪一种标准命令 ↓video_usercopy 将应用参数编组为内核临时对象 ↓__video_do_ioctl 获取适当的锁,检查状态、能力和优先级 ↓命令适配器完成共性处理,选择驱动回调或专用框架 ↓实现层执行操作,修改参数并返回状态 ↓标准分发释放锁 ↓video_usercopy 按规则回写结果并清理临时对象这里有四个始终不能混淆的边界:命令身份与节点能力、应用地址与内核临时对象、命令分发与实际硬件动作、驱动返回值与系统调用最终结果。
理解这些边界以后,v4l2-ioctl.c 就不再是一组分散的 VIDIOC_* 包装函数,而是一套结构清楚的执行协议:入口负责找到节点,参数层负责建立可处理的数据,分发层负责检查与选路,具体实现负责完成动作,返回层负责把结果可靠地交回。
继续深入缓冲区时,只需要从已经明确的 .vidioc_qbuf = vb2_ioctl_qbuf 连接处进入,便能自然追到 VB2 的对象、状态机、等待与完成机制,而不必重新猜测调用入口。