ARTICLE · 1207431
V4L2 源码深度解析(十):先停访问,再还缓冲区——STREAMOFF 与异常退出
上一节中,采集已经跑通:驱动写入一帧,调用 vb2_buffer_done();应用通过 DQBUF 取得结果,处理完再 QBUF。同一组内存就这样反复使用。
现在准备结束采集。最容易想到的做法是把四块映射解除、释放缓冲区、关闭节点。但此时可能有一块仍在处理线程手中,另一块已经完成却还没有出队,还有一个线程睡在 DQBUF 里面。直接释放存储,可能让仍在运行的执行方访问失效内存;只清掉 streaming 标志,又不能让正在执行的操作凭空消失。
停止过程必须按依赖关系收尾:先让实际访问结束,再撤销本轮排队,最后按需要释放存储和对象。 STREAMOFF 负责其中的停流与取消,不能把它理解成一次“释放所有内存”。
本篇继续使用 VIMC、普通 CAPTURE、MMAP、vmalloc 后端,默认不使用 Media Request。缓冲区池和映射已经建立。源码采用 min_buffers_needed、prepared、synced、固定 bufs[] 的这套实现;特定分支的行为以文中的函数体为准。
1. 先把“收回缓冲区”讲清楚
1.1 停流时,四块缓冲区不一定处于同一阶段
假设池里有四块缓冲区,停止前出现下面这个时刻:buffer 0 已从驱动私有列表取出,处理线程正在写它;buffer 1 还在驱动列表中等待;buffer 2 已经完成,留在 VB2 的完成列表;buffer 3 只做了 PREPARE_BUF,还没有 QBUF。
这是一个用于推演的快照,不是固定的索引分工。四块存储都存在,但需要收尾的关系不一样:
ACTIVE | ||
ACTIVE | ||
DONE | ||
DEQUEUEDprepared=1 |
前三块都还在 VB2 的 queued_list 中,只有 buffer 2 同时位于 done_list;buffer 3 不在这两条排队列表中。此时 queued_count=3,owned_by_drv_count=2。驱动的 buf_list 则只有 buffer 1,正在执行的 buffer 0 已经离开这条私有列表。
驱动列表为空,不等于没有正在处理的缓冲区。 这是停流时首先要排除的误判。

图 1:按本篇假设的时刻绘制。虚拟映射可以同时存在,但不能用映射是否存在判断实际访问是否已经结束。
1.2 停止运行、取消排队和释放存储是三件事
停止运行,解决的是“还会不会继续写这块内存”。对 VIMC,是让软件处理线程退出;对真实设备,可能涉及 DMA 停止、硬件空闲确认及中断、工作队列的同步。
取消排队,解决的是“这一轮已提交和已完成的对象还算不算数”。STREAMOFF 会撤销本轮提交,也会丢弃尚未出队的完成结果。它不是把历史结果逐个 DQBUF 后才结束的排空操作。
释放存储,解决的是“队列是否还需要持有这些缓冲区”。MMAP 主线中,成功 STREAMOFF 后,缓冲区管理对象、索引和像素存储仍可保留,已有映射也还在。重新 QBUF、STREAMON 可以复用这个池;不必每次暂停都重新 REQBUFS 和 mmap。
因此,本文所说的“收回全部缓冲区”,首先是收回本轮交给框架和驱动的使用关系,而不是把四块存储全部 kfree()。已经 DQBUF 并交给其他应用线程或设备处理的图像,也不会被 STREAMOFF 自动停止消费。
实现对照:vimc_capture_process_frame()、vb2_buffer_done()、__vb2_queue_cancel();停止接口约定见文末“停流与取消”。
2. STREAMOFF 从哪里进入,在哪里可能被拒绝
2.1 参数是队列类型,不是缓冲区编号
应用按已经建立的队列类型发起请求:
int type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (ioctl(fd, VIDIOC_STREAMOFF, &type) < 0) {
perror("VIDIOC_STREAMOFF");
/* 不能在这里直接宣告停流已经完成。 */
}
多平面采集使用相应的 _MPLANE 类型。这里必须传 &type,因为 ioctl 参数是整数的地址。STREAMOFF 作用于整条队列,没有一个表示“只停 buffer 0”的 index 参数。
VIMC 的操作表连接了 .vidioc_streamoff = vb2_ioctl_streamoff。标准 ioctl 层先处理参数、选锁、检查节点状态与命令能力,再经 v4l_streamoff() 将类型值传入 VB2。
应用 ioctl(STREAMOFF)
→ v4l2_ioctl → video_ioctl2 → __video_do_ioctl
→ v4l_streamoff → vidioc_streamoff
→ vb2_ioctl_streamoff
→ vb2_streamoff
→ vb2_core_streamoff
→ __vb2_queue_cancel
上图省略了此前讲过的参数编组内部层次。调用到取消函数之前,仍可能在外层失败,不能因为“这是退出命令”就假设它绕过全部检查。
2.2 owner、fileio 与类型检查各管一层
intvb2_ioctl_streamoff(struct file *file, void *priv, enum v4l2_buf_type i)
{
structvideo_device *vdev = video_devdata(file);
if (vb2_queue_is_busy(vdev->queue, file))
return -EBUSY;
return vb2_streamoff(vdev->queue, i);
}
这一层检查当前打开上下文是否能操作队列。队列已经属于另一个上下文时返回 -EBUSY,不会继续调用驱动的停流回调。它不比较 PID,也不因这是 STREAMOFF 就临时改变 owner。
intvb2_streamoff(struct vb2_queue *q, enum v4l2_buf_type type)
{
if (vb2_fileio_is_active(q)) {
dprintk(q, 1, "file io in progress\n");
return -EBUSY;
}
return vb2_core_streamoff(q, type);
}
下一层排除 fileio 辅助流占用,之后才进入核心。fileio 有自己的组合清理入口,不能把显式流式接口与 read/write 辅助流任意混用。
intvb2_core_streamoff(struct vb2_queue *q, unsignedint type)
{
if (type != q->type) {
dprintk(q, 1, "invalid stream type\n");
return -EINVAL;
}
__vb2_queue_cancel(q);
q->waiting_for_buffers = !q->is_output;
q->last_buffer_dequeued = false;
dprintk(q, 3, "successful\n");
return0;
}
核心没有 if (!q->streaming) return 0。类型匹配以后,即使尚未运行,也会执行取消。因此,只有 PREPARE_BUF 或 QBUF、从来没有 STREAMON 的对象,同样可以由这条路径恢复到正常可重新使用的状态。
函数最后为 CAPTURE 设置 waiting_for_buffers=true,并清掉 last_buffer_dequeued。前者供后续等待/就绪语义使用,不是启动标志;它与 waiting_in_dqbuf 也不是同一个字段。
2.3 外层仍有优先级和锁
命令表将 STREAMOFF 标为 INFO_FL_PRIO | INFO_FL_QUEUE。使用标准文件句柄时,优先级检查可能在进入上述辅助函数前返回 -EBUSY;队列命令按规则选择操作互斥锁。VIMC 的 q->lock 和 vdev->lock 都指向 vcapture->lock。
支持 Media Request 的设备还会在 STREAMON/OFF 分发时先取得可选的 req_queue_mutex,再取得本次操作锁。本篇普通路径不需要画成始终持有两把锁。等待可中断锁失败时,也可能尚未开始任何停流工作。

图 2:布局回调与内存分配不在这条路径中。取消过程返回核心后,核心设置两个等待相关字段,再逐层返回并释放实际取得的锁。
实现对照:v4l_streamoff()、v4l2_ioctls[]、__video_do_ioctl()、两个 STREAMOFF 辅助入口及 vb2_core_streamoff()。
3. 进入取消函数:先让驱动停止,不能反过来
3.1 为什么先判断 start_streaming_called
__vb2_queue_cancel() 的第一步是:
if (q->start_streaming_called)
call_void_qop(q, stop_streaming, q);
它依据的是“驱动启动入口是否已经执行”,而不是只看 q->streaming。这恰好对应第八篇的延后启动:框架可能接受了 STREAMON,但提交数量还不够,驱动并没有开始运行。这时取消预排对象即可,不应虚构一次已经启动的硬件再去停止。
反过来,正常运行时,VB2 先调用驱动 stop_streaming(),等它返回后,才清框架状态。这个顺序保留了回调所需要的运行上下文,也要求驱动在返回前结束实际访问、归还自己接收的全部对象。
停止回调的类型是 void。它不是“发一个停止请求就返回,让 VB2 随后自己等硬件”;也没有一个通用的错误返回值,可以告诉核心“硬件没停住,请继续保留 ACTIVE”。设备必须在自身实现中完成停止、等待或必要的安全恢复,满足回调契约。
3.2 回调返回时,驱动应当交出什么
从 buf_queue() 接收的每个对象,都要通过 vb2_buffer_done() 归还。正常停流中允许 DONE 或 ERROR;未形成可交付图像的剩余对象通常用 ERROR。启动失败时的 QUEUED 回退属于另一种场景,不能混用。
对于 VIMC,处理线程手里的对象可能完成最后一次复制并报告 DONE;私有列表里没来得及处理的对象,则在停止后按 ERROR 归还。两者最终都会被整队取消,不能期待应用在 STREAMOFF 返回后继续逐个 DQBUF 这些结果。
“按 ERROR 归还给 VB2”与“应用会收到一个错误帧”不是同一承诺。 停流期间归还,只是先闭合驱动接收与归还的配对,随后取消函数还会撤销完成列表。
4. VIMC 的停止:等待线程退出,再清私有列表
4.1 三个动作的实际顺序
staticvoidvimc_capture_stop_streaming(struct vb2_queue *vq)
{
structvimc_capture_device *vcapture = vb2_get_drv_priv(vq);
vimc_streamer_s_stream(&vcapture->stream, &vcapture->ved, 0);
/* Stop the media pipeline */
video_device_pipeline_stop(&vcapture->vdev);
/* Release all active buffers */
vimc_capture_return_all_buffers(vcapture, VB2_BUF_STATE_ERROR);
}
第一步让 streamer 停止;第二步结束媒体管线的框架登记;第三步从捕获驱动的私有列表归还剩余缓冲区。
video_device_pipeline_stop() 处理媒体管线的关联和使用登记,不能单独当成停止所有 Sensor 或 DMA 的万能接口。本例对各子设备的 s_stream(0) 调用,发生在 streamer 的终止路径中。
4.2 kthread_stop 不是在 memcpy 中间强行销毁线程
vimc_streamer_s_stream(..., 0) 在发现线程存在以后,执行如下停止分支节选:
if (!stream->kthread)
return0;
ret = kthread_stop(stream->kthread);
if (ret)
dev_dbg(ved->dev, "kthread_stop returned '%d'\n", ret);
stream->kthread = NULL;
vimc_streamer_pipeline_terminate(stream);
kthread_stop() 设置停止条件、唤醒目标线程,并等待它退出。线程需要检查 kthread_should_stop() 后主动返回。VIMC 在线程循环开头检查这个条件;如果此时已经进入一轮 process_frame(),通常要等当前轮执行到能够退出的位置。
这说明两个实际现象:STREAMOFF 可能等待正在处理的一帧完成;等待耗时也不能只根据帧率写成一个固定的毫秒数。如果处理函数卡在不能结束的操作里,停流路径也可能卡住。
VIMC 对 kthread_stop() 返回的线程结果只记录调试信息,然后继续终止管线。这不是忽略“线程仍在写”的错误;等待线程退出与线程函数的返回值要分开理解。代码注释还专门考虑了刚启动便停止、线程尚未正常进入执行的情形。
4.3 为什么必须先等线程,而不是先遍历 buf_list
vimc_capture_process_frame() 在 qlock 下摘除一个对象,然后解锁,才复制像素并报告完成。一个正在复制的缓冲区已经不在 buf_list 中。
如果先把列表中的对象全归还,再宣告“所有缓冲区已经回收”,正在复制的那块就会被漏掉。更糟的是,如果核心随后释放它,线程仍持有裸指针。
本例先 kthread_stop() 等线程退出,就把这段列表外的在途访问也覆盖了。等线程确实结束,再终止子设备处理路径、清理剩余列表,依赖顺序才成立。

图 3:停止请求可以发生在一轮处理途中。图中允许当前处理完成后退出,并不要求每次停流都恰好产生最后一帧。
4.4 剩余对象怎样按 ERROR 归还
staticvoidvimc_capture_return_all_buffers(struct vimc_capture_device *vcapture,
enum vb2_buffer_state state)
{
structvimc_capture_buffer *vbuf, *node;
spin_lock(&vcapture->qlock);
list_for_each_entry_safe(vbuf, node, &vcapture->buf_list, list) {
list_del(&vbuf->list);
vb2_buffer_done(&vbuf->vb2.vb2_buf, state);
}
spin_unlock(&vcapture->qlock);
}
list_for_each_entry_safe() 允许遍历时删除当前成员。每次先摘掉驱动的 list,再调用 vb2_buffer_done(),后者撤销 ACTIVE 和驱动持有计数,并把 ERROR 对象放入 VB2 完成列表。
这里持有的是 VIMC 的 qlock,不是 VB2 的 done_lock。完成函数会按自身规则取得后者。相应锁顺序以及调用上下文必须一致,不能在自旋锁临界区里加入可能睡眠的任意资源销毁。VIMC 的软件示例也不能作为所有 DMA 后端都能在任意上下文执行的证明。
终止管线函数按已记录数组的逆序取出实体,对子设备调用 s_stream(0)。该实现没有把每次子设备关闭返回值汇总成一个失败码;因此不应把它推广成所有硬件的完备故障恢复模板。
实现对照:vimc_capture_stop_streaming()、vimc_streamer_s_stream()、vimc_streamer_thread()、vimc_streamer_pipeline_terminate()、vimc_capture_return_all_buffers();线程停止约定见文末。
5. 驱动返回以后,VB2 为什么还要检查 ACTIVE
5.1 兜底告警不是硬件停止机制
取消函数随后检查:
if (WARN_ON(atomic_read(&q->owned_by_drv_count))) {
for (i = 0; i < q->num_buffers; ++i)
if (q->bufs[i]->state == VB2_BUF_STATE_ACTIVE) {
pr_warn("driver bug: stop_streaming operation is leaving buf %p in active state\n",
q->bufs[i]);
vb2_buffer_done(q->bufs[i], VB2_BUF_STATE_ERROR);
}
WARN_ON(atomic_read(&q->owned_by_drv_count));
}
正确的停止回调应该使 owned_by_drv_count 已经归零。这里出现告警,意味着驱动没有按契约归还干净。核心遍历仍为 ACTIVE 的对象,强制按 ERROR 收回,再检查计数。
这只是软件账目的兜底。它不能替驱动关闭 DMA,也不会自动清理驱动私有链表和定时任务。如果硬件仍在写,核心把对象从 ACTIVE 改成 ERROR 反而可能让后续清理继续推进。看到告警后不能以“框架已经帮我回收”为由忽略驱动缺陷。
排障要同时检查:是否漏归还了对象,是否重复归还导致计数不平,是否还存在中断或工作线程的晚到完成。重复调用 vb2_buffer_done() 也不是合法的幂等清理;入口要求对象仍为 ACTIVE。
5.2 vb2_wait_for_all_buffers 只等待配对完成
intvb2_wait_for_all_buffers(struct vb2_queue *q)
{
if (!q->streaming) {
dprintk(q, 1, "streaming off, will not wait for buffers\n");
return -EINVAL;
}
if (q->start_streaming_called)
wait_event(q->done_wq, !atomic_read(&q->owned_by_drv_count));
return0;
}
这个辅助函数等待的是 owned_by_drv_count == 0,不是 DMA 硬件空闲寄存器。它不扫描像素、不触发归还,也不替调用者释放操作锁,没有内置超时。
它可以用于设备仍能推进完成的停止实现,但前提是还有某条执行路径负责将对象归还。若先屏蔽了唯一的完成中断,再等待计数归零,却没有其他回收路径,等待不会自行结束。
它也不会调用 DQBUF 等待所用的 wait_prepare/wait_finish。持有操作锁调用时,必须保证负责完成的路径不依赖同一把锁。不能拿着 done_lock 或某把完成路径必需的私有锁,再睡眠等完成。
本例 VIMC 没有调用这个函数。它通过等待线程退出,再主动归还私有列表来闭合配对,不能在 VIMC 时序里添加一个不存在的“统一等待所有缓冲区”步骤。
6. 核心怎样取消列表,并给每块缓冲区收尾
6.1 先清队列运行状态,再撤销两条列表
q->streaming = 0;
q->start_streaming_called = 0;
q->queued_count = 0;
q->error = 0;
q->uses_requests = 0;
q->uses_qbuf = 0;
INIT_LIST_HEAD(&q->queued_list);
INIT_LIST_HEAD(&q->done_list);
atomic_set(&q->owned_by_drv_count, 0);
wake_up_all(&q->done_wq);
这里归零的是本轮运行和提交计数。num_buffers、bufs[]、当前 MMAP 存储以及 owner 没有在这段代码中释放或清空。
两次 INIT_LIST_HEAD() 使 VB2 的提交列表与完成列表都恢复为空。它不是逐个调用 DQBUF,未取走的图像结果不会再作为本轮结果交给应用。后续重新排队时,缓冲区的链表成员会重新挂接。
源码没有在这里包一层 done_lock。成立的前提是:驱动已停止所有完成生产路径,排队和出队按外层操作锁串行化。不能只复制“无锁重置列表头”这几行到仍有中断并发写列表的驱动里。
6.2 唤醒发生在逐缓冲区收尾之前
这是顺序上很容易画错的一处。核心先清 streaming、撤销列表、唤醒 done_wq,然后才遍历整个池收尾。
唤醒不等于等待线程立刻越过所有锁。VIMC 的标准 DQBUF 醒来后,要重新取得 vcapture->lock;STREAMOFF 路径还持有它,所以 DQBUF 不能在这个临界区中间就把半清理对象交回应用。
因此,应该画出“先唤醒,稍后重新取得操作锁”,而不是把源代码顺序改成“全部清完再唤醒”。对于直接调用核心、自己管理锁的驱动,也必须建立等价的同步条件。
6.3 提前准备但没排队的对象,也需要 finish
遍历池时,非 Request 主干执行:
__vb2_buf_mem_finish(vb);
if (vb->prepared) {
call_void_vb_qop(vb, buf_finish, vb);
vb->prepared = 0;
}
__vb2_dqbuf(vb);
if (vb->req_obj.req) {
media_request_object_unbind(&vb->req_obj);
media_request_object_put(&vb->req_obj);
}
if (vb->request)
media_request_put(vb->request);
vb->request = NULL;
vb->copied_timestamp = 0;
__vb2_buf_mem_finish() 先看 synced:未做过内存 prepare,或已经在正常完成阶段执行过 finish,就直接返回;否则清 synced,逐平面调用内存后端的 finish。
接着,只有 prepared 为真,才调用驱动的 buf_finish 并清准备标志。最后 __vb2_dqbuf() 使对象回到 DEQUEUED。
两种 finish 不可混为一谈。内存后端 finish 处理存储交接;驱动 buf_finish 处理设备相关收尾。VIMC 没有提供自己的 buf_finish,vmalloc 后端也没有实际的 prepare/finish 回调,但核心仍维护配对标志。换成需要缓存操作的后端时,相同调用位置才会执行对应工作。
6.4 同一状态名,可能需要不同的收尾
这张表讨论一次遵守契约的取消。PREPARING 是准备函数内部的临时状态,不应被当成可以与正常取消随意并发操作的稳定状态;相关调用必须按对应的串行化规则组织。

图 4:唤醒位于逐对象收尾之前;驱动停止先于列表重置。虚线表示异常兜底,不代表它可以替代真实访问终止。
6.5 DEQUEUED 不是“数据和结构体全部清零”
staticvoid __vb2_dqbuf(struct vb2_buffer *vb)
{
structvb2_queue *q = vb->vb2_queue;
if (vb->state == VB2_BUF_STATE_DEQUEUED)
return;
vb->state = VB2_BUF_STATE_DEQUEUED;
call_void_bufop(q, init_buffer, vb);
}
已经是 DEQUEUED 的对象在这里直接返回。其他对象改变状态后,调用 buf_ops->init_buffer;当前 V4L2 适配实现主要将 request_fd 设为 -1,并不是对整个缓冲区描述做 memset()。
因此,取消之后不要把旧时间戳、序号或像素残留重新当成有效结果。STREAMOFF 撤销的是本轮结果资格,不需要为了结束排队就把每个像素擦成零。
原函数还有 Request 分支:读取关联请求的状态,必要时调用 buf_request_complete,之后解除 request object 和请求引用。这一段用于避免请求关联悬挂,不等于简单地“释放整个 Request”。源码旁的历史注释出现了 buf_request_cancel 字样,实际调用成员是 buf_request_complete;分析应以代码为准。
实现对照:__vb2_queue_cancel()、__vb2_buf_mem_finish()、__vb2_dqbuf()、__init_vb2_v4l2_buffer()。
7. 另一个线程阻塞在 DQBUF,停止以后怎样退出
7.1 等待条件中本来就包含停止和错误
DQBUF 没有完成结果时,设置 waiting_in_dqbuf,调用 wait_prepare 让出操作锁,再等待:
wait_event_interruptible(q->done_wq,
!list_empty(&q->done_list) || !q->streaming || q->error);
STREAMOFF 把 streaming 清零,再唤醒等待者,所以即使没有新图像,也能使等待条件成立。醒来后,DQBUF 通过 wait_finish 重新取得操作锁,清掉 waiting_in_dqbuf,再检查队列状态。
在节点仍注册、没有信号打断、没有其他操作竞争的本例中,下一轮检查发现 !q->streaming,返回 -EINVAL。不能把它写成“返回一块空缓冲区”,也不能强行套用非阻塞无帧时的 -EAGAIN。
设备已经断开、信号到达、队列错误或其他等待者存在时,返回路径可能不同,应按实际先后判定。waiting_in_dqbuf 由等待路径在醒来后清除,取消函数没有把它直接清零;这也防止另一个等待者在旧等待尚未退出时被误认为不存在。

图 5:控制线程与接收线程共享同一个打开上下文。接收线程睡眠时释放的是实际持有的队列操作锁,醒来后重新取得。
7.2 完成通知先唤醒了等待者,也不保证能取到最后一帧
驱动停流过程中调用 vb2_buffer_done(ERROR),会先产生一次唤醒;取消函数后面又会清空完成列表并唤醒。等待者醒来仍要等操作锁,因此最终可能看到已停止的空队列,而不是中间那块 ERROR 对象。
相反,如果 DQBUF 在停流取得操作锁之前已经成功交付一帧,那块存储的应用处理责任已经成立。STREAMOFF 不会撤销其他线程已经拿到的 C 指针,更不会停止下游编码器或 GPU 的访问。
7.3 应用多线程退出时,不要先等一个永远等不到的 join
一个可实施的退出顺序是:控制线程发布停止状态,阻止新的 QBUF 和新的下游任务;通过共享的有效队列身份执行 STREAMOFF,使阻塞 DQBUF 有机会返回;再等待接收线程退出,并等待已经提交的图像消费者结束,最后解除映射和释放池。
停止标志以及“是否继续 QBUF”的判断也要有实际同步,不能只依赖一句注释。如果先 pthread_join() 一个阻塞在 DQBUF 的线程,却没有停止、错误或信号使它醒来,就可能一直等待。
另一种更易控制的实现是使用非阻塞节点,以 poll 同时等待视频节点和退出通知。无论哪种方式,都不要在信号处理函数里堆叠 ioctl、释放内存和线程等待;处理函数只设置退出标志或发出适当通知,由正常执行路径完成收尾。
实现对照:__vb2_wait_for_done_vb()、vb2_ops_wait_prepare()、vb2_ops_wait_finish()。应用线程设计是建立在这些实现上的同步方案,不是 VB2 自动创建的线程管理。
8. STREAMOFF 返回后,池里到底还剩什么
回到开头的四块缓冲区。假定处理中的 buffer 0 正常完成后线程退出,buffer 1 被按 ERROR 归还,其他两块没有额外并发变化:
中间的完成顺序由实际执行决定,表中不要求固定为 0、1、2。重要的是:驱动归还时没有减少 queued_count,整队取消时才统一撤销提交。
成功 STREAMOFF 不会清 owner,也不会把 num_buffers 改成零。别的独立打开上下文,不能因此就绕过已有池的归属检查。释放池或最后关闭时,才有相应的 owner 更新。
复用池时必须重新 QBUF。不能只再发一个 STREAMON,就假定上一轮已经被取消的列表仍然有效。实际是否立即进入驱动启动回调,仍取决于预排数量和启动门槛。
9. 几种异常退出,不能都画成同一条回滚线
9.1 只有 PREPARE_BUF 或 QBUF,没有真正启动
尚未 STREAMON 时,streaming=0、start_streaming_called=0。STREAMOFF 跳过驱动 stop,但仍执行准备收尾、列表取消和状态恢复。
已经 STREAMON、预排数量还未达到门槛时,可能是 streaming=1、start_streaming_called=0。同样没有运行中的 VIMC 线程需要停止;核心清掉接受过的流请求与预排对象即可。
9.2 启动回调失败,不能寄希望于后面的 stop 再补救
启动函数失败时,驱动已经要按 QUEUED 归还自己接收的对象,并撤销启动过程中实际取得的资源。VB2 会将 start_streaming_called 清零。
后续 STREAMOFF 可能因此不再调用驱动 stop。若启动回调失败返回时还残留运行中的 DMA 或软件线程,不能指望“以后肯定还会再停一次”。这是启动失败分支必须自己闭合实际访问的原因。
立即启动失败与延后 QBUF 触发启动失败,留下的 streaming 标志和本次提交状态也不同。第八篇已经展开这些分支;本篇只强调:取消函数仍会收尾 prepared/synced,但这不能代替驱动启动失败时的硬件撤销。
9.3 单帧 ERROR 与队列 error 不同
voidvb2_queue_error(struct vb2_queue *q)
{
q->error = 1;
wake_up_all(&q->done_wq);
}
vb2_queue_error() 只设置队列错误并唤醒等待者。它没有调用 stop_streaming(),没有逐个归还 ACTIVE,也没有释放存储。
DQBUF 可以因为 q->error 返回 -EIO,即使完成列表里还残留对象。接下来仍需要按设备故障语义停止真实访问、执行取消或移除清理。
取消函数会清 q->error,使框架可以用于下一轮。但清标志不等于修复失联 Sensor、总线错误或硬件死锁。驱动必须先完成真实故障处理,再允许重新开始。
9.4 退出命令返回错误,先找失败发生在哪一层
EBUSY | ||
EINVAL | ||
ENODEV | ||
ENOTTY | ||
STREAMOFF 的参数是只输入的整数。不要把 REQBUFS、DQBUF 那种“驱动动作成功、输出复制再失败”的一般例子原样套到这条命令。输入地址不可访问时会更早失败;停流本身没有一份图像描述需要在最后回写。
“返回 0”也不是检测任意错误驱动硬件状态的独立证明:stop 回调是 void,核心主要依赖驱动履约。发现 ACTIVE 回收告警、晚到中断或停止后写内存,必须修复相应路径。

图 6:不同异常先处理各自尚未结束的访问,再进入核心取消;外层提前拒绝不等于停流完成。
实现对照:vb2_core_streamon()、vb2_core_qbuf()、vb2_start_streaming()、vb2_queue_error()、标准 ioctl 分发与 STREAMOFF 包装。
10. 真正释放池:REQBUFS(0) 与 queue_release
10.1 停止后,应用才决定是否继续保留存储
准备稍后重启,可以保留池。彻底退出时,常见顺序是先完成 STREAMOFF,等待应用侧已有消费者结束,再解除映射、REQBUFS(0)、最后关闭节点。
这里的 REQBUFS(0) 表示不再保留这组队列缓冲区,不是用零填充所有像素。当前 vb2_core_reqbufs() 在最前面检查 q->streaming,即使 count=0 也会返回 -EBUSY。因此,本实现应先显式 STREAMOFF,不依赖接口概述里“零数量隐式停止”的简写。
这是理解接口与实现的一个边界:跨设备程序应遵循接口约定,同时处理具体返回;源码文章不能跳过实际存在的前置拒绝条件。
10.2 释放池还有自己的分层
REQBUFS(0) 的核心分支在 mmap_lock 下先取消,再调用 __vb2_queue_free()。取消可能是重复进入,但不会重复停止一个已经清掉启动标志的驱动,也不会重复 finish 已经清理的普通准备状态。它不是每次调用都保证所有内部语句完全无动作。
__vb2_queue_free() 先检查将释放的对象是否处于 PREPARING;遇到这种状态可返回 -EAGAIN。随后对具备相应存储关联的对象调用可选的 buf_cleanup,释放内存后端关联,再释放缓冲区管理对象、清 bufs[] 并减少 num_buffers。池全部清空时,把 memory 设回 UNKNOWN。
MMAP 对每个平面执行后端 put;USERPTR 和 DMA-BUF 则分别走其解除关联路径。本篇的 MMAP 主线不会在 STREAMOFF 时执行这些 put。 也不要从一句“恢复初始排队状态”推导所有内存模式都立刻解除了全部页或 attachment 关联;应继续查看所选模式的释放函数。
正常取消的 buf_finish 与释放时的 buf_cleanup 也不同:前者结束一次准备/使用,后者撤销缓冲区存在期间的驱动附加资源。先结束使用,后撤销这些资源,再释放存储与管理对象。
10.3 核心 queue_release 不是 kfree(q)
voidvb2_core_queue_release(struct vb2_queue *q)
{
__vb2_cleanup_fileio(q);
__vb2_queue_cancel(q);
mutex_lock(&q->mmap_lock);
__vb2_queue_free(q, q->num_buffers);
mutex_unlock(&q->mmap_lock);
}
它先清理适用的 fileio,再取消队列,最后在 mmap_lock 下释放池。struct vb2_queue 本身没有在这里被 kfree();如果队列内嵌在设备对象中,外层设备存储由驱动自己的生命周期管理。
还有一项调用前提:这个函数返回 void,没有把内部 __vb2_queue_free() 的错误向上传出。关闭和移除路径必须保证准备等访问已经按锁协议排除,不能一边无序并发 prepare,一边希望这个 void 接口给出“所有资源必然回收”的失败报告。
与 REQBUFS(0) 不同,这条核心释放路径会先主动取消,因此不需要先通过 vb2_core_reqbufs() 的 streaming 前置检查。两者可以复用清理原语,但不是完全同一条入口。
实现对照:vb2_core_reqbufs()、__vb2_queue_free()、__vb2_free_mem()、vb2_core_queue_release()。
11. 为什么已经释放池,映射还可能存在
11.1 管理对象与后端存储不是同一个寿命
第五篇中的缓冲区管理对象持有各平面的后端对象。vmalloc 后端分配存储后,建立一份初始引用;成功 mmap 又通过 VMA 回调取得存储引用。
看一个简化例子:一个平面、一个映射、没有导出或其他额外引用。后端引用的变化为:
申请平面:1,队列持有
mmap: 2,队列与映射各持有一份
STREAMOFF:仍为 2,只取消本轮使用
REQBUFS(0) 成功:1,队列放弃持有
最后一个映射退出:0,后端才释放存储
该顺序用于解释引用,不是推荐在一般应用中先释放池再解除映射。本例允许这样观察,依赖实际支持 orphaned buffers 的能力及后端引用实现;跨驱动不能省略能力检查。
staticvoidvb2_vmalloc_put(void *buf_priv)
{
structvb2_vmalloc_buf *buf = buf_priv;
if (refcount_dec_and_test(&buf->refcount)) {
vfree(buf->vaddr);
kfree(buf);
}
}
所以,内存统计没有在 STREAMOFF 后立刻下降,本来就不奇怪。即使池已释放,仍有映射或导出持有,也不一定是泄漏。需要核对的是每一份引用最终有没有归还。

图 7:引用数字只针对示例中的一个平面。VMA 拆分、继承、共享导出等会增加其他持有关系。
11.2 旧映射不会自动绑定新池
释放旧池再重新申请,可以再次出现 index 0 和相同数值的映射偏移。它们是新池的身份。旧 VMA 仍指向旧存储,不会因为编号相同就自动重定向。
旧像素即使仍然可读,也不再具有当前新池的帧结果语义。重新建立池以后,应重新查询和映射,不能沿旧地址消费新索引对应的结果。
11.3 close(fd) 也不是结束全部持有关系的总开关
描述符、打开文件对象和后端存储各有自己的引用。dup、继承、正在进行的文件操作或仍存活的文件映射,可能让最后的文件 release 延后。DMA-BUF 导出对象还可能由其他设备持有;关闭一个导出 fd 也不应被无条件等同于所有对象引用消失。
应用已把某块图像交给另一个线程、编码器或 GPU 时,STREAMOFF 只能结束当前采集队列,不能替这些消费者完成工作。解除映射以前,必须按消费者的接口等待实际使用结束。这个等待是内存安全的一部分,不只是为了让退出日志看起来整齐。
实现对照:vb2_vmalloc_alloc()、vb2_vmalloc_put()、vb2_common_vm_open/close();通用文件引用补充对照文末 fs/file_table.c。
12. 最终关闭与设备断开:谁负责最后一次清理
12.1 正常 close 走 owner 分支,再退出 fh
int _vb2_fop_release(struct file *file, struct mutex *lock)
{
structvideo_device *vdev = video_devdata(file);
if (lock)
mutex_lock(lock);
if (file->private_data == vdev->queue->owner) {
vb2_queue_release(vdev->queue);
vdev->queue->owner = NULL;
}
if (lock)
mutex_unlock(lock);
return v4l2_fh_release(file);
}
外层 vb2_fop_release() 选择队列锁,没有队列锁时再选择节点锁。上面这个函数在适用的锁保护下比较 owner;匹配时释放队列、清 owner,然后解锁。最后,无论是否为 owner,都清理本次标准文件句柄。
因此,非 owner 关闭不会沿此分支释放别人的 VB2 队列。它仍会退出自己的 fh,并可能触发 fh 相关的媒体源钩子,不能据此扩大成“不会影响任何共享对象”。第三篇已经分析过 fh,这里只强调与队列清理的先后。
上层 v4l2_release() 最后归还节点引用;支持 Request 时,它还可能在驱动 release 外围持有请求串行化锁。队列锁没有一直包住整个 fh 清理,不能在流程图里把这些层次合成一把总锁。
12.2 程序被终止,不代表驱动可以省略停止实现
正常程序主动停流,便于报告错误并组织下游消费者退出。程序异常终止时,系统仍按文件和映射的生命周期撤销进程资源,在最后引用退出后触发相应 release。
但这不是不正确驱动的补救机制。驱动若在 release 路径仍让 DMA 访问已释放内存,进程退出不会自动修复它;设备卡死造成停止函数不能返回,也不能靠“让进程结束”证明内存已经安全回收。SIGKILL 不能执行应用的清理代码,其他进程继承或传递得到的持有关系也可能继续存在。
12.3 注销节点之后,不能再指望应用发 STREAMOFF
新进入的标准文件操作会检查节点注册状态。节点已经被注销,应用继续发 STREAMOFF 可能在外层得到 -ENODEV,到不了驱动停止回调。
因此,设备移除路径必须主动组织队列清理。VIMC 的 vimc_capture_unregister() 调用 vb2_video_unregister_device(),不是仅删除节点名称后就释放捕获对象。
这个辅助函数的关键顺序是:
检查节点是否仍注册
→ get_device:暂时持有节点对象
→ video_unregister_device:撤销节点访问
→ queue 存在且 owner 非空时:
选择并取得队列/节点锁
vb2_queue_release
owner = NULL
释放操作锁
→ put_device:归还临时节点引用
这里判断的是 queue && queue->owner,不是简单的 q->streaming。只有预排或提前准备、没有真正启动的 owner 队列,也可能需要清理。
还要保持调用组合正确:不要先手工 video_unregister_device(),再调用这个辅助函数企图补清理;后者开头看到节点已不注册便返回,可能跳过原本期待的队列处理。
额外的设备引用只防止这个函数在队列收尾前失去节点对象,不会自动延长所有外部硬件资源的寿命。其 owner 条件判断在取得队列锁之前,函数也不是任意 remove、close、自定义访问可以无序并发的完整证明;外层驱动仍要安排一致的移除和资源销毁协议。

图 8:两条路径都需要正确停止和释放队列,但入口条件、临时引用和节点状态不同;它们不是相互递归调用。
实现对照:_vb2_fop_release()、vb2_fop_release()、v4l2_release()、vimc_capture_unregister()、vb2_video_unregister_device()。
13. 写真实硬件停止回调时,必须守住哪些条件
VIMC 的顺序是一个清楚的软件实例:等线程退出,终止相关处理,归还剩余对象。真实硬件应保留同样的依赖关系,但不能逐句照抄。
首先,要阻止新的硬件任务继续提交。仅屏蔽中断不等于停止 DMA;中断不再通知,设备仍可能继续写内存。需要按硬件协议停止、等待空闲或复位,必要时采用能保证设备不再访问这组存储的恢复手段。
其次,要同步所有可能持有对象的完成路径。中断、线程化中断、tasklet、工作队列或软件线程,各自都有自己的同步方法。同步调用通常会等待正在执行的处理结束,因此不能持有被等待方还需要取得的锁。kthread_stop()、同步取消工作和 IRQ 同步也不能简单互换。
随后,归还已经接收的缓冲区。完成元数据和实际写入必须先结束,vb2_buffer_done() 之后不能继续用旧指针访问。驱动私有列表也要摘干净,防止后续残留任务再次完成同一个对象。
最后,才允许核心取消、释放池,以及驱动继续拆除它们依赖的硬件和上下文。若停止失败,不能仅靠强行把 ACTIVE 改成 ERROR 就恢复安全;stop 回调需要兑现“返回以后不再访问”的契约。
审查一个停止实现时,可以按下面几项逐一找证据,而不是只搜索有没有 stop_streaming:
这些是对当前调用关系得出的工程要求,不是宣称任意硬件都能使用同一段通用停止伪代码。
14. 用一轮可重复的实验检查停止与复用
14.1 不必故意制造设备故障,先观察合法的取消
配套程序 streamoff_probe.c 使用当前格式,支持单平面和多平面 CAPTURE。它不会设置格式,也不读取或修改像素;只建立 MMAP 池,观察状态和完成描述。
完整源码:streamoff_probe.c。
程序执行两个场景。第一轮只 QBUF、不调用 STREAMON,然后 STREAMOFF,查询每块是否回到未排队、未准备状态。第二轮复用同一池和映射,重新 QBUF、STREAMON,取得少量完成描述,再 STREAMOFF。最后解除映射,执行 REQBUFS(0),关闭节点。
这样能分别观察“从未实际运行的预排取消”和“运行后的停止与复用”。没有硬件测试环境时,不能用编译成功代替这两轮的实际结果。
cc -std=c11 -Wall -Wextra -Werror -O2 \
streamoff_probe.c -o streamoff_probe
./streamoff_probe /dev/video0
应在空闲、拓扑和格式已经配置正确的测试节点上执行。程序会真正尝试启动采集,REQBUFS 会改变队列状态。不支持相应路径的驱动可能返回错误,日志应作为定位起点,而不是强行认定框架有错。
14.2 清理条件要记录“可能已经改变状态”,不只记录成功 STREAMON
下面是配套程序所采用的收尾原则,省略了具体映射数组循环:
/* 原则示意;完整错误处理见 streamoff_probe.c。 */
if (queue_may_need_cancel)
ioctl(fd, VIDIOC_STREAMOFF, &type);
/* 没有其他线程或设备继续使用这些映射后,才解除。 */
unmap_all_successful_mappings();
if (pool_created)
ioctl(fd, VIDIOC_REQBUFS, &zero_count_request);
close(fd);
queue_may_need_cancel 在尝试 QBUF 之前就设置,而不是只在 STREAMON 成功后设置。准备失败、预排以及延后启动失败,都可能已经留下需要取消的状态。真正代码逐项检查错误,不会因为上一个清理失败就跳过所有其他持有关系的处理,也不会因 close 报错反复关闭一个可能已经释放并复用的描述符。
上述顺序适用于正常设备驱动履约的进程资源收尾。停流失败时,程序会记录错误;解除本进程映射和最终关闭不能被当成“应用已经证明故障硬件停止”的证据。
14.3 日志应该围绕一组可解释的状态
驱动侧可在停止前、stop 回调返回后、取消完成后分别记录:streaming、start_streaming_called、num_buffers、queued_count、owned_by_drv_count 及各 buffer 的状态。高频完成路径的日志应短时使用,避免日志开销改变时序。
这些观察点能直接区分“驱动没归还”“只清了框架状态”“池根本没有释放”和“后端还有映射引用”。不要只根据系统内存没有下降就定位为泄漏,也不要只根据 STREAMOFF 返回 0 就忽略后续晚到写入。
15. 把整个退出过程收束成一条依赖链
从第五篇申请到第九篇取回,缓冲区一直在不同执行方之间交接。第十篇完成的是反方向的收束:
停止继续提交与生产
→ 驱动结束实际访问,归还持有的对象
→ VB2 撤销提交与完成列表,唤醒等待者
→ 为所有已准备对象补齐收尾,恢复可重新提交状态
→ 应用退出接收与消费,决定保留还是释放池
→ 解除映射,放弃队列对存储的持有
→ 最后引用退出,后端存储与外层对象按各自职责释放
STREAMOFF 的重点是让一轮使用结束;REQBUFS(0) 的重点是让队列放弃这个池;最后一次存储引用退出,才决定内存真正被回收的时刻。
这三件事分清以后,正常暂停、启动失败、阻塞退出和设备断开就不会再被混成一次含糊的“释放缓冲区”。















































































