V4L2 videobuf2缓冲区管理与摄像头取流排错
发布时间:2026/10/27 15:57:04 锦皓数字建站

前阵子接了个摄像头 bring-up 的活sensor 那边的寄存器表调通了I2C 能读能写MIPI 也有波形出来结果一到取流环节就卡住——应用层VIDIOC_DQBUF永远超时dmesg里干干净净什么都没报。那几天我把videobuf2的代码从头翻了一遍才发现问题根本不在 sensor 上而是我在buf_prepare回调里漏了对vb2_plane的bytesused处理导致帧数据在DONE状态里打了个转又回去用户态拿到的是空包。这件事之后我养成了一个习惯只要碰 V4L2 的采集或者输出链路先把 videobuf2 这一层吃透。它是 V4L2 框架里专门负责缓冲区管理的内核子系统管着 buffer 的申请、分配、映射、入队、出队、状态流转、DMA 同步这一整套脏活累活。你在用户态调的那几个VIDIOC_REQBUFS、VIDIOC_QBUF、VIDIOC_DQBUF、VIDIOC_STREAMON底下全是它在跑。它解决的问题很实在把几十个驱动里重复写了几十遍的 buffer 逻辑抽成公共层让驱动作者只需要实现几个回调剩下的队列调度、内存后端选型、多平面处理、文件描述符导出全交给框架。这篇东西适合两类人看。一类是刚接手摄像头、编解码器、显示输出这类驱动的同学正在被vb2_queue的一堆字段和回调绕得头晕另一类是想搞清楚用户态mmap拿到的指针到底指向哪块物理内存、dmabuf共享是怎么串起来的老手想回头补一下底层的账。我下面会按“为什么这么设计、结构长什么样、一次取流到底发生了什么、驱动怎么落地、出问题怎么查”这个顺序讲中间夹着我自己踩过的坑能直接抄的部分我尽量写成可以照着改的代码。1. 为什么 V4L2 要单独抽出一层 videobuf21.1 从字符设备的 ioctl 说起buffer 管理到底有多少脏活要理解 videobuf2 存在的理由得先看没有它的时候驱动作者要写什么。V4L2 的设备节点本质上是个字符设备用户态所有的取流动作都落到ioctl上。以最经典的VIDIOC_REQBUFS为例用户态说“我要 4 个 buffer类型是V4L2_BUF_TYPE_VIDEO_CAPTURE内存方式是 mmap”内核这边要做的事情一长串校验数量是否超过驱动上限、判断当前是否还在 streaming 状态streaming 时不允许改 buffer 数量、按buf_struct_size逐个分配驱动自定义的 buffer 结构体、为每个 buffer 分配若干 plane、根据内存后端去真正申请物理内存或虚拟内存、把物理页框准备好给后面的mmap用、把 buffer 挂进队列、最后把实际分配成功的数量回填给用户态。这还只是REQBUFS一个。QUERYBUF要把每个 buffer 的长度、偏移、plane 数量报给用户QBUF要把用户态传下来的index校验一遍把 buffer 状态推到排队态如果是 streaming 状态还得立刻推给硬件DQBUF要在没有完成帧的时候把调用者挂起睡眠等中断里有人wake_upSTREAMON要按顺序把每个已排队的 buffer 交给驱动去启动 DMAmmap要能根据偏移反查出是哪个 buffer 的哪个 plane然后把页框映射进用户地址空间。这些东西每一行都不难但堆在一起就是几百行而且几乎每个 V4L2 驱动都长得差不多。更麻烦的是内存后端有四五种纯软件处理的用vmalloc有 IOMMU 的用dma-sg物理连续要求强的用dma-contig要给用户态直接操作物理地址的用dma-contig的 cookie 模式还有userptr和dmabuf这种外部内存导入。要是每个驱动自己维护同一份逻辑在不同驱动里慢慢就跑偏了bug 修了一处漏了三处。这就是第一代 videobuf 出现的原因也解释了为什么后来要重写。老版本 videobuf 把这些逻辑抽出来了但抽象层次切得不够干净内存管理和队列状态机缠在一起多平面支持几乎是硬的dmabuf导入这种新需求塞不进去最后内核里干脆另起炉灶做了 videobuf2。所以你现在看到的所有新驱动基本都只认 vb2。1.2 videobuf2 的职责边界它管什么不管什么搞清楚边界比背结构体名字重要得多。vb2 管的是“buffer 的生命周期”具体包括四件事。第一件是内存申请与释放。它定义了一层vb2_mem_ops把“申请一块能 DMA 的内存”这个动作抽象成alloc、put、get_userptr、put_userptr、prepare_fb、finish_fb、map_dmabuf、unmap_dmabuf、attach_dmabuf这些函数指针。框架本身不关心你是从伙伴系统要页还是从 CMA 要连续块它只调这几个钩子。第二件是队列与状态机。每个 buffer 有QUEUED、ACTIVE、DONE、ERROR等状态什么时候从排队变活跃、什么时候从活跃变完成、完成之后是回到排队还是直接给用户态这套流转由vb2_core_*系列函数统一维护驱动不能自己乱改。第三件是用户态接口适配。vb2_ioctl_*和vb2_fop_*这两组函数把ioctl、mmap、poll、read都实现好了驱动只要在file_operations里挂上就行连v4l2_fh相关的poll唤醒逻辑都给你写好了。第四件是同步与时序。streamon的时候要不要先调驱动的start_streaming、失败了怎么回滚、streamoff时怎么把还在硬件里的 buffer 收回来、buf_finish在DQBUF之前还是之后调这些顺序问题框架替你定了。它不管的是硬件本身寄存器怎么配、DMA 描述符怎么填、中断里怎么判断是哪一帧完成、sensor 的 exposure 和 gain 怎么联动。这些统统是驱动的事。我在带新人的时候常说一句话vb2 是“搬运工”不是“生产车间”它负责把空的 buffer 送进去、把满的 buffer 收回来中间那个把数据真正塞进内存的动作得你自己在中断里做。1.3 buffer 数量、大小与内存后端的三方权衡在真正动手写驱动之前有三个参数必须先想清楚它们互相牵制选错了后面全是麻烦。buffer 数量。太少会丢帧太多会浪费内存并且增加延迟。经验值是这样的如果输出端是实时显示2 到 3 个足够因为多了只会让画面延迟变大如果是编码或者录像4 到 6 个比较稳因为编码器偶尔会卡一下如果是高帧率高速采集比如 120fps 以上至少要 6 到 8 个DMA 完成中断的响应抖动很容易吃掉一两个。数量上限在vb2_queue里由max_num_buffers限制用户态传超过这个值会被裁剪不会报错这点要留意有时候你以为是申请成功 8 个实际只给了 4 个。缓冲大小。这里最容易踩的是 stride 对齐。假设 sensor 输出 1920x1080 的 YUYV一行 1920 像素、每像素 2 字节看着是 3840 字节一行。但如果硬件要求行对齐到 256 字节实际 stride 就是 38403840 刚好是 256 的 15 倍凑巧对齐换成 1280 宽的 RGB565 呢1280x2 等于 2560也是对齐的。真正出问题的是那些奇怪的分辨率比如 1366 宽1366x2 等于 2732不是 256 的倍数得补到 2816。bytesperline填错用户态解析出来的图像就是斜的画面像被人拧了一下。内存后端。这个我在第 2 节还会展开先说结论绝大多数采集驱动用vb2_dma_contig_memops或者vb2_dma_sg_memops前者要求物理连续、适合小分辨率和小 buffer后者靠 scatter-gather 表、适合大分辨率和有 IOMMU 的平台。纯软件处理的虚拟设备比如基于内存的测试驱动、格式转换器用vb2_vmalloc_memops。要给用户态共享或者跟其他设备零拷贝的走dmabuf。注意buf_struct_size这个字段必须填成你自己那个结构体的sizeof而且你的结构体第一个成员必须是struct vb2_buffer多平面的话是struct vb2_v4l2_buffer。框架在alloc的时候是按这个大小申请的但它只会把前面对齐到vb2_buffer的部分当作标准结构用后面那截是你自己的私有数据区。填成sizeof(struct vb2_buffer)而你又往里塞了私有字段等于每次访问都在越界。2. videobuf2 的整体架构与核心数据结构2.1 vb2_queue、vb2_buffer、vb2_ops 三者的咬合关系这三个结构是整套框架的骨架理解了它们的咬合方式剩下的都是细节。vb2_queue是“一个队列”一个驱动实例通常有至少一个采集和输出各一个。它记录着这个队列的类型type比如V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE、当前 buffer 数量num_buffers、最大数量max_num_buffers、当前 plane 数num_planes、内存后端mem_ops、要用的 ioctl 组io_modes、驱动的操作集ops、以及一大串内部状态queued_count、start_streaming_called、streaming、last_buffer_dequeued等等。它还有一个struct list_head queued_list和struct list_head done_list分别装着排队态和完成态的 buffer。vb2_buffer是“一个 buffer”里面有index用户态看到的编号、type、memory内存方式、num_planes、planes[]每个 plane 的地址、长度、数据量、偏移、dbuf 等、state当前状态、queued_entry和done_entry两个链表节点。多平面场景下用的是vb2_v4l2_buffer它套了一层vb2_buffer再额外带field、timecode、sequence、flags这些 V4L2 语义字段。vb2_ops是驱动要实现的回调集合最关键的是这么几个queue_setup告诉框架我要几个 plane、每个多大、buf_initbuffer 第一次被分配时做一次初始化、buf_prepare每次 QBUF 之前调用来填bytesused、做 cache 同步、buf_finish每次 DQBUF 之后调用来做 cache invalidate、buf_cleanupbuffer 被释放前调、start_streaming启动硬件、stop_streaming停止硬件、buf_queue把单个 buffer 交给硬件、wait_prepare和wait_finish控制睡眠时的锁释放与重取。咬合关系是这样的用户态调REQBUFS框架先调一次queue_setup问“你要几个 plane、每个多大、要几个 buffer”驱动根据当前格式算出来填进去框架据此分配vb2_buffer和 plane 内存然后对每个 buffer 调buf_init。之后每次QBUF框架调buf_prepare接着把这个 buffer 挂进queued_list如果此时正在 streaming就立刻调buf_queue把它推给驱动。驱动在中断里把帧数据写完后调vb2_buffer_done(buf, VB2_BUF_STATE_DONE)框架把它挪进done_list唤醒在DQBUF里睡眠的进程。用户态取走之后框架调buf_finishbuffer 重新回到可排队状态。这个链路有个容易被忽略的细节buf_prepare和buf_queue的调用上下文不一样。前者可能在用户态的ioctl上下文里跑后者也可能但如果是 streaming 状态下 QBUF这两个是一起在ioctl里同步调完的。而vb2_buffer_done通常在中断上下文不能睡眠。所以你在buf_queue里做 DMA 启动没问题但千万别在里面等信号量或者调可能睡眠的函数一旦这个buf_queue是被中断里的某个路径间接触发的比如streamon之后驱动在中断里继续投喂直接就 BUG。2.2 vb2_plane 与多平面模型单平面思维是万恶之源vb2_plane是“一个 buffer 里的一块连续区域”。单平面格式比如 YUYV、RGB565一个 buffer 就一个 plane视频的 Y 和 UV 全在里头交织。多平面格式比如 NV12M、YUV420M一个 buffer 有多个 planeY 一块、UV 一块各自独立。这个区别看起来只是数组长度的事实际上影响到用户态接口、内存布局、甚至mmap的偏移计算。单平面用struct v4l2_buffer多平面用struct v4l2_buffer里那个m.planes指针指向一个struct v4l2_plane数组。你在驱动里判断用户态用的是哪套靠的是queue-type是不是以_MPLANE结尾。我见过最常见的错误是把多平面格式当单平面处理。比如某个驱动声明支持 NV12Mqueue_setup里却只填了一个 plane 的大小把 Y 和 UV 加起来用户态按多平面去解析第二个 plane 的长度是 0取出来直接段错误。反过来也有声明单平面 NV12 却在queue_setup里返回 2 个 plane框架会拒绝REQBUFS返回-EINVAL。正确的做法是在queue_setup里根据format-fmt.pix_mp.num_planes多平面或者自己维护的格式表来决定。给个能直接用的写法static int my_queue_setup(struct vb2_queue *q, unsigned int *num_buffers, unsigned int *num_planes, unsigned int sizes[], struct device *alloc_devs[]) { struct my_dev *dev vb2_get_drv_priv(q); unsigned int size; /* * 注意这里不要直接信任 *num_buffers * 用户态可能传 0表示给个默认值或者传个很大的数。 */ if (*num_buffers 0) *num_buffers 4; if (*num_buffers MY_MAX_BUFS) *num_buffers MY_MAX_BUFS; if (dev-fmt-num_planes 1) { /* 单平面把 stride 和高度都算进去 */ size dev-fmt-bytesperline * dev-fmt-height; *num_planes 1; if (sizes[0] sizes[0] size) return -EINVAL; /* 用户态给的太小拒绝 */ sizes[0] size; } else { /* 多平面逐 plane 计算 */ *num_planes dev-fmt-num_planes; for (int i 0; i *num_planes; i) { size dev-fmt-plane_fmt[i].sizeimage; if (sizes[i] sizes[i] size) return -EINVAL; sizes[i] size; } } return 0; }这里有个手法值得单独说sizes[i]非零的时候要拿它跟驱动算出来的比。用户态在REQBUFS之前可能已经通过S_FMT拿到过建议的sizeimage然后按那个值传下来。如果用户态传的比驱动需要的小必须返回-EINVAL不能默默用大的那个否则用户态按自己传的长度去 mmap映射长度不够读越界。反过来如果用户态传大了允许把大的那个填回去就行框架会尊重用户的选择。2.3 vb2_mem_ops内存后端抽象层怎么选这一层是 vb2 里抽象得最漂亮的部分也是选型时最需要想清楚的。框架提供了四套现成的实现我按使用频率排个序顺便把适用场景和坑说清楚。后端符号物理连续性要求典型场景主要坑点DMA 连续vb2_dma_contig_memops必须连续小分辨率摄像头、老平台无 IOMMU大 buffer 分配失败率高靠 CMA 池DMA 分散vb2_dma_sg_memops不要求大分辨率、有 IOMMU、多平面需要正确的dma_attrs描述符表要够长vmallocvb2_vmalloc_memops不要求纯软件处理、虚拟/测试设备不能用dma_map只能 CPU 访问dma-buf 导入vb2_dma_sg_memops 专用map_dmabuf跟随来源零拷贝共享、ISP/编码器直连生命周期由 exporter 决定引用计数要准dma_contig的逻辑最简单调dma_alloc_coherent或dma_alloc_attrs要一块连续的 DMA 内存CPU 虚拟地址和总线地址一起拿到。它的优点是驱动写起来最省事缓冲区地址直接就是物理地址硬件描述符表填一个地址一个长度就完事。缺点是内存碎片一严重就分配不出来尤其是 4K 分辨率、NV12 双平面、8 个 buffer 这种配置一上来就要几十兆连续内存系统跑一天之后基本没戏。我之前在一个只有 512MB 内存的板子上配 1080p 六缓冲REQBUFS直接返回-ENOMEM换成dma_sg立刻就过了。dma_sg走的是 scatter-gather 表每块内存单独分配用sg_table串起来硬件支持分散 DMA 的话直接把这个表喂进去硬件不支持的话需要自己在驱动里做一次拷贝合并性能就下来了。它的分配成功率几乎百分之百代价是初始化时多几次sg_alloc_table的开销以及你要确保硬件的描述符表长度够用。vmalloc那套基本只在软件路径里出现比如一个把文件解码成视频帧的虚拟设备或者格式转换的测试驱动。它申请的是普通内核虚拟内存没有 DMA 地址你在buf_prepare里拿到的plane-mem_priv转出来是vmalloc的指针直接memcpy就行。dmabuf不是独立的一套 memory ops而是一个能力vb2_dma_sg_memops和vb2_dma_contig_memops都实现了attach_dmabuf和map_dmabuf。用户态通过VIDIOC_QBUF传进来的fd会被转成dma_buf对象框架调attach把它挂到 plane 上取流时再map成 sg_table 给硬件。这里的关键是引用计数attach时getdetach时put中间不管发生什么错误路径都不能漏漏一次就是内存永远不释放用户态关掉 fd 也没用因为内核里还挂着一个引用。我排查过一个“采集程序反复启停之后内存持续上涨”的问题最后就是buf_cleanup里少了一个dma_buf_put。3. 一次完整的数据流从 REQBUFS 到 DQBUF3.1 REQBUFS 与 QBUF 的调用链路拆解我习惯把这条链路完整走一遍因为几乎所有诡异问题都能在这里找到落点。用户态第一步是open设备节点驱动在open里通常要调v4l2_fh_open把v4l2_fh初始化好后面poll和事件订阅都依赖它。接着是VIDIOC_S_FMT驱动把用户请求的宽高格式落到自己的fmt结构里同时算出bytesperline和sizeimage再通过VIDIOC_G_FMT把实际结果告诉用户态。注意这一步是“协商”不是“命令”用户态传 1366 宽驱动可能返回 1368必须让用户态读回实际值再用。然后是VIDIOC_REQBUFS落到vb2_ioctl_reqbufs它做几件事检查q-streaming为假streaming 时改 buffer 数是错的、检查memory类型是否在q-io_modes支持的范围内、调vb2_core_reqbufs。核心函数里先算实际的 buffer 数量count为 0 表示释放全部再调__vb2_queue_alloc这里面才真正调queue_setup拿到 plane 数和大小然后循环kzalloc出buf_struct_size大小的结构体逐个call_memop(alloc)申请 plane 内存最后对每个 buffer 调一次buf_init。REQBUFS成功之后用户态对每个 buffer 调VIDIOC_QUERYBUF拿长度和偏移然后mmap。vb2 的mmap实现是vb2_fop_mmap它根据vma-vm_pgoff反查是哪个 buffer 的哪个 plane单平面的偏移算法是plane_index buffer_index * num_planes然后调 memory ops 的mmapdma_contig直接dma_mmap_attrsdma_sg则是逐页remap_pfn_range。接下来是VIDIOC_QBUF。vb2_core_qbuf里先校验index范围、buffer 状态必须是DEQUEUED或PREPARED、plane 的bytesused不能超过length然后调buf_prepare。如果是 streaming 状态紧接着调buf_queue并更新内部计数如果没在 streaming就只把 buffer 挂进queued_list等STREAMON一起推。3.2 buffer 状态机那几个状态什么时候变vb2 里的状态不多但每一个转换点都值得记牢。VB2_BUF_STATE_DEQUEUED是初始态buffer 在用户态手里不在框架的任何链表上。REQBUFS之后所有 buffer 都是这个状态。VB2_BUF_STATE_PREPARED是buf_prepare成功之后、还没进队列的状态主要用于reqbufs之后框架内部的一些校验路径实际取流时这个态停留时间极短。VB2_BUF_STATE_QUEUED是已经进了queued_list等硬件来取。这个状态下如果streamoff发生框架会把它退回DEQUEUED并通知用户态。VB2_BUF_STATE_ACTIVE是已经调过buf_queue交给硬件了硬件可能正在往里写也可能还没轮到它。这个状态是“危险区”因为 DMA 正在进行你绝对不能在这时候去改 buffer 的内容或者释放它。VB2_BUF_STATE_DONE是驱动调了vb2_buffer_done之后的状态buffer 在done_list里等着用户态DQBUF。VB2_BUF_STATE_ERROR和DONE类似但DQBUF返回时flags里会带V4L2_BUF_FLAG_ERROR用户态拿到这一帧可以选择丢弃或者继续用。还有一个VB2_BUF_STATE_DEQUEUED之后的VB2_BUF_STATE_PREPARING那是buf_prepare执行期间的临时态用来防止重入。状态机最典型的事故是“buffer 卡在 ACTIVE”。原因通常是驱动在中断里判断帧完成的条件写错了或者中断根本没进来。这时候DQBUF会一直睡眠用户态表现为卡死。排查手段我在第 5 节会讲。3.3 start_streaming 的返回值与失败回滚这个点我要单独拎出来说因为它涉及到一段特别绕的代码而且出错时的现象很有欺骗性。vb2_core_streamon里框架先把queued_list上的 buffer 逐个调buf_queue交给驱动然后调start_streaming(q, count)count是排队的 buffer 数。驱动的start_streaming要做的是配置硬件、开中断、启动第一帧 DMA。关键在返回值处理上。如果驱动返回-ENODEV或者-ENOMEM之类的错误框架会认为启动失败把已经推给硬件的 buffer 状态从ACTIVE改回QUEUED并把start_streaming_called清掉。但如果驱动返回 0 并且已经开始了 DMA然后后来又发现某个 buffer 因为硬件限制没法启动这时候要调vb2_buffer_done(buf, VB2_BUF_STATE_QUEUED)把那个 buffer 退回去框架会自己处理剩下的事情。有个反直觉的约定start_streaming里如果返回非 0 且count 0框架会把q-streaming直接置成 false 并返回错误。也就是说允许“空队列启动失败”不允许“空队列启动成功”。如果你在start_streaming里没检查count就返回 0之后STREAMON显示成功但队列里一个 buffer 都没有硬件启动了也拿不到数据用户态就永远等不到DQBUF。我踩过的坑是反过来的在start_streaming里对count做了检查如果不够就返回-EINVAL结果用户态只QBUF了一个 buffer 就STREAMON驱动拒绝用户态以为驱动坏了。正确做法是给个最小数量的建议值但别硬性拒绝除非你的硬件确实需要多个 buffer 才能起转。4. 驱动落地实操最小可用的采集骨架4.1 vb2_ops 各回调的实现时机与写法我把最核心的几个回调按调用顺序捋一遍每个都给出可以直接改的代码。假设我们已经有一个struct my_dev里面放着v4l2_device、video_device、vb2_queue、当前格式、硬件寄存器基址这些东西。queue_setup前面给过完整代码要点是算准 plane 数和每块大小并且尊重用户传入的sizes。buf_init只在 buffer 第一次被分配时调一次用来做一次性的内存初始化。比如你要在这块内存前面留一段元数据区或者要把整块内存清零就在这里做static int my_buf_init(struct vb2_buffer *vb) { struct my_dev *dev vb2_get_drv_priv(vb-vb2_queue); struct my_buf *buf container_of(vb, struct my_buf, vb); /* * 这里可以挂驱动私有的 DMA 描述符、 * 也可以把 buffer 加入驱动的某个内部池。 * 注意别在这睡眠太久某些路径下上下文不允许。 */ buf-dev dev; return 0; }buf_prepare每次QBUF都调采集方向上它主要干两件事采集前把 buffer 标记为可写、让 CPU cache 里关于这块内存的旧数据失效如果是 cache 一致性的内存后端这一步可以省输出方向则是把bytesused填上、把 cache 刷到内存里。采集驱动的典型写法static int my_buf_prepare(struct vb2_buffer *vb) { struct my_dev *dev vb2_get_drv_priv(vb-vb2_queue); struct vb2_v4l2_buffer *vbuf to_vb2_v4l2_buffer(vb); if (vb-type V4L2_BUF_TYPE_VIDEO_CAPTURE) vbuf-field V4L2_FIELD_NONE; for (int i 0; i vb-num_planes; i) vb2_set_plane_payload(vb, i, dev-fmt-plane_fmt[i].sizeimage); return 0; }buf_prepare返回非 0 会让这次QBUF失败但不影响队列适合做参数校验。但如果你在这里返回-EPIPE框架会把 buffer 直接推成ERROR态并唤醒等待者这个语义是给“出错了要立刻通知用户态”用的别乱用。buf_finish在用户态DQBUF之后调采集方向上用来做 cache invalidate确保用户态读到的是 DMA 写进去的最新数据输出方向上用来等 DMA 真正写完。很多驱动这两个回调留空在dma_contigcoherent 内存下确实没问题但在dma_sg配合非一致性内存时不刷 cache 就会读到旧数据现象是画面上一帧的内容重影或者干脆是一整片雪花。buf_queue是真正把 buffer 推给硬件的地方static void my_buf_queue(struct vb2_buffer *vb) { struct my_dev *dev vb2_get_drv_priv(vb-vb2_queue); struct vb2_v4l2_buffer *vbuf to_vb2_v4l2_buffer(vb); unsigned long flags; spin_lock_irqsave(dev-slock, flags); list_add_tail(vbuf-list, dev-pending); dev-hw_ready true; /* 如果硬件此刻空闲立刻启动下一帧 */ my_start_dma(dev); spin_unlock_irqrestore(dev-slock, flags); }注意这里用的是自旋锁加中断禁用因为中断处理函数里也会碰pending链表和hw_ready两边必须用同一把锁。start_streaming和stop_streaming是一对。前者要在返回前把队列里已有的 buffer 都启动并且要处理“启动过程中硬件报错”的清理后者要在返回前确保硬件完全停下把还在 DMA 中的 buffer 收回来对每个调vb2_buffer_done(buf, VB2_BUF_STATE_ERROR)。4.2 队列初始化那些必须有值的字段vb2_queue是kzalloc出来的全零但不代表都能零。我列一个清单这些字段初始化时必须有明确的值字段必须填什么填错的后果type缓冲类型含_MPLANE判断用户态REQBUFS直接EINVALio_modesVB2_MMAP/VB2_USERPTR/VB2_DMABUF的组合用户态用了未声明的内存方式会被拒dev设备指针用于 DMA 分配为 NULL 时dma_alloc失败ops你的vb2_ops空指针解引用mem_ops内存后端同上buf_struct_size你的 buffer 结构体大小私有数据越界min_buffers_needed启动流所需最少 buffer太小可能启动失败太大会误伤lock保护队列的锁并发访问链表损坏timestamp_flags时间戳来源标志时间戳不准或为 0lock这个字段特别值得一提。它是可选的填了之后框架会在vb2_core_*里自己加锁lock通常指向v4l2_fh里那个vdev-lock或者驱动自己的互斥量。填了lock之后你在vb2_ops的回调里就不能再拿同一把锁否则死锁。反过来如果你自己管锁把lock留空那所有vb2_core_*调用都得你自己在外部加锁。timestamp_flags填V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC是最常见的选择框架会用ktime_get_ns打时间戳。如果你需要硬件时间戳比如某些传感器带帧同步信号要在中断里自己打并且把这里设成V4L2_BUF_FLAG_TIMESTAMP_COPY否则框架会覆盖你的值。初始化代码大致长这样static int my_queue_init(struct my_dev *dev) { struct vb2_queue *q dev-queue; q-type V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; q-io_modes VB2_MMAP | VB2_DMABUF; q-dev dev-dma_dev; q-ops my_vb2_ops; q-mem_ops vb2_dma_sg_memops; q-buf_struct_size sizeof(struct my_buf); q-min_buffers_needed 2; q-timestamp_flags V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC; q-lock dev-mutex; q-drv_priv dev; return vb2_queue_init(q); }vb2_queue_init里会做一致性检查比如buf_struct_size不能小于sizeof(struct vb2_buffer)min_buffers_needed不能超过max_num_buffers返回非 0 就说明配置有矛盾一定要在probe里处理掉。4.3 DMA 映射与 addr/cookie 的取舍vb2_plane里和 DMA 相关的字段有两个m.phys_addr和m.dma_addr。前者是给那些需要真实物理地址的老硬件用的后者是给走 DMA API 的标准路径用的。这两者是互斥的填一个就不能填另一个。用哪个取决于你的硬件。如果硬件的描述符表里填的是“物理地址”而且这个平台没有 IOMMU那phys_addr和dma_addr恰好相等随便用哪个都行。如果平台有 IOMMU总线地址和物理地址就对不上了必须用dma_addr用phys_addr的话硬件访问会打到错的地方通常是访问越界触发 SMMU 报错。获取的时机是在buf_prepare之后、buf_queue之前。如果你用的是dma_sg_memops可以在buf_init或者buf_prepare里调vb2_dma_sg_plane_desc拿到sg_table再自己遍历。也可以直接读plane-dma_addr框架在alloc的时候已经帮你映射好了。dma_contig的 cookie 模式值得提一句。有些平台需要把 buffer 的物理地址交给其他设备或者用户态用又不能暴露内核虚拟地址就用 cookie。vb2_dma_contig_set_max_seg_size和相关的接口可以设置 cookie 的回调硬件通过 cookie 来访问。这套东西用起来比较重没有强需求就别上。5. 常见问题与排查实录5.1 症状速查表我把这些年遇到的现象整理成了表按“现象 → 最可能的原因 → 动手查哪里”的顺序排出问题时可以对着扫一遍。现象可能原因排查手段REQBUFS返回-ENOMEM连续内存申请失败、queue_setup算的大小太大cat /proc/buddyinfo换dma_sg检查sizeimageREQBUFS返回-EINVALtype与实际不匹配、num_planes为 0打印q-type和num_planesQBUF返回-EINVALbytesused超length、index越界、状态不对打印vb-state和 plane 长度DQBUF永久阻塞buffer 卡在ACTIVE、中断没进、vb2_buffer_done没调DQBUF前cat /sys/kernel/debug/vb2/*看queued_count和done_countDQBUF返回但画面全黑DMA 目标地址错、bytesperline与硬件不符用hexdump看 buffer 前 64 字节画面斜切、错位bytesperline与实际 stride 不一致对比G_FMT返回值与硬件寄存器启停几次后内存不释放dmabuf引用漏put、buf_cleanup没释放私有资源kmemleak、/proc/slabinfoSTREAMON返回-EBUSYstart_streaming里返回了错误但没清理硬件检查错误路径的寄存器状态帧率只有一半每次都等到中断才投下一帧没做乒乓改成buf_queue里立刻启动下一个5.2 用 trace 和 debugfs 定位卡在 ACTIVE 的 buffervb2 在 debugfs 里挂了一个目录路径通常是/sys/kernel/debug/vb2/里面按设备名和队列名分了子目录每个 buffer 一行能看到状态、地址、引用计数。这个是最直接的入口比加一堆printk快。如果 debugfs 里显示 buffer 是ACTIVE而硬件早就把数据写完了基本就是中断里判断完成的条件有问题。这时候我会开trace# 打开 vb2 相关的 trace 点 echo 1 /sys/kernel/debug/tracing/events/vb2/enable # 只看某个进程的 echo comm my_capture /sys/kernel/debug/tracing/events/vb2/filter cat /sys/kernel/debug/tracing/trace_pipetrace 里能看到vb2_buf_queue、vb2_buf_done、vb2_buf_prepare这些事件把时间戳一对比就知道是没进中断、还是进了中断但没找到对应 buffer、还是done调了但状态传错了。还有一种情况是“中断进了done也调了但queued_count一直是 1”。这通常是因为驱动在中断里调vb2_buffer_done的时候传了VB2_BUF_STATE_QUEUED框架把它退回排队态而不是完成态用户态永远等不到。这个错误特别隐蔽因为done的日志有DQBUF就是不返回。改成VB2_BUF_STATE_DONE或者ERROR就好。5.3 几个我踩过的坑值得单独记一笔第一个坑是buf_prepare里对输出方向的bytesused处理。输出设备比如编码器、显示输出的用户态会通过QBUF传下来“这一帧有多少字节有效”值在plane-bytesused里。看起来很简单但你如果在buf_prepare里想当然地把它覆盖成sizeimage用户态送进来的数据量信息就丢了编码器会按整个 buffer 编码结果是画面后半截全是垃圾。正确做法是buf_prepare里只做校验不该改bytesused。第二个坑是streamoff之后立刻STREAMON。有些驱动在stop_streaming里只关了硬件没等 DMA 真正停下就返回框架紧接着就进入下一次启动硬件还在处理上一轮的描述符新的一轮配置直接把它覆盖掉表现为“第二次启动后丢帧率奇高”。稳妥的做法是在stop_streaming里对硬件做一次同步比如读某个状态位直到 idle或者等一个stop done中断并且对还在ACTIVE的 buffer 逐个vb2_buffer_done(buf, VB2_BUF_STATE_ERROR)确保框架内部计数归零。第三个坑是多平面格式下length与sizeimage的语义差异。length是这块 plane 在内存里实际占多大sizeimage是这一帧在这个 plane 里实际要用多少。前者由框架在alloc时确定后者由驱动在buf_prepare里通过vb2_set_plane_payload设置。两者可以不等但payload不能大于length否则QBUF会失败。我在一个支持动态分辨率的驱动里就吃过这个亏分辨率切小之后sizeimage变小了但length还是按最大分辨率分配的代码里做了个min()就以为万事大吉结果硬件那边按sizeimage写数据、软件这边按length解析中间那段全是上一帧的残影。最后一个也是我觉得最容易被忽略的reqbufs传count 0是“释放所有 buffer”的意思不是“给我 0 个 buffer”。有的应用在退出时想清干净传 0这没问题框架会把 buffer 全释放掉、状态清空。但如果应用在 streaming 状态下传 0框架返回-EBUSY应用如果没处理这个错误就直接往下走后面所有的QBUF都会失败。所以驱动侧在vb2_ioctl_reqbufs之外最好别自己再拦一次让框架的语义统一应用出问题也好定位。我个人在实际操作中的体会是vb2 这一层真正的难点不在结构体多而在于“状态和责任”的划分。buffer 在谁手里、谁能改它、什么时候能改这三个问题的答案就藏在state字段和那几个回调的调用时机里。把第 3 节那条链路在纸上自己画一遍标出每次状态变化发生在哪个函数里比读十遍代码管用。后面如果要把这套东西接到零拷贝的链路上比如采集完直接喂给 ISP 或者编码器走dmabuf是正路但那时候要盯的就是另一件事了——引用计数的每一个get后面是不是都有一个能到达的put。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。