夜雨聆风学习资料网

ARTICLE · 1207431

V4L2 源码深度解析(十):先停访问,再还缓冲区——STREAMOFF 与异常退出

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。

这是一个用于推演的快照,不是固定的索引分工。四块存储都存在,但需要收尾的关系不一样:

缓冲区
停止前的使用情况
需要结束的关系
buffer 0
ACTIVE
,正在处理
等实际处理退出,不能只扫描驱动待处理列表。
buffer 1
ACTIVE
,驱动尚未使用
从驱动私有列表摘除,并归还给 VB2。
buffer 2
DONE
,尚未 DQBUF
取消未取走的完成结果。
buffer 3
DEQUEUED
,prepared=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 同一状态名,可能需要不同的收尾

取消前
取消阶段需要做什么
取消后
新建的 DEQUEUED,未准备
无内存 finish,无驱动 buf_finish
仍为 DEQUEUED。
PREPARE_BUF 后的 DEQUEUED
补内存 finish,按 prepared 调用 buf_finish
DEQUEUED,两个准备标志清除。
QUEUED,尚未交给驱动
撤销提交,补准备收尾
DEQUEUED。
ACTIVE
先等驱动停止并归还,再收尾
DEQUEUED。
DONE / ERROR,尚未 DQBUF
丢弃完成项;内存 finish 通常已做,补驱动 buf_finish
DEQUEUED。

这张表讨论一次遵守契约的取消。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 归还,其他两块没有额外并发变化:

观察时刻
queued_count
owned_by_drv_count
两条 VB2 列表
缓冲区池
停止前
3
2
提交:0、1、2;完成:2
4 块,存储与映射存在。
驱动 stop 返回
3
0
提交关系仍在;0、1 也已归还
仍是同一池。
核心取消完成
0
0
提交和完成列表均为空
4 块,全为 DEQUEUED,prepared/synced 为 0。
再次 QBUF、STREAMON
随新一轮变化
随新一轮变化
重新形成提交与完成关系
可复用原池和映射。

中间的完成顺序由实际执行决定,表中不要求固定为 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
优先级、owner 或 fileio 检查
可能根本没进入取消函数。
EINVAL
队列类型不匹配
不会因为参数错误顺手替其他类型停流。
ENODEV
节点状态检查
新请求被拒绝,驱动移除路径仍必须负责收尾。
信号导致的中断/重启
等待外层操作锁等位置
可能尚未取得锁,不可直接认定已经停止。
ENOTTY
缺失或不支持的操作入口
此入口不能完成预期的停流。
返回 0
核心取消走完
在驱动遵守契约的前提下,本轮访问与排队结束。

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:

检查项
应该在代码中找到的依据
不再产生新任务
停止门控与提交路径共享同一套状态/锁协议。
当前任务已经结束
硬件空闲、完成等待或软件线程退出的实际依据。
没有晚到完成
与中断和延后工作的同步,不遗留裸指针。
接收和归还配对
正常与失败分支覆盖每个 ACTIVE 对象。
不在持锁等待中死锁
被等待方不依赖调用方仍持有的锁。
对象寿命覆盖清理
回调、控制对象和内存关联在最后一次使用前仍有效。

这些是对当前调用关系得出的工程要求,不是宣称任意硬件都能使用同一段通用停止伪代码。

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 就忽略后续晚到写入。

现象
首先核对
STREAMOFF 卡住
stop 回调里等待谁,被等待方需要什么锁,硬件是否还会推进完成。
出现遗留 ACTIVE 告警
驱动接收/归还是否配对,私有列表外是否有在途对象。
停止后还有写内存
中断或工作是否未同步,屏蔽通知是否被误当成停止实际访问。
DQBUF 因停止返回 EINVAL
是否为当前实现预期的等待复查结果,而不是缓冲区描述损坏。
REQBUFS(0) 返回 EBUSY
是否仍 streaming,或在外层发生 owner/fileio 冲突。
再次 STREAMON 没有图像
是否重新 QBUF 并满足启动数量,不能复用已取消的旧排队关系。
释放池后仍占内存
查询映射、导出、其他文件/存储引用是否已经全部结束。

15. 把整个退出过程收束成一条依赖链

从第五篇申请到第九篇取回,缓冲区一直在不同执行方之间交接。第十篇完成的是反方向的收束:

停止继续提交与生产
  → 驱动结束实际访问,归还持有的对象
  → VB2 撤销提交与完成列表,唤醒等待者
  → 为所有已准备对象补齐收尾,恢复可重新提交状态
  → 应用退出接收与消费,决定保留还是释放池
  → 解除映射,放弃队列对存储的持有
  → 最后引用退出,后端存储与外层对象按各自职责释放

STREAMOFF 的重点是让一轮使用结束;REQBUFS(0) 的重点是让队列放弃这个池;最后一次存储引用退出,才决定内存真正被回收的时刻。

这三件事分清以后,正常暂停、启动失败、阻塞退出和设备断开就不会再被混成一次含糊的“释放缓冲区”。

相关学习资料