ARTICLE · 1136863
V4L2 源码深度解析(八):从预排缓冲区到第一帧——STREAMON 怎样启动采集?
上一节结束时,应用已经把缓冲区交给 VB2。沿用同一个例子:VIMC 的采集格式是 640 × 480 RGB24,四块 MMAP 缓冲区已经申请并完成映射。应用先提交 buffer 0 和 buffer 1,另外两块暂时没有提交。
此时,内存已经准备好,VB2 也知道本轮可以使用哪两块,但图像还没有开始写入。还缺少一次启动请求:
int type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
int ret = ioctl(fd, VIDIOC_STREAMON, &type);
这次调用不再分配四块图像内存。它要做的是判断启动条件是否具备,把预排对象送到驱动,准备数据链路,再让实际处理路径运行起来。
最容易读错的地方是“启动”二字:STREAMON 成功、进入驱动的启动回调、处理线程开始运行、第一帧完成,并不是同一个时刻。本篇沿着这些时刻往下读,直到 VIMC 真正取得一块缓冲区并写入图像。完成队列和 DQBUF 的内部实现留到下一篇。
主线采用普通 CAPTURE、MMAP、vmalloc 后端,不使用 Media Request;实例代码使用 min_buffers_needed、streaming 和 start_streaming_called 这些字段。具体分支以正文展示的实现为准,不将其他版本的字段或调用顺序混入。
1. 缓冲区已经 QBUF,为什么还要 STREAMON?
1.1 提交存储和允许开始处理,是两个决定
QBUF 表达的是“这块缓冲区本轮可以交给采集流程”。应用可以先把几块缓冲区准备齐,再统一请求启动。这样,驱动启动以后就有可用存储,不必刚开始运行便等待应用交出第一块内存。
STREAMON 表达的是“这条队列可以开始运行”。它针对的是队列,不是某个 buffer 的索引。因此参数中只有队列类型,没有宽高、存储地址或缓冲区编号。
对当前 VIMC 实现,尚未调用 STREAMON 时,预排的 buffer 0、buffer 1 留在 VB2 的 queued_list 中。驱动自己的 buf_list 仍然为空。它们已有 QUEUED 状态,但还没有经过 buf_queue 回调移交给驱动。
1.2 为什么启动前还要数缓冲区?
连续处理通常需要不止一个可用位置。一个位置正在写入时,后续帧还需要其他位置接续。真实硬件可能要求预先准备足够的地址描述符;软件驱动也可以声明自己的启动要求。这只是门槛的用途,不表示所有设备都需要相同数量,更不表示配置为 2 就一定对应某种固定的“双缓冲 DMA”硬件。
VIMC 在队列初始化时设置:
q->min_buffers_needed = 2;
于是有两个不同的数量问题:池里是否已经建立了至少两块缓冲区;应用是否已经把至少两块交给本轮处理。申请四块,只提交一块,前一个条件成立,后一个条件还不成立。
1.3 启动并不等待第一帧拍完
驱动的启动回调负责建立运行条件。它可以配置硬件,也可以像 VIMC 一样创建软件线程。启动成功并不要求等待第一帧完成才返回。
反过来,新线程一旦被唤醒,就可能很快得到调度;第一帧也可能在 STREAMON 的系统调用返回之前完成。因此,不能根据打印日志的顺序认定“先返回应用,再开始产生图像”。

图 1:每一步回答不同的问题。框架接受流请求,不等于已经形成可供应用取回的一帧。
理解这三个动作以后,再看源码中的数量判断和两个启动标志,就不需要把每个名字都解释成同一种“正在采集”。
实现对照:vimc_capture_add()、vb2_core_qbuf()、vb2_core_streamon()、vb2_start_streaming()。
2. 一条 STREAMON 命令怎样进入 VB2
2.1 传入的是类型变量的地址
VIDIOC_STREAMON 的参数按接口约定是一个类型值的地址。普通单平面采集使用 V4L2_BUF_TYPE_VIDEO_CAPTURE;多平面采集则使用相应的 _MPLANE 类型。它必须与当前队列的类型一致。
标准 ioctl 参数层把这个整数复制到内核临时对象以后,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);
}
这里的 arg 已经指向本次调用的内核参数,而下一级回调接收的是整数值。不能把应用的调用写成 ioctl(fd, VIDIOC_STREAMON, type),把数值当作地址传进去。
VIMC 通过操作表建立以下连接:
.vidioc_streamon = vb2_ioctl_streamon,
调用关系从这里接入 VB2:
video_ioctl2 → video_usercopy → __video_do_ioctl
→ v4l_streamon
→ vb2_ioctl_streamon
→ vb2_streamon
→ vb2_core_streamon
前面几篇已经展开过标准分发,本篇只保留与启动有关的入口。VIDIOC_STREAMON 的命令表项带有 INFO_FL_PRIO | INFO_FL_QUEUE,所以优先级检查和队列锁选择可能在抵达 VB2 之前发生。
2.2 接口层先检查当前打开上下文
intvb2_ioctl_streamon(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_streamon(vdev->queue, i);
}
video_devdata(file) 找到节点,节点中的 queue 指向已经初始化的 VB2 队列。vb2_queue_is_busy() 检查的是队列是否由另一个打开上下文占用,而不是“只要队列有缓冲区就拒绝启动”。
当前上下文本来就是队列 owner 时,检查通过。多个描述符共用同一个打开上下文的情况,与多个独立 open 的情况不同;这里不再重复文件句柄的构造过程。
2.3 再检查是否已有 fileio 路径占用
intvb2_streamon(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_streamon(q, type);
}
vb2_streamon() 是 V4L2 适配入口。它先避免显式流式接口与正在使用队列的 read/write 辅助路径混用,随后进入通用核心。
因此,最后看到 EBUSY 时,不能立刻断言是驱动启动硬件失败。请求可能在标准分发的优先级检查、owner 检查或 fileio 检查时就已经被拒绝。

图 2:布局和存储分配不在这条主调用链中。真正的启动工作由核心在条件满足时继续发起。
3. vb2_core_streamon:先分清池大小与预排数量
3.1 完整入口实现
intvb2_core_streamon(struct vb2_queue *q, unsignedint type)
{
int ret;
if (type != q->type) {
dprintk(q, 1, "invalid stream type\n");
return -EINVAL;
}
if (q->streaming) {
dprintk(q, 3, "already streaming\n");
return0;
}
if (!q->num_buffers) {
dprintk(q, 1, "no buffers have been allocated\n");
return -EINVAL;
}
if (q->num_buffers < q->min_buffers_needed) {
dprintk(q, 1, "need at least %u allocated buffers\n",
q->min_buffers_needed);
return -EINVAL;
}
/*
* Tell driver to start streaming provided sufficient buffers
* are available.
*/
if (q->queued_count >= q->min_buffers_needed) {
ret = v4l_vb2q_enable_media_source(q);
if (ret)
return ret;
ret = vb2_start_streaming(q);
if (ret)
return ret;
}
q->streaming = 1;
dprintk(q, 3, "successful\n");
return0;
}
函数不长,但判断顺序决定了几个很容易误解的结果。可以把前半段理解为“这条队列能否接受流请求”,后半段理解为“现在就启动,还是等后续提交补齐条件”。
3.2 类型错误与重复启动,在最前面处理
第一个检查是 type != q->type,不匹配直接返回 -EINVAL。
接着检查 q->streaming。已经为 1 时,核心直接返回成功,不重复移交缓冲区,也不再次执行驱动 start_streaming()。这不是一次重置操作,不会重新清零 VIMC 的帧序号。
这个提前返回还意味着:如果此前因预排数量不足而只记录了流请求,再发一次相同 STREAMON,不会越过这个分支去强制启动。正常推进来自后续满足条件的 QBUF,而不是重复发启动命令。
3.3 分配数量不足,是错误;预排数量不足,可以等待
q->num_buffers 是当前池中的对象数量。池为空,或小于驱动声明的下限,函数返回 -EINVAL。通常 REQBUFS 已经帮助满足最低分配要求,但启动入口仍保留检查,不能因为正常申请路径会调整数量就忽略它。
q->queued_count 则表示已提交、尚未完成出队的对象数量。在本篇尚未启动的状态里,它就是当前预排数量。只有它达到下限,才立即进入媒体源钩子和实际启动辅助函数。
以 min_buffers_needed = 2 为例:
-EINVAL | ||
-EINVAL | ||
streaming = 1,不立即调用驱动启动回调。 | ||
streaming 设为 1。 |
表中假定类型正确、原先未启动,其他外层检查也已通过。
最低数量为零是另一种配置。此时,只要池非空,queued_count >= 0 就成立,即使尚未预排也会尝试驱动启动。不能把 VIMC 的“至少两块”推广成 VB2 对所有驱动的固定要求。
3.4 为什么成功返回后还可能没有图像?
若池中有四块,但只 QBUF 一块,代码会跳过整个实际启动分支,直接执行:
q->streaming = 1;
return0;
这说明核心接受了流请求,后续提交应按这个请求推进。它并不表示 start_streaming() 已经执行,更不表示设备已写过一帧。

图 3:分配数量和预排数量不是同一次检查。图中“返回 0”有立即启动和仅记录请求两种来源。
3.5 媒体源钩子不是统一的硬件启动器
数量足够时,核心先调用 v4l_vb2q_enable_media_source(q)。该辅助函数尝试从 q->owner 取得标准句柄,再进入 v4l_enable_media_source()。后者在存在相应媒体设备及 enable_source 钩子时执行源选择或准备;没有对应钩子时可直接成功。
它不遍历所有 Sensor 并统一调用 .s_stream(1),也不是“打开 DMA”的固定入口。后面的 VIMC 启动回调还会明确建立媒体管线、准备各子设备和线程。应按实际调用分别分析这些步骤。
此外,vb2_core_streamon() 本身没有检查并清除 q->error。不能因为它返回成功,就宣称队列历史错误已被自动复位。错误恢复仍要按停止和取消路径处理。
实现对照:vb2_core_streamon();v4l2-mc.c 中的 v4l_vb2q_enable_media_source()、v4l_enable_media_source()。
4. 真正进入驱动之前,先把缓冲区交过去
4.1 vb2_start_streaming 的前半段
list_for_each_entry(vb, &q->queued_list, queued_entry)
__enqueue_in_driver(vb);
/* Tell the driver to start streaming */
q->start_streaming_called = 1;
ret = call_qop(q, start_streaming, q,
atomic_read(&q->owned_by_drv_count));
if (!ret)
return0;
这是 vb2_start_streaming() 的启动部分,后面的失败回收在第 11 节展开。
执行顺序很明确:先遍历 queued_list,逐个调用 __enqueue_in_driver();完成这轮移交后,才设置 start_streaming_called 并调用驱动启动回调。
因此,不应画成“驱动 start_streaming 成功以后,VB2 才第一次调用 buf_queue”。驱动需要先拿到预排对象,才能组织初始地址描述符或软件待处理列表。
4.2 ACTIVE 和驱动持有数先于回调更新
staticvoid __enqueue_in_driver(struct vb2_buffer *vb)
{
structvb2_queue *q = vb->vb2_queue;
vb->state = VB2_BUF_STATE_ACTIVE;
atomic_inc(&q->owned_by_drv_count);
trace_vb2_buf_queue(q, vb);
call_void_vb_qop(vb, buf_queue, vb);
}
这个函数先把对象设为 ACTIVE,再增加 owned_by_drv_count,最后调用 buf_queue。这样,当驱动接收对象时,核心已经记录“这个对象归驱动负责”。
某个驱动甚至可以在回调内很快报告完成。完成入口需要看到合法的 ACTIVE 状态,所以不能把状态更新移到回调之后。
ACTIVE 只表示驱动已经接收对象。它可能先把对象放进自己的队列等待处理,而不是立刻让每一块同时参与 DMA。当前 VIMC 就采用这种组织方式。
4.3 VIMC 先把对象挂入自己的列表
staticvoidvimc_capture_buf_queue(struct vb2_buffer *vb2_buf)
{
structvimc_capture_device *vcapture = vb2_get_drv_priv(vb2_buf->vb2_queue);
structvimc_capture_buffer *buf = container_of(vb2_buf,
structvimc_capture_buffer,
vb2.vb2_buf);
spin_lock(&vcapture->qlock);
list_add_tail(&buf->list, &vcapture->buf_list);
spin_unlock(&vcapture->qlock);
}
vb2_get_drv_priv() 找到捕获设备;container_of() 从通用缓冲区找回驱动扩展对象。短时持有 qlock 后,驱动把它尾插进 vcapture->buf_list。
这个列表与 VB2 的 queued_list 使用不同的链表成员。移交驱动时,VB2 不会因此从 queued_list 摘除对象。前者记录驱动等待处理的对象,后者继续记录已经提交、尚未完成出队的对象。
对于 buffer 0、buffer 1,驱动还未消费时的快照为:
VB2 queued_list:0 → 1
VIMC buf_list: 0 → 1
两块状态: ACTIVE
驱动持有数量: 2
这里没有像素复制。只是在两套管理关系中登记同一组对象。
4.4 start_streaming 的 count 参数从哪里来?
调用表达式读取的是:
atomic_read(&q->owned_by_drv_count)
这是调用时尚由驱动负责的对象数量,不是 q->num_buffers,也不是应用第一次 REQBUFS 的请求值。VIMC 在这个启动函数里没有使用 count 参数进一步分支,但接口仍把这个信息提供给驱动。
在当前预排两块、驱动尚未消费的场景中,count 为 2。对其他驱动,如果移交过程中已经归还过对象,不能仍然机械地把参数写成预排总数。

图 4:状态和计数是一个明确时刻的快照。处理线程运行以后,驱动持有数不必保持为 2。
5. 两个启动标志,记录的是不同阶段
5.1 为什么不能只看 streaming?
在首次立即启动的路径里,vb2_core_streamon() 要等 vb2_start_streaming() 返回成功后,才设置 q->streaming = 1。
但 vb2_start_streaming() 在调用驱动之前,已经设置 q->start_streaming_called = 1。所以首次立即进入驱动启动回调时,两个值可以是:
streaming = 0
start_streaming_called = 1
这不是初始化遗漏。前一个标志还没有走到本次入口的成功提交位置;后一个标志已经标记了正在尝试执行的驱动启动阶段。
start_streaming_called == 1 在回调执行期间也成立,不能直接翻译成“驱动已经成功启动”。失败时辅助函数会将它清零。
5.2 先记录请求,再由下一次 QBUF 触发
预排一块时调用 STREAMON,核心成功返回,但状态是 streaming = 1、start_streaming_called = 0。随后提交第二块,QBUF 在排队后检查:
if (q->streaming && !q->start_streaming_called &&
q->queued_count >= q->min_buffers_needed) {
ret = vb2_start_streaming(q);
if (ret) {
/*
* Since vb2_core_qbuf will return with an error,
* we should return it to state DEQUEUED since
* the error indicates that the buffer wasn't queued.
*/
list_del(&vb->queued_entry);
q->queued_count--;
vb->state = orig_state;
return ret;
}
}
此时调用驱动回调时,streaming 已经是 1。因此,立即启动与延后启动在回调入口处的这个字段并不相同。驱动不应依赖“进入我的 start_streaming 时 streaming 必定为 1”来决定是否准备资源。

图 5:立即路径由 STREAMON 触发,延后路径由后续 QBUF 触发。两者最终都使用 vb2_start_streaming。
5.3 延后路径不能补画不存在的调用
这份 QBUF 分支直接调用 vb2_start_streaming(),没有再执行 v4l_vb2q_enable_media_source()。前面的 STREAMON 数量不足时又跳过了媒体源调用,所以两条入口在这一点上确实不同。
应将它作为当前控制流的事实保留,而不是为了让总图整齐就给延后分支加上不存在的钩子。特定设备是否依赖这层源准备、是否在其他地方完成相关操作,需要继续看那个驱动,不能从 VIMC 推广到全部设备。
本文的正常采集建议先提交足够的缓冲区再 STREAMON。延后路径主要用于解释框架行为与排障,不作为所有设备都适用的启动习惯。
6. 进入 VIMC:启动回调先准备什么?
6.1 完整驱动回调
staticintvimc_capture_start_streaming(struct vb2_queue *vq, unsignedint count)
{
structvimc_capture_device *vcapture = vb2_get_drv_priv(vq);
int ret;
vcapture->sequence = 0;
/* Start the media pipeline */
ret = video_device_pipeline_start(&vcapture->vdev, &vcapture->stream.pipe);
if (ret) {
vimc_capture_return_all_buffers(vcapture, VB2_BUF_STATE_QUEUED);
return ret;
}
ret = vimc_streamer_s_stream(&vcapture->stream, &vcapture->ved, 1);
if (ret) {
video_device_pipeline_stop(&vcapture->vdev);
vimc_capture_return_all_buffers(vcapture, VB2_BUF_STATE_QUEUED);
return ret;
}
return0;
}
第一步重置 vcapture->sequence。这发生在每次实际进入这个回调时,不是每次应用重复发送 STREAMON 都会发生。即使后续启动失败,这次赋值也已经执行;下一次实际重试又会从这里开始。
随后有两项不同的准备:video_device_pipeline_start() 建立并检查媒体管线;vimc_streamer_s_stream() 建立 VIMC 的执行顺序、启用子设备,再创建线程。第一项成功不代表第二项必然成功。
6.2 媒体管线为什么需要单独检查?
采集节点通常只是数据链路末端。以一条有效的 VIMC 路径为例,数据可以经过 Sensor A、Debayer A、Scaler,最后到达 RGB/YUV Capture。
媒体框架用 entity 表示模块,用 pad 表示输入或输出端口,用 link 表示连接。启动时要确认相关连接和格式能组成可用链路,而不是只确认 capture 自己的四块内存存在。
video_device_pipeline_start() 要求当前节点实体有一个 pad,然后进入 media_pipeline_start()。媒体核心在 graph_mutex 保护下建立管线关联并进行相应校验,包括已启用链接的 link_validate 和必须连接端口的检查。首次建立失败时,核心回滚已经设置的关联。
VIMC 捕获节点的链接校验会比较上游与当前节点的宽、高、像素格式,并按相应规则检查 field 与颜色信息。因而缓冲区申请成功、QBUF 成功,启动仍然可能因为链路格式不匹配返回 -EPIPE。
本篇延续的 640 × 480 RGB24 只是捕获侧的配置;并不保证任意默认媒体链路都恰好与它匹配。实际实验要先将启用链路上的格式配置一致。
6.3 管线登记不等于启动图像引擎
media_pipeline_start() 主要处理媒体图中的关联、占用和校验。它不会替 VIMC 创建采集线程,也不自动调用所有子设备的 .s_stream(1)。
这正是驱动还要执行 vimc_streamer_s_stream() 的原因。“媒体管线已登记”与“数据处理已开始”必须分开;否则排查无图问题时,会停在错误的成功点上。
实现对照:video_device_pipeline_start()、media_pipeline_start()、__media_pipeline_start()、vimc_vdev_link_validate()。
7. vimc_stream:把图中的连接变成线程的调用顺序
7.1 先认识线程要使用的对象
structvimc_stream {
structmedia_pipelinepipe;
structvimc_ent_device *ved_pipeline[VIMC_STREAMER_PIPELINE_MAX_SIZE];
unsignedint pipe_size;
structtask_struct *kthread;
};
pipe 是媒体核心使用的管线对象;ved_pipeline[] 是 VIMC 为软件处理建立的实体数组。数组有 pipe_size 个有效成员,kthread 保存线程对象指针。两种“pipeline”分别服务于媒体图管理和实际函数调用,不能合并理解。
本实现数组容量为 16。驱动不会因此允许无限深的处理链;超过上界还不能到达源端时,会终止已经准备的部分并返回错误。
每个数组成员是 struct vimc_ent_device *。这个对象带有 process_frame 函数指针,用于执行该模块的一次软件图像处理。节点建立时已经填写这些回调,不是在 STREAMON 中临时根据字符串找函数。
7.2 找上游时,只沿当前实例支持的路径走
static struct media_entity *vimc_get_source_entity(struct media_entity *ent)
{
structmedia_pad *pad;
int i;
for (i = 0; i < ent->num_pads; i++) {
if (ent->pads[i].flags & MEDIA_PAD_FL_SOURCE)
continue;
pad = media_pad_remote_pad_first(&ent->pads[i]);
return pad ? pad->entity : NULL;
}
returnNULL;
}
函数跳过 SOURCE pad,在第一个非 SOURCE pad 处查找已启用连接的对端;在当前 VIMC 中,这就是寻找输入端的上游。media_pad_remote_pad_first() 返回第一条匹配的已启用数据链接,并不在这里完成多输入选择或唯一性证明。
因此,这个 helper 不是任意复杂媒体图的通用调度器。VIMC 依据自己的实体布局和链路配置,形成一条可执行的上游路径。多输入合成、复杂路由和独立硬件流水线仍要按各自驱动处理。
7.3 一边记录实体,一边准备子设备
vimc_streamer_pipeline_init() 从发起启动的 Capture 开始。每轮先把当前 ved 放入数组,增加 pipe_size;若它是 V4L2 子设备,就调用 .video.s_stream(1);然后再查找上游,继续循环。
关键的记录与启用顺序如下:
stream->ved_pipeline[stream->pipe_size++] = ved;
if (is_media_entity_v4l2_subdev(ved->ent)) {
sd = media_entity_to_v4l2_subdev(ved->ent);
ret = v4l2_subdev_call(sd, video, s_stream, 1);
if (ret && ret != -ENOIOCTLCMD) {
dev_err(ved->dev, "subdev_call error %s\n",
ved->ent->name);
vimc_streamer_pipeline_terminate(stream);
return ret;
}
}
-ENOIOCTLCMD 在这里作为没有实现该子设备操作的情况被容忍,其他非零结果触发回退。但被容忍不表示凭空完成了设备准备;可运行所需资源仍必须由具体实体正确提供。
在这条示意链上,数组按以下顺序形成:
[0] RGB/YUV Capture
[1] Scaler
[2] Debayer A
[3] Sensor A
Capture 是视频节点,不走子设备的 s_stream 分支。子设备准备顺序因此是 Scaler、Debayer A、Sensor A,从下游向上游进行。
7.4 到了“没有上游”的位置,也要确认它真的是源
找不到上游不一定代表成功。代码继续调用 vimc_is_source(),检查当前实体是否没有 SINK 端口。在本例的有效实体定义中,这对应源端。若当前仍有输入端却没有连接到源,返回 -EPIPE,并终止已记录部分。
所以“没有找到下一项,就算建链成功”不是这个函数的规则。找到合法源端才结束;空指针、子设备准备失败和链过长都有各自的退出条件。

图 6:建表从 Capture 追向源端;线程反向遍历数组,才得到源到 Capture 的数据处理顺序。图中选定路径需先配置为有效且格式匹配。
7.5 为什么申请缓冲区成功后,启动还可能内存不足?
第 5 篇分配的是 Capture 的缓冲区池。VIMC 的软件 Sensor、Debayer、Scaler 在各自 .s_stream(1) 中,还可能申请用于生成或处理图像的中间存储。
这些中间帧不是那四块应用 MMAP 缓冲区。它们具有不同的分配时机与所有者,因此启动阶段仍可能返回 -ENOMEM。不能根据 REQBUFS 已成功,就排除所有启动期的存储分配失败。
本例也说明,MMAP 只减少了应用访问 Capture 存储时的一次额外整帧复制,不证明整个软件处理链没有中间存储或像素复制。
实现对照:vimc_streamer_pipeline_init()、vimc_get_source_entity()、vimc_is_source();Sensor、Debayer、Scaler 的 s_stream 实现。
8. kthread_run:从同步调用走到并发执行
8.1 先建好路径,再创建并唤醒线程
下面是 vimc_streamer_s_stream() 的启动分支。参数校验已在分支前完成;停止分支在第 11 节说明其配对作用。
if (enable) {
if (stream->kthread)
return0;
ret = vimc_streamer_pipeline_init(stream, ved);
if (ret)
return ret;
stream->kthread = kthread_run(vimc_streamer_thread, stream,
"vimc-streamer thread");
if (IS_ERR(stream->kthread)) {
ret = PTR_ERR(stream->kthread);
dev_err(ved->dev, "kthread_run failed with %d\n", ret);
vimc_streamer_pipeline_terminate(stream);
stream->kthread = NULL;
return ret;
}
}
已有 stream->kthread 时直接返回 0,是 streamer 自己的重复启动保护。正常首次启动先完成 pipeline_init,然后调用 kthread_run()。
kthread_run() 是创建内核线程并唤醒它的组合辅助接口,不等于在当前调用栈内执行完 vimc_streamer_thread()。返回成功时保存的是线程对象指针,失败则是错误指针,VIMC 用 IS_ERR() 和 PTR_ERR() 处理。
8.2 线程不需要等 ioctl 返回以后才运行
线程被唤醒以后可能与当前调用方并行。它可以在 vimc_streamer_s_stream() 返回之前执行,也可以在 STREAMON 已经返回应用后才获得调度。
因此,准备工作必须放在唤醒之前:实体数组、pipe_size、子设备的帧存储、Capture 的缓冲区列表,都不能等 kthread_run() 成功后才补齐。
VIMC 的启动顺序正是先接收预排缓冲区、再准备子设备、最后运行线程。线程执行函数取得的是已准备好的 stream,不需要等赋值表达式把返回指针存入 stream->kthread,才开始处理图像。

图 7:两条路径在唤醒后并发。第一帧完成与系统调用返回之间没有固定的先后保证。
8.3 驱动状态和框架标志不是硬件完成通知
首次立即启动时,处理线程可能已经写完第一帧,而 vb2_core_streamon() 还没有执行最后的 streaming = 1。这与源码顺序并不矛盾:驱动已经收到 ACTIVE 对象,start_streaming_called 也已设置,完成入口可以处理它们。
反过来,STREAMON 返回时两个标志都为 1,也只能说明已经走过相应软件启动路径。实际处理没有进展、没有可用缓冲区或上游处理失败,仍可能导致没有新帧交付。
应用最终应以完成出队的结果判断本轮图像是否取得,不能把一次启动成功作为无限期有效的“设备一直在正常出图”的证明。
接口核对:Linux v6.1 的 include/linux/kthread.h 中 kthread_run;执行顺序以这里展示的 VIMC 函数为准。
9. 第一帧由谁真正写入?
9.1 线程按源到接收端的顺序处理
staticintvimc_streamer_thread(void *data)
{
structvimc_stream *stream = data;
u8 *frame = NULL;
int i;
set_freezable();
for (;;) {
try_to_freeze();
if (kthread_should_stop())
break;
for (i = stream->pipe_size - 1; i >= 0; i--) {
frame = stream->ved_pipeline[i]->process_frame(
stream->ved_pipeline[i], frame);
if (!frame || IS_ERR(frame))
break;
}
//wait for 60hz
set_current_state(TASK_UNINTERRUPTIBLE);
schedule_timeout(HZ / 60);
}
return0;
}
set_freezable() 与循环里的 try_to_freeze() 使线程配合系统冻结流程。每轮先检查是否应停止,再从 pipe_size - 1 倒着遍历数组,逐个调用 process_frame()。
前面数组是 Capture、Scaler、Debayer、Sensor,这里便按 Sensor、Debayer、Scaler、Capture 执行。每个阶段的返回指针成为下一阶段的输入。
frame 在循环外初始化;不能把实现描述成每轮都会显式重新赋值为 NULL。在本例正常路径中,源模块生成自己的图像,不依赖上一轮传来的输入,Capture 处理完成后又返回 NULL。
9.2 NULL 或错误指针只结束本轮处理
内层循环中,!frame || IS_ERR(frame) 会 break。它跳出的是本轮实体遍历,不是最外层无限循环。
Capture 成功后本来就返回 NULL,因为它是数据的接收端;这个 NULL 不意味着启动失败。Capture 没有可用缓冲区时返回 ERR_PTR(-EAGAIN),则表示这轮没有可填位置,线程随后还会进入下一轮。
这里没有把所有 process_frame() 错误转换成 vb2_queue_error(),也没有把运行期错误送回早已返回的 STREAMON。因而“启动成功但没有帧”的排障,要继续检查处理循环,而不是只检查启动回调的返回值。
9.3 源码中的“60hz”注释不能当作精确帧率承诺
本实现每轮处理后执行:
set_current_state(TASK_UNINTERRUPTIBLE);
schedule_timeout(HZ / 60);
HZ 是内核时钟节拍频率,不是摄像头帧率。HZ / 60 先进行整数除法,等待的节拍数还受配置影响;图像处理时间和线程调度延迟也位于实际帧间隔中。
例如假设 HZ = 250,表达式得到 4 个节拍,对应名义上约 16 毫秒的等待量,再加处理与调度开销。它既不构成精确的 60 帧定时器,也不能用于证明第一帧必须等满 1/60 秒才出现:第一次处理在这次等待之前执行。
9.4 Capture 取出对象以后才写入像素
vimc_capture_process_frame() 在 qlock 下取出私有列表首项并摘链,然后释放锁。真正的复制在解锁之后进行:
vimc_buf->vb2.vb2_buf.timestamp = ktime_get_ns();
vimc_buf->vb2.sequence = vcapture->sequence++;
vimc_buf->vb2.field = vcapture->format.field;
vbuf = vb2_plane_vaddr(&vimc_buf->vb2.vb2_buf, 0);
memcpy(vbuf, frame, vcapture->format.sizeimage);
/* Set it as ready */
vb2_set_plane_payload(&vimc_buf->vb2.vb2_buf, 0,
vcapture->format.sizeimage);
vb2_buffer_done(&vimc_buf->vb2.vb2_buf, VB2_BUF_STATE_DONE);
顺序是填写时间戳、序号和 field,取得存储地址,复制像素,设置有效数据量,最后通知 VB2 完成。
时间戳使用这里调用 ktime_get_ns() 的时刻,不是模拟源曝光开始的硬件时间戳。序号由 Capture 自己增加,也不是缓冲区索引。
这一帧的实际像素写入来自 memcpy(),不是 DMA 中断。真实设备通常以硬件描述符和中断完成接入相同的缓冲区交接接口,但不能在 VIMC 的调用图上凭空加一条 DMA 路径。
完成通知如何加入完成队列、怎样唤醒 DQBUF,下一篇再深入。本篇到这里已经回答了“处理路径由谁启动、谁实际写入第一帧”。
10. 把启动前后的状态放在同一张记录表里
仍以四块缓冲区、预排 0 和 1、最低数量为 2 为例。下面是人为选定的阶段快照,假设观察到第一帧之前没有其他并发提交或出队;不能把表中每一项当成任意时刻打印日志都会稳定看到的值。
第三行到第四行之间,线程实际上可能已经完成第一帧,因此最后两行只是为了分开观察而作的调度假设。另一个合法快照是 streaming = 0、start_streaming_called = 1,但 buffer 0 已经 DONE。
queued_count 仍为 2,是因为完成通知不等于应用出队。owned_by_drv_count 才在驱动归还对象时递减。驱动列表里已经取走的对象,在复制期间仍然属于 ACTIVE,也仍计入驱动持有数。
对延后启动,进入回调时 streaming 已经为 1,其余移交动作仍由相同辅助函数完成。排查“STREAMON 已成功却没有进入驱动”时,最先记录的应是预排数量和两个标志,而不是只打印 num_buffers。
实现对照:vb2_start_streaming()、__enqueue_in_driver()、vimc_capture_process_frame()、vb2_buffer_done()。
11. 启动失败,不能把所有东西都叫作“恢复初始状态”
11.1 子设备准备失败,streamer 先退回已经记录的路径
vimc_streamer_pipeline_init() 把实体加入数组以后才调用它的 s_stream(1)。因此,失败实体本身也已经计入 pipe_size。
回退函数倒着处理已经记录的数组:
staticvoidvimc_streamer_pipeline_terminate(struct vimc_stream *stream)
{
structvimc_ent_device *ved;
structv4l2_subdev *sd;
while (stream->pipe_size) {
stream->pipe_size--;
ved = stream->ved_pipeline[stream->pipe_size];
stream->ved_pipeline[stream->pipe_size] = NULL;
if (!is_media_entity_v4l2_subdev(ved->ent))
continue;
sd = media_entity_to_v4l2_subdev(ved->ent);
v4l2_subdev_call(sd, video, s_stream, 0);
}
}
不是子设备的 Capture 只从数组退出;子设备会收到 s_stream(0)。在失败位置调用停止,要求具体实现能够处理部分初始化状态。函数没有检查这些停止回调的返回值,所以这里只能确认发出了回退调用,不能把它推广成对任意错误驱动都保证资源清理成功。
如果实体准备都完成,但线程创建失败,vimc_streamer_s_stream() 同样先终止实体路径,并把错误指针清为 NULL,随后向 Capture 返回错误。
11.2 Capture 再收回自己已经取得的资源
媒体管线建立本身失败时,Capture 只归还已经接收的缓冲区,不为一个未成功取得的管线重复执行 stop。
媒体管线已成功、streamer 随后失败时,Capture 执行 video_device_pipeline_stop(),再用 vimc_capture_return_all_buffers(..., QUEUED) 归还私有列表中的对象。
这两种分支正好对应第 6 节完整回调中的两个错误出口。实际资源只在已经成功取得后配对释放,不能无条件堆一串 stop/free。
11.3 为什么必须按 QUEUED 归还?
启动失败并没有产生正常图像结果。此时 vb2_buffer_done(vb, VB2_BUF_STATE_QUEUED) 撤销驱动对对象的责任,使它回到 VB2 的预排安排,而不是把一帧失败图像塞进完成队列。
QUEUED 分支不会加入 done_list,也不执行正常 DONE/ERROR 完成时的内存 finish;准备标志可能继续保留。不能把它等同于 DQBUF,也不能向应用报告“缓冲区已经完整取回”。
vb2_start_streaming() 的失败部分会再次检查驱动是否归还干净:
q->start_streaming_called = 0;
dprintk(q, 1, "driver refused to start streaming\n");
/*
* If you see this warning, then the driver isn't cleaning up properly
* after a failed start_streaming(). See the start_streaming()
* documentation in videobuf2-core.h for more information how buffers
* should be returned to vb2 in start_streaming().
*/
if (WARN_ON(atomic_read(&q->owned_by_drv_count))) {
unsigned i;
/*
* Forcefully reclaim buffers if the driver did not
* correctly return them to vb2.
*/
for (i = 0; i < q->num_buffers; ++i) {
vb = q->bufs[i];
if (vb->state == VB2_BUF_STATE_ACTIVE)
vb2_buffer_done(vb, VB2_BUF_STATE_QUEUED);
}
/* Must be zero now */
WARN_ON(atomic_read(&q->owned_by_drv_count));
}
/*
* If done_list is not empty, then start_streaming() didn't call
* vb2_buffer_done(vb, VB2_BUF_STATE_QUEUED) but STATE_ERROR or
* STATE_DONE.
*/
WARN_ON(!list_empty(&q->done_list));
return ret;
核心清 start_streaming_called,对残留驱动持有数告警,并将遗留 ACTIVE 按 QUEUED 回收;还检查 done_list 是否意外非空。
这些是软件协议检查。它们不能停止仍在运行的 DMA,也不能替驱动删除它自己错误保留的链表节点。驱动必须先保证实际访问和异步使用已经退出,再按协议归还。否则即使核心把 state 改了,后面仍可能重复处理或访问失效存储。

图 8:先回退 streamer,再回退 Capture 已取得的管线与缓冲区,最后由 VB2 检查归还结果。前置管线失败走较短分支。
11.4 STREAMON 失败与延后 QBUF 失败,结果不同
假设驱动正确回收全部对象,且原有 buffer 0 预排成功:
后一条分支不会自动把 streaming 清零。再次 STREAMON 可能仅因它已经为 1 而直接成功,不能当作重新执行启动回调的保证。应用应依据当前状态补交、修正问题,或执行 STREAMOFF 取消后重新组织。
对被撤销的 buffer 1,恢复 DEQUEUED 也不等于所有准备痕迹都消失。prepared、synced 可以仍为 1;这是第七篇已经分析的准备复用语义。正确的取消路径负责补齐收尾,而不是在应用中擅自改内部标志。
11.5 正常停止与启动失败的配对边界
正常停止时,VIMC 先 kthread_stop(),等处理线程不再访问图像,再终止子设备,停止媒体管线,按 ERROR 归还仍在私有列表中的缓冲区。
这里仅交代为什么停止顺序也能验证启动资源的层次。STREAMOFF 的完整取消过程、完成列表清理与缓冲区复用,将在停止专题继续展开。
12. 锁保护的是哪些关系?
12.1 ioctl 操作锁并不会冻结处理线程
在 VIMC 中,q->lock 和 vdev->lock 指向同一个 vcapture->lock。标准 STREAMON 分发选择并取得操作锁,在其中进入 VB2 和驱动的启动回调。
这有助于串行化相关队列操作,却不意味着创建出的处理线程也会等待这把锁。VIMC 的处理路径使用 qlock 保护自己的缓冲区列表,取出对象后解锁,再执行像素复制。
因此,持有 ioctl 互斥锁期间仍可能产生完成结果。它保护的是相应操作之间的协作,不是“把整个采集系统暂停到 ioctl 返回”。
12.2 媒体图锁和私有列表锁各有边界
媒体核心建立管线时在内部取得 graph_mutex,用于图关联、链接校验和占用管理。它没有被 vb2_core_streamon() 从入口一直持有到第一帧完成。
vcapture->qlock 用于驱动私有 buf_list 的插入、取出和批量归还。不能持着这把锁去做耗时的整条管线准备,也不能持着它等待一个退出时还需要这把锁的线程。
q->done_lock 则是完成路径的锁,具体范围将在下一篇说明。申请映射时的 q->mmap_lock 也不是这里覆盖整个启动过程的操作锁。

图 9:多把锁保护不同对象,不是一层套一层覆盖整段采集流程。虚线说明线程创建后的独立执行关系。
12.3 Request 的外层锁只在适用条件下出现
标准分发在媒体设备支持 Request,且命令是 STREAMON/STREAMOFF 时,还会先取得 req_queue_mutex,再取得本次 ioctl 操作锁,以协调请求提交与流状态变化。
本篇 VIMC 普通路径不依赖 Request,不把这个条件性锁画成每个设备的必经步骤。直接调用 VB2 core 的驱动路径,也不能假定标准 ioctl 层已经替自己加锁。
实现对照:__video_do_ioctl()、v4l2_ioctl_get_lock()、media_pipeline_start();VIMC 的缓冲区接收、填帧和停止路径。
13. 怎样验证:区分启动成功与第一次出现完成结果
13.1 先做正常启动,再观察延后启动
配套的 streamon_probe.c 完成最小启动观察:读取当前格式,申请四块 MMAP 缓冲区,查询并映射,预排缓冲区,发 STREAMON,用有上限的 poll 等待可读,最后取消队列并清理。
程序支持单平面和多平面 CAPTURE。它不设置管线格式,不读取或修改像素,不执行 DQBUF;等待结束后用 STREAMOFF 取消本轮安排。程序适合验证“请求是否走到能产生完成结果”,不代替下一篇的完整出队程序。
cc -std=c11 -Wall -Wextra -Werror -O2 \
streamon_probe.c -o streamon_probe
./streamon_probe /dev/videoX
./streamon_probe /dev/videoX --delayed
正常模式先预排所有实际返回的缓冲区。--delayed 模式先只预排 buffer 0,调用 STREAMON 后做一次短等待,再提交剩余缓冲区。它用于观察当前 VIMC 的最低两块策略;其他驱动的下限、接口行为和拓扑不同,不保证得到同样现象。
实验必须在空闲、格式已配置正确的测试节点上进行。与第六、七篇只准备存储不同,这次确实会尝试启动实际采集。不能随意对运行中的业务节点执行。
13.2 应观察哪些关系,而不是预设哪些数字?
对当前 VIMC,延后模式的第一次短等待通常没有图像结果,因为第二块尚未提交。补交后才可能进入实际启动。但“没有等到帧”本身不是唯一证据:上游格式不匹配、线程处理错误或没有足够资源,也可能没有结果。
更直接的验证点是:
vb2_core_streamon() | |
__enqueue_in_driver() | |
vimc_capture_start_streaming() | |
vimc_streamer_pipeline_init() | |
vimc_streamer_thread() | |
vimc_capture_process_frame() |
应用层没有一个通用 ioctl 可以直接读出 start_streaming_called。QUERYBUF 返回的标志也只是缓冲区快照,不能替代内核状态观察;线程已经执行时,日志看到 DONE 而不是 ACTIVE 是可能的。
13.3 常见失败要按最早失败层次定位
EBUSY | |
EINVAL | |
EPIPE | |
ENOMEM | |
错误码并非与某一层一一对应,表中列出的是本篇最值得优先排查的位置。
本轮复核包括源函数和节选核对、模拟启动与失败分支,以及程序编译和错误路径检查。模拟不包含真实调度、媒体设备驱动硬件操作、DMA 和缓存维护;具体结果记录在配套复核说明中。
14. STREAMON 这一步结束时,究竟完成了什么?
对本篇的正常立即启动路径,过程可以归纳为:
足够的预排缓冲区
→ 核心确认类型、池大小与启动门槛
→ 逐个移交驱动,建立 ACTIVE 责任关系
→ 驱动建立并校验媒体管线
→ 从 Capture 向源端建表、准备子设备
→ 创建并唤醒处理线程
→ 线程从源端向 Capture 处理图像
→ Capture 写入缓冲区,报告完成
这不是一条永不返回的同步调用链。线程唤醒以后,系统调用路径还在逐层返回;什么时候发生第一帧,要看实际调度和处理是否推进。
数量不足时,STREAMON 可以先接受请求,后续 QBUF 再触发真正启动。启动失败时,已移交对象按 QUEUED 退回,而不是凭空成为完成图像。无论哪个分支,都应同时看队列标志、单个对象状态和实际驱动资源。
至此,一块缓冲区已经从“可访问的存储”,走到“由执行路径实际填入图像”。下一篇从 vb2_buffer_done() 继续,分析这个完成结果怎样进入等待与出队过程。