ARTICLE · 1290815
V4L2 源码深度解析(十五):把图像和逐帧参数绑在一起——Media Request 与 VB2
开篇:为什么一帧图像需要一份独立的请求?
普通的 V4L2 流式采集已经能完成一条完整链路:申请缓冲区、QBUF、STREAMON、驱动写入、vb2_buffer_done(),最后由 DQBUF 取回结果。对很多设备来说,这已经够用。
但有些硬件需要在每一帧开始前改变一组参数。例如相机在不同帧使用不同曝光时间、模拟增益或白平衡参数;某些视频处理设备也需要让一组控制值与指定缓冲区一起生效。若只调用普通 VIDIOC_S_CTRL,控制值的设置动作与某个具体缓冲区的提交并没有天然的绑定关系。
问题不在于“能不能设置控制值”,而在于怎样明确表达:这组参数属于哪一帧,应该与哪一个缓冲区一起进入处理流程。
Media Request API 为这类场景提供一个请求对象。请求可以关联缓冲区对象和控制参数对象;当请求被提交时,框架验证关联对象,再按请求队列流程将它们交给相应模块处理。它不是新的图像内存分配器,也不会替硬件自动安排曝光时序。
本文以 videobuf2-core.c、videobuf2-v4l2.c、mc-request.c 和 v4l2-ctrls-request.c 的实现为主线,讲清请求对象如何创建、缓冲区怎样绑定、请求怎样验证和排队,以及请求何时完成。特别说明:本文所读的 VIMC 捕获节点用于普通 VB2 采集示例,并没有完整启用 Request API;VIMC 用来对照普通 QBUF/完成路径,Request 的具体机制以 VB2 核心、Media Request 核心和控制框架实现为准,不能把 VIMC 描述成可直接运行的 Request 示例。

1. 先把三个对象分清楚:buffer、control 和 request
1.1 Buffer 仍然是 VB2 管理的缓冲区
一个 vb2_buffer 仍然有自己的索引、平面、存储关联和队列状态。使用 Request API 并不会把它替换成另一种缓冲区,也不会把像素数据放进 request fd 里。
Request 场景中,缓冲区会通过 struct media_request_object 的关联机制挂到请求上。VB2 还保留 struct vb2_buffer 自己的请求关联成员,例如 req_obj 和请求引用指针。请求关联的是“这块缓冲区对象参加哪次请求”,不是复制整帧图像。
1.2 Control 是参数对象,不是图像缓冲区
V4L2 控制框架为请求中的控制值建立独立的 handler/对象。VIDIOC_S_EXT_CTRLS 在 request-value 模式下,修改的是该请求所关联的控制值集合,而不是简单地把全局当前控制值立即改成这组值。
驱动需要在合适的处理时机调用 v4l2_ctrl_request_setup(),把请求里的值应用到对应控制处理路径;完成后再调用 v4l2_ctrl_request_complete(),让控制请求对象完成并整理返回值。具体在哪个硬件阶段应用控制,取决于驱动如何把它与传感器或处理器的帧边界关联起来。
1.3 Request 是把对象组织在一起的容器
struct media_request 管理一组 request objects,并记录请求状态、引用计数、尚未完成的对象数量和等待队列。一个请求可以包含控制对象和一个或多个缓冲区对象;但哪些对象组合有效,最终还取决于媒体设备的验证规则和各驱动支持的能力。

这三者不要混为一谈:
buffer 描述一块存储及其队列状态。 control object 保存这次请求的参数状态。 request 负责把多个对象关联起来,并协调验证、排队和完成。
2. Request API 不是所有 V4L2 节点都自动支持
2.1 需要媒体设备和请求操作
请求 fd 由 Media Request 核心创建。应用通常先打开对应的 /dev/mediaX,通过 MEDIA_IOC_REQUEST_ALLOC 申请 request fd。内核使用匿名文件为 request 建立文件描述符入口;这个 fd 指向请求对象,不指向图像存储,也不是 /dev/videoX 的另一个 fd。
media_request_get_by_fd() 会验证 fd 对应的文件操作和所属 media_device,并取得请求引用。无效 fd、普通文件 fd,或者属于另一个媒体设备的请求,都不能被当作当前节点可用的 request。
媒体设备还需要提供请求验证和排队操作。若 mdev 不存在,或者 req_validate、req_queue 等必要操作没有建立,Request 路径会失败,而不是退化成普通 QBUF。
2.2 队列能力也必须明确配置
VB2 队列通过 supports_requests 和 requires_requests 描述请求能力:前者表示队列可以接受 Request 方式,后者表示队列必须使用 Request 方式。requires_requests 不能脱离 supports_requests 单独设置。
框架还会记录队列实际使用的是普通 QBUF 还是 Request QBUF。普通模式与 Request 模式不能在同一队列中随意混用;一旦队列进入一种模式,再从另一种方式提交,核心会按当前实现返回错误。实现细节上,模式标志是在核心分支中较早设置的,早于后续所有状态检查;因此某次 QBUF 最终报错,并不保证队列的模式记录完全没有变化。排查“第一次提交失败后,另一种提交方式为什么被拒绝”时,应同时查看 uses_qbuf、uses_requests 的赋值位置。
Request 队列的配置还有额外约束。例如所分析实现会警告 supports_requests 与非零 min_buffers_needed 的组合;这类队列初始化规则应与具体源码版本的 vb2_core_queue_init() 一起检查,不能只设置一个布尔字段就宣称支持 Request。

2.3 VIMC 在这里扮演什么角色
VIMC 捕获节点展示了普通 VB2 路径:VIDIOC_QBUF 进入 vb2_ioctl_qbuf(),再进入 VB2 的队列核心;驱动提供 buf_queue、start_streaming、stop_streaming 等回调。
但它没有在这份捕获队列配置中完整启用 Request 支持,也没有提供 Request 路径要求的 buf_request_complete 回调。因此,不能用 VIMC 的普通捕获节点直接验证完整的 Request 流程。本文将它作为普通队列的对照对象,Request 的真实实现则沿通用核心和控制框架代码展开。
3. Request 的状态:不是把 fd 排进队列就结束了
Media Request 在实现中有一组明确状态:IDLE、UPDATING、VALIDATING、QUEUED、COMPLETE 和 CLEANING。它们表示请求对象自身的生命周期,不是 VB2 缓冲区的 DEQUEUED/QUEUED/ACTIVE/DONE 状态。UPDATING 通常是锁定请求进行对象更新时的临时状态;一次更新结束、解锁后,请求会回到 IDLE,不会一直停留在 UPDATING 等待排队。
IDLE | ||
UPDATING | ||
VALIDATING | MEDIA_REQUEST_IOC_QUEUE 后 | |
QUEUED | ||
COMPLETE | ||
CLEANING |

请求状态和缓冲区状态是两套相互关联但不相同的状态机。一个请求可以包含多个缓冲区;单个缓冲区完成,并不一定意味着整个请求完成。只有请求中的所有未完成对象都完成或解除绑定,请求才会进入 COMPLETE。
4. 第一步:分配 request fd
典型的用户态操作从媒体设备节点开始:
int media_fd = open("/dev/media0", O_RDWR | O_CLOEXEC);
int request_fd = -1;
if (media_fd < 0) {
perror("open media device");
return-1;
}
if (ioctl(media_fd, MEDIA_IOC_REQUEST_ALLOC, &request_fd) < 0) {
perror("MEDIA_IOC_REQUEST_ALLOC");
close(media_fd);
return-1;
}
这段代码展示接口关系,实际设备节点、权限和支持能力需要在目标系统确认。MEDIA_IOC_REQUEST_ALLOC 成功后,内核创建 struct media_request,初始化对象链表、锁、等待队列、状态和引用计数,再把 request fd 安装到进程 fd 表中。
源码中的 media_request_alloc() 先分配 request 对象和未使用 fd,再创建匿名文件,最后安装 fd。中途任何一步失败,都要撤销已经取得的资源。request_fd 只是查找入口;media_request_get_by_fd() 成功后拿到的是内核 request 对象引用。

5. 第二步:把逐帧控制值写进请求
5.1 普通控制与 Request 控制的区别
普通控制设置通常修改设备当前控制状态;Request 控制则通过 V4L2_CTRL_WHICH_REQUEST_VAL 和 request_fd 指明要修改哪一个请求的控制值集合。
概念上的调用形态如下,具体控制 ID、类型和数组长度要按驱动暴露的控制项填写:
structv4l2_ext_controlsecs = {0};
structv4l2_ext_controlec[2] = {0};
ecs.which = V4L2_CTRL_WHICH_REQUEST_VAL;
ecs.request_fd = request_fd;
ecs.count = 2;
ecs.controls = ec;
/* ec[0]、ec[1] 分别填写驱动支持的逐帧控制 ID 和值 */
if (ioctl(video_fd, VIDIOC_S_EXT_CTRLS, &ecs) < 0) {
perror("VIDIOC_S_EXT_CTRLS for request");
}
不要把示例中的两个控制项理解成任意相机都支持的曝光和增益 ID。控制是否支持 Request、数值范围以及控制簇关系,都由设备控制框架决定。
5.2 控制框架怎样保存这些值
v4l2-ctrls-request.c 中的 try_set_ext_ctrls_request() 根据 request_fd 取得 request,锁定请求以便更新,再查找或创建与主控制处理器对应的 request handler。新 handler 会克隆需要的控制引用,并作为 request object 绑定到请求。
这一步先建立“这组值属于哪个请求”的关系,不代表传感器寄存器已经在此刻写入。稍后驱动在处理该请求对应的缓冲区时,才需要在合适时机调用 v4l2_ctrl_request_setup()。request fd 关闭也不必然立即销毁请求对象:VB2 在绑定缓冲区时会持有请求引用,内核引用用于保证对象在异步处理期间仍然有效;但应用仍应按正常生命周期等待完成并执行清理,而不是依赖引用计数代替协议。

6. 第三步:QBUF 不再直接把缓冲区送进普通队列
6.1 在 v4l2_buffer 中指定 request_fd
使用 Request 方式提交缓冲区时,需要设置 V4L2_BUF_FLAG_REQUEST_FD,并填写对应的 request_fd。简化示例如下:
structv4l2_bufferb = {0};
b.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
b.memory = V4L2_MEMORY_MMAP;
b.index = 0;
b.flags |= V4L2_BUF_FLAG_REQUEST_FD;
b.request_fd = request_fd;
if (ioctl(video_fd, VIDIOC_QBUF, &b) < 0) {
perror("VIDIOC_QBUF with request");
}
这里的 index 仍然选择 VB2 缓冲区槽位;request_fd 选择这块缓冲区要绑定到哪个请求。两个字段表达不同关系。
6.2 vb2_queue_or_prepare_buf() 做哪些检查
V4L2 适配层检查缓冲区类型、索引、平面数组和相关输入字段。若设置了 Request 标志,还要检查队列支持请求、当前队列没有锁定在普通 QBUF 模式、request fd 属于正确的媒体设备、请求状态允许更新,并确认队列提供 buf_request_complete 等必要回调。
它还会检查缓冲区是否处于可绑定的状态。不能把一个已经 ACTIVE 或已经排队的对象再绑定到另一个请求。
6.3 vb2_core_qbuf() 的 Request 分支
这是本篇最重要的区别。普通 QBUF 会准备缓冲区、加入 queued_list,并在满足条件时移交驱动。Request QBUF 的第一次进入则不同:核心初始化缓冲区的 req_obj,锁定 request 以便更新,把缓冲区对象绑定到 request,随后将缓冲区状态设为 VB2_BUF_STATE_IN_REQUEST,并持有 request 引用。
此时缓冲区还没有进入普通的 queued_list,也没有因为这次 QBUF 直接调用驱动的 buf_queue()。 它只是成为这个请求的一部分。真正的队列提交发生在后续 MEDIA_REQUEST_IOC_QUEUE 成功验证之后。

7. 第四步:MEDIA_REQUEST_IOC_QUEUE 验证并提交请求
应用把控制和缓冲区对象准备好后,通过 request fd 发出 MEDIA_REQUEST_IOC_QUEUE。这次操作由 mc-request.c 的请求文件操作处理,不是视频节点上的 VIDIOC_STREAMON,也不是普通的 VIDIOC_QBUF。调用形式如下:
if (ioctl(request_fd, MEDIA_REQUEST_IOC_QUEUE, NULL) < 0) {
perror("MEDIA_REQUEST_IOC_QUEUE");
}
这里的 request_fd 是 ioctl 的文件描述符参数;请求队列命令本身不使用视频缓冲区的 index。
media_request_ioctl_queue() 先使用 req_queue_mutex 串行化请求排队与相关更新,检查请求处于允许提交的状态,把它标为 VALIDATING,再调用媒体设备的 req_validate。验证失败时请求回到 IDLE,并按对象回调撤销已经完成的准备;验证成功后先把状态改成 QUEUED,再调用 req_queue。
这个顺序很重要:框架在调用对象的 queue 回调之前先把 request 标为 QUEUED,允许某些对象在队列回调中很快完成或解除绑定。排队回调本身没有统一的错误返回通道,所以对象的验证必须尽可能在前面完成。

7.1 vb2_request_validate() 不只是检查 fd
VB2 的验证辅助函数首先检查请求中是否至少有一个 VB2 buffer object,再遍历请求对象,对实现了 .prepare 回调的对象执行准备。缓冲区对象的 .prepare 会进入 VB2 的缓冲区准备路径;若某个对象准备失败,框架会按反向顺序调用已准备对象的 .unprepare,尽量撤销先前准备。
这不是硬件启动。验证主要保证请求对象在进入排队阶段前满足框架约束。传感器寄存器什么时候写、DMA 描述符什么时候提交,仍属于设备驱动的运行时安排。
7.2 vb2_request_queue() 怎样让缓冲区进入队列
验证通过后,媒体设备的 req_queue 通常连接到 vb2_request_queue()。该函数遍历请求对象,调用每个对象提供的 .queue 回调。
VB2 缓冲区对象的 vb2_req_queue() 会在队列锁保护下调用 vb2_core_qbuf(),但这次不再携带 request 参数。因为缓冲区状态是 IN_REQUEST,核心将它作为已经绑定请求的缓冲区推进普通排队路径,执行必要准备、加入 queued_list,并根据流状态决定何时调用驱动 buf_queue()。
控制对象也有自己的 .queue 回调。v4l2_ctrl_request_queue() 将对应控制 handler 加入主 handler 的请求控制队列。请求对象的组织顺序使控制对象可以先被登记,再处理缓冲区对象;但这不等于控制值必然在同一 CPU 指令时刻应用到硬件,应用时机仍由驱动控制处理路径决定。
8. 控制参数什么时候真正应用到设备?
8.1 S_EXT_CTRLS 绑定值,setup() 才进入应用阶段
控制请求对象被排入请求后,驱动在处理对应缓冲区的适当阶段调用 v4l2_ctrl_request_setup(req, handler)。这个函数要求请求处于 QUEUED,找到该控制 handler 的 request object,再将请求值装入控制框架的待设置值并调用对应控制操作。
“适当阶段”不能用一句固定话套到所有设备上。传感器可能需要在帧开始前写入寄存器;某些硬件会把参数写入每帧描述符;也有设备在特定边界切换控制值。框架提供请求值的保存和应用机制,驱动必须将它与真实硬件时序对齐。
8.2 complete() 是控制对象的完成通知
当驱动确认这次请求的控制处理已经完成,调用 v4l2_ctrl_request_complete()。控制框架会收集适用的控制结果,移除 queued handler 关联,并调用 media_request_object_complete() 标记这个控制对象完成。
如果驱动漏掉 setup 或 complete,结果可能是控制值没有按预期应用,或者 request 一直不能进入 COMPLETE。因此,Request 支持不是只在 QBUF 时填一个 fd;驱动还必须在处理和完成阶段配对控制对象的生命周期。

9. 一次请求怎样算真正完成?
Media Request 在对象绑定时增加未完成对象计数。请求里可以有控制对象、缓冲区对象或其他对象。每个对象需要通过完成或解除绑定路径减少这个计数;当计数归零时,请求状态变为 COMPLETE,等待请求完成的 poll 路径会被唤醒。
对于 VB2 缓冲区,驱动完成处理后调用 vb2_buffer_done()。在正常 DONE/ERROR 分支中,VB2 会解除缓冲区的 request-object 绑定并减少请求的未完成对象计数。注意:这与应用何时 DQBUF 是不同的时刻。完成通知说明驱动结束本轮使用;DQBUF 负责把缓冲区结果从 VB2 队列取回。内核还会保留必要的 request 引用,避免对象在后续清理前失效。
控制对象通常由 v4l2_ctrl_request_complete() 完成。因此,一个请求含有缓冲区和控制对象时,只有二者都按约定完成或解除绑定,整个 request 才能完成。单个 buffer DONE 不足以证明请求整体 COMPLETE。

10. REINIT、取消与错误路径
10.1 REINIT 不是重新申请图像缓冲区
MEDIA_REQUEST_IOC_REINIT 用于重置处于允许状态的请求。所分析实现允许 IDLE 或 COMPLETE 请求进入清理,但若仍有访问者,重置会失败。清理过程解绑请求对象、释放对象引用,然后将 request 状态恢复为 IDLE,供下一轮重新填充。
REINIT 重置的是请求容器和关联对象,不会替代 REQBUFS 重新分配图像存储,也不会自动重新映射视频缓冲区。
10.2 请求验证失败
如果请求仍未通过验证,媒体请求核心将状态恢复为 IDLE。VB2 验证辅助函数会对已准备的对象调用 .unprepare;具体对象是否保留已有的底层存储关联,要看对应对象回调,不能把“验证失败”理解成所有内存都已经释放。
10.3 队列取消时的 Request 清理
VB2 在取消队列时会检查缓冲区关联的 request。如果请求仍处于 QUEUED,核心可调用驱动的 buf_request_complete 回调,让驱动完成与缓冲区相关的请求对象;随后清理内存准备状态、缓冲区状态和 request-object 引用。
这也是队列配置要求实现 buf_request_complete 的原因之一:请求取消时,框架需要给驱动机会收尾它额外维护的请求状态。这个回调不是“释放图像内存”的通用替代品,驱动仍须按自己获取的资源逐项配对。

11. 普通 QBUF 与 Request QBUF:把差异放在同一张表里
QUEUED,满足条件时交给驱动 | IN_REQUEST,尚未进入普通队列 | |
MEDIA_REQUEST_IOC_QUEUE | ||
这张表的关键是:Request 不是把普通 QBUF 包一层名字。它在缓冲区进入队列前增加了“绑定—验证—整体排队”的阶段,并且需要管理缓冲区以外的请求对象。
12. 驱动接入时要检查什么
若要让实际采集驱动支持 Request,至少要逐项核实以下事项,而不是只在 vb2_queue 上设置一个标志:
队列是否声明并正确支持 Request,是否实现必需的 buf_request_complete等回调。媒体设备是否提供与 VB2 相匹配的请求验证和排队操作。 控制处理器是否初始化并接入 Request 控制机制;请求控制值何时由驱动 setup,何时 complete。 buf_queue收到的缓冲区如何关联到对应请求;驱动怎样在正确帧边界应用该请求的参数。启动失败、STREAMOFF、设备错误和关闭时,仍绑定的 request object 如何取消、完成或解绑。 缓冲区 DONE/ERROR 与 request COMPLETE 是两层结果,应用不能假设它们一定在同一个时刻发生。
vimc-capture.c 可以用来观察普通 VB2 的 buf_queue 和 vb2_buffer_done(),但它不是完整 Request 支持驱动的模板。真正移植时,应选择明确支持 Media Request 的驱动实现作为第二个对照,并检查该驱动如何调用控制 setup/complete、如何把 request 与硬件帧边界对应起来。
13. 一轮请求的用户态顺序
在设备和驱动确实支持 Request 的前提下,一轮操作的概念顺序如下:
打开 /dev/mediaX 与对应视频节点
↓
为视频队列准备缓冲区池与映射
↓
MEDIA_IOC_REQUEST_ALLOC → 得到 request_fd
↓
S_EXT_CTRLS(REQUEST_VAL)把逐帧控制值绑定到 request
↓
QBUF(REQUEST_FD + request_fd)把缓冲区绑定到 request
↓
MEDIA_REQUEST_IOC_QUEUE
↓
媒体设备验证请求,随后排队 request objects
↓
驱动在适当阶段应用请求控制,处理对应缓冲区
↓
缓冲区完成 + 控制对象完成
↓
request 进入 COMPLETE;应用按接口取回缓冲区结果
↓
按允许状态 REINIT,或关闭 request fd
这是一条概念主线,不表示控制 setup、硬件启动、buffer done 和 request complete 都在同一函数或同一线程执行。实际时序由媒体设备和驱动实现决定。应用还必须处理每个 ioctl 的失败返回,不能假定分配、设置控制、绑定缓冲区和排队一定全部成功。
14. 复核时最容易出现的误解
把 request fd 当成图像 fd。 request fd 指向请求对象,图像仍由 VB2 缓冲区及其内存后端管理。 认为带 REQUEST_FD的 QBUF 立即进入buf_queue(). 在本实现中,这一步先把缓冲区绑定到 request,状态进入IN_REQUEST;请求被成功排队后,buffer object 的 queue 回调才继续推进 VB2 队列。认为 MEDIA_REQUEST_IOC_QUEUE等于 STREAMON。 前者提交一组请求对象,后者启动 VB2 流队列;二者职责不同。认为 S_EXT_CTRLS成功就表示传感器寄存器已经写入。 它建立请求参数,驱动要在合适时机调用控制 setup 路径。认为一个缓冲区 DONE 就代表整个 request COMPLETE。 请求可能还包含控制对象或其他缓冲区,需等所有对象完成或解绑。 认为所有 V4L2 节点都支持 Request。 队列配置、媒体设备操作、控制框架和驱动回调必须共同支持。 把 VIMC 当成 Request 的可运行示例。 本文所分析的 VIMC 捕获路径用于解释普通 VB2;它没有完整启用 Request 支持。
小结
Media Request 解决的是“把缓冲区和逐帧参数作为一个请求组织起来”的问题。它不会替代 VB2 的缓冲区管理,也不会自动完成硬件时序。
读源码时,最关键的分界是:Request QBUF 先绑定对象,MEDIA_REQUEST_IOC_QUEUE 验证成功后才调用对象的排队回调;缓冲区完成和控制对象完成分别推进 request 的完成计数。 只有把这几层分开,才能准确判断一次请求现在处于哪个阶段,以及异常时应该由谁清理。















































































