ARTICLE · 1127872
V4L2 源码深度解析(六):从缓冲区编号到可访问地址——QUERYBUF 与 mmap
上一节中,REQBUFS 已经把缓冲区池建立起来。假设这次成功得到四个缓冲区,VIMC 使用 640 × 480 的 RGB24 格式,每块需要 921600 字节。内核能够通过队列找到这些存储,应用手里却仍然只有设备文件描述符和返回的缓冲区数量。
应用知道“有四块”,并不知道“每块应当怎样访问”。下一步需要先问清某一块的容量和映射标识,再把它接入本进程的地址空间。这两件事分别由 QUERYBUF 和 mmap 完成。
本篇沿普通 CAPTURE、MMAP、vmalloc 后端展开。先把编号、偏移和地址的含义讲清,再从 VIMC 的回调连接进入 VB2,走到映射建立和解除。结束时,应用已经有了访问地址,但尚未提交缓冲区,也没有启动采集。
1. 内存已经分配,为什么还不能直接读取?
1.1 一个编号只能指出“是哪一块”
REQBUFS 成功返回的 count 决定了可使用的索引范围。返回 4,索引就是 0、1、2、3。index = 1 表示查询池中的第二个缓冲区,不表示第 1 帧,也不是字节地址。
VB2 通过 q->bufs[1] 找到这块缓冲区的管理对象;在当前单平面场景中,再通过 vb->planes[0].mem_priv 找到 vmalloc 后端对象。后端对象保存了 vaddr,即内核访问这组像素存储时使用的虚拟地址。
这条关系已经存在,但它只解决了内核怎样管理和访问存储。不能把 vaddr 这个数值直接填给应用,让应用照着解引用。
1.2 指针必须放回它所在的地址空间理解
启用 MMU 的系统中,应用使用本进程的虚拟地址;内核也有自己的虚拟地址。CPU 访问某个虚拟地址时,需要通过地址转换找到实际存储页。
同一组页可以有不同的访问入口。内核地址 K 和应用地址 A 可以数值不同,却对应同一份像素数据。映射建立后,应用通过 A 访问,不需要为了得到一个可用指针,再复制一整帧到另一块内存。

图 1:左右是两套虚拟地址,中间是共同的存储页。P0、P1、P2 表示逻辑上的先后页,不要求物理位置相邻。
mmap() 做的就是建立这类访问关系。它返回本进程中的起始地址,而不是把内核指针公开出去。页表、映射区间和引用管理仍然需要开销;这里避免的是为应用访问而额外复制整帧,不是保证整个视频链路没有任何复制。
1.3 应用还需要一个“选择哪块存储”的参数
同一个 /dev/videoX 后面有多个缓冲区,单个缓冲区还可能包含多个平面。mmap 接口只有一个 offset 参数,没有单独的 index 和 plane 参数。
VB2 因此为各平面安排映射偏移。应用先用 QUERYBUF 查询,得到长度和偏移;再把这两个值原样交给 mmap。VB2 根据偏移选中平面,内存后端随后建立映射。
先记住三件事就够了:index 选择查询对象;offset 选择映射对象;mmap 的返回值才是应用访问的起点。 后面会逐一看到它们怎样进入源码。
2. 先查询 buffer 1,看应用真正拿到了什么
2.1 一次单平面查询
下面的代码假定 MMAP 缓冲区池已经建立,而且返回数量至少为 2:
structv4l2_bufferb = {0};
b.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
b.memory = V4L2_MEMORY_MMAP;
b.index = 1;
if (ioctl(fd, VIDIOC_QUERYBUF, &b) < 0) {
perror("VIDIOC_QUERYBUF");
/* 本次没有取得可用的查询结果,应进入错误清理。 */
}
type 与申请缓冲区时的队列类型保持一致,index 指定池中的对象。把结构体清零,可以让保留字段有确定初值。这里也显式填写 memory,让代码意图清楚;稍后会看到,当前 QUERYBUF 实现会用内部对象的实际内存方式覆盖返回字段,并不会因为填写这个字段就切换队列模式。
对本文的单平面 MMAP 场景,成功后最需要读取的是:
b.length /* 这块缓冲区的字节容量 */
b.m.offset /* 交给 mmap 的映射偏移 */
时间戳和帧序号不是此阶段的采集结果。接口文档也没有承诺 QUERYBUF 返回的所有其他元数据都具有帧结果意义。后续真正处理图像时,应使用完成出队得到的结果。
2.2 用同一个 RGB24 例子观察返回值
假设页大小为 4096 字节,每块长度都是 921600 字节,即恰好 225 页;四块均按当前实现成功建立,则这次分配形成如下偏移:
length / 字节 | m.offset / 字节 | |
|---|---|---|
这是按本实现计算的例子,不是应该写死到程序里的接口常量。真正的程序逐块查询,以每次返回结果为准。其他格式、平面布局和页大小可能得到不同的长度与偏移。
offset = 0 完全可能有效。它表示当前队列中某个平面的选择值为零,不表示得到空指针,也不表示把物理地址零映射给应用。
3. QUERYBUF 的源码:按索引找对象,再把描述填回来
3.1 先确认 VIMC 建立了哪些连接
VIMC 的文件操作表和 ioctl 操作表分别提供了下面几项连接。这里将原本分散在两张表中的成员集中列出,便于观察:
/* struct v4l2_file_operations 中的成员 */
.unlocked_ioctl = video_ioctl2,
.mmap = vb2_fop_mmap,
/* struct v4l2_ioctl_ops 中的成员 */
.vidioc_querybuf = vb2_ioctl_querybuf,
QUERYBUF 走标准 ioctl 分发,mmap 走文件映射入口。它们最终访问同一条队列,但不是同一种系统调用,也不经过相同的参数处理函数。
QUERYBUF 的正常执行关系是:
video_ioctl2()
→ video_usercopy() 准备参数
→ __video_do_ioctl() 标准分发
→ v4l_querybuf() 检查格式类型
→ vidioc_querybuf = vb2_ioctl_querybuf()
→ vb2_querybuf()
→ vb2_core_querybuf()
→ fill_user_buffer = __fill_v4l2_buffer()
描述填好以后,函数逐层返回。标准分发层释放实际持有的操作锁,参数层再把结果回写到应用结构体中。

图 2:实线向下进入处理,右侧虚线概括返回。这里是缓冲区描述的往返,不是图像数据的往返。
3.2 vb2_ioctl_querybuf() 取得当前节点的队列
intvb2_ioctl_querybuf(struct file *file, void *priv, struct v4l2_buffer *p)
{
structvideo_device *vdev = video_devdata(file);
/* No need to call vb2_queue_is_busy(), anyone can query buffers. */
return vb2_querybuf(vdev->queue, p);
}
这个包装函数只从文件找到当前 video_device,再把队列和描述交给 vb2_querybuf()。参数 priv 在这个函数里没有被使用。
源码特意注明:这里不调用 vb2_queue_is_busy()。也就是说,不能把 REQBUFS 中的 owner 检查原样套到查询入口。一个已经合法打开节点的其他上下文,也可能查询现有缓冲区的描述;其他前置检查仍然有效。
3.3 vb2_querybuf() 先验证,再访问 bufs[]
intvb2_querybuf(struct vb2_queue *q, struct v4l2_buffer *b)
{
structvb2_buffer *vb;
int ret;
if (b->type != q->type) {
dprintk(q, 1, "wrong buffer type\n");
return -EINVAL;
}
if (b->index >= q->num_buffers) {
dprintk(q, 1, "buffer index out of range\n");
return -EINVAL;
}
vb = q->bufs[b->index];
ret = __verify_planes_array(vb, b);
if (!ret)
vb2_core_querybuf(q, b->index, b);
return ret;
}
第一项检查保证描述中的队列类型与 q->type 一致。第二项检查保证索引落在已建立对象的范围内。只有两项都通过,才访问 q->bufs[b->index]。
这里用的是 REQBUFS 最终建立的 q->num_buffers,不是应用最初期望的数量。请求 4 个、实际返回 3 个时,索引 3 就不合法。
接着检查平面描述数组。单平面会直接通过这一层;多平面还要检查数组地址及容量,下一节再展开。验证成功以后,核心才填返回描述。
3.4 vb2_core_querybuf() 调用的不是采集驱动的 queue_setup
voidvb2_core_querybuf(struct vb2_queue *q, unsignedint index, void *pb)
{
call_void_bufop(q, fill_user_buffer, q->bufs[index], pb);
}
fill_user_buffer 来自 q->buf_ops,并在 V4L2 队列初始化时接到 __fill_v4l2_buffer()。它负责把通用缓冲区对象转换成 V4L2 接口描述。
这里不会再次执行 queue_setup(),不会重新分配像素存储,也不会调用负责采集的 buf_queue()。当前对象已经存在,查询只是在回答它的描述是什么。
3.5 __fill_v4l2_buffer() 的三段工作
函数先取得通用对象、V4L2 扩展对象及队列,再填写公共字段。关键代码如下:
b->index = vb->index;
b->type = vb->type;
b->memory = vb->memory;
b->bytesused = 0;
b->flags = vbuf->flags;
b->field = vbuf->field;
v4l2_buffer_set_timestamp(b, vb->timestamp);
b->timecode = vbuf->timecode;
b->sequence = vbuf->sequence;
b->reserved2 = 0;
b->request_fd = 0;
这段代码说明,返回的 index、type、memory 来自内部对象。它们不是把应用的输入原样抄回去。时间戳、序号、场信息等则来自通用对象和 V4L2 扩展的当前记录。
随后按单平面或多平面填写容量、载荷与内存相关描述。本文单平面 MMAP 分支中,最关键的赋值可以概括为:
b->length = vb->planes[0].length;
b->bytesused = vb->planes[0].bytesused;
b->m.offset = vb->planes[0].m.offset;
最后根据内部状态重新组织状态标志,并处理时间戳类型、映射持有等信息。这里没有把整帧像素复制进 struct v4l2_buffer。该结构只是一个接口描述,实际图像页仍留在原处。
对应实现:v4l_querybuf()、vb2_ioctl_querybuf()、vb2_querybuf()、vb2_core_querybuf()、__fill_v4l2_buffer()。
4. 多平面:一个描述里为什么还要带一个数组?
4.1 先把两层数量分开
单平面接口用一组长度和偏移描述缓冲区。多平面接口则可能需要分别描述同一帧的几块存储,例如 NV12M 的亮度区域和色度区域。
这里有两个数量:池中有多少个 buffer,每个 buffer 又有多少个 plane。b.index 仍然选择缓冲区,不是平面;b.m.planes 指向用于接收平面信息的数组。
structv4l2_bufferb = {0};
structv4l2_planeplanes[VIDEO_MAX_PLANES] = {0};
b.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
b.memory = V4L2_MEMORY_MMAP;
b.index = 1;
b.m.planes = planes;
b.length = VIDEO_MAX_PLANES;
planes[] 是一组小型描述结构,不是用于存放亮度、色度的像素数组。VIMC 的本文主线仍是单平面;多平面在此解释通用接口,并不表示该 VIMC 捕获节点已经支持 NV12M。

图 3:一次 QUERYBUF 查询一个缓冲区,返回这个缓冲区内各平面的信息。之后对每个平面单独建立映射。
4.2 length 的含义由所在位置决定
进入多平面 QUERYBUF 时,b.length 表示应用数组能容纳多少个元素;成功返回后,框架把它改成实际填写的平面数。每项 planes[p].length 才表示该平面的字节容量。
例如应用提供了可容纳 8 项的数组,当前缓冲区实际有 2 个平面。输入 b.length = 8,成功后返回 b.length = 2,程序只映射前两项。
b.index | ||
b.length | ||
b.length | planes[p].length | |
b.m.offset | planes[p].m.mem_offset |
多平面 API 也可以描述只有一个内存平面的格式。循环次数必须使用返回的平面数,不能根据 _MPLANE 后缀推定一定有两个或三个平面。
4.3 __verify_planes_array() 验证的是什么
staticint __verify_planes_array(struct vb2_buffer *vb, const struct v4l2_buffer *b)
{
if (!V4L2_TYPE_IS_MULTIPLANAR(b->type))
return0;
/* Is memory for copying plane information present? */
if (b->m.planes == NULL) {
dprintk(vb->vb2_queue, 1,
"multi-planar buffer passed but planes array not provided\n");
return -EINVAL;
}
if (b->length < vb->num_planes || b->length > VB2_MAX_PLANES) {
dprintk(vb->vb2_queue, 1,
"incorrect planes array length, expected %d, got %d\n",
vb->num_planes, b->length);
return -EINVAL;
}
return0;
}
这里检查的是已经进入内核处理路径的描述:需要非空数组入口,数组容量不能少于实际平面数,也不能超过内部上界。
它不是一个检查任意应用地址是否可读写的通用函数。标准 ioctl 参数层在此之前,已经为外层结构和已知平面数组准备了内核临时副本。因此,地址不可访问可能在更早的复制阶段就返回 EFAULT,并不一定走到这里得到 EINVAL。
4.4 两级复制处理的是描述,不是像素
多平面查询的参数准备大致为:先复制 v4l2_buffer,再根据它的数组容量复制 v4l2_plane[];内核临时结构中的 m.planes 随后被替换为内核数组地址。
返回时,参数层在进入正常回写分支后先恢复原来的应用数组地址,再回写平面数组和主结构。这样,应用看到的仍是自己提供的 planes[],不会拿到只在本次调用中有效的内核临时指针。
QUERYBUF 表项带有 INFO_FL_CLEAR(v4l2_buffer, length)。本机 ABI 的输入处理据此保留到 length 字段末尾的必要前缀,后面的尾部先清零;数组仍由单独的处理分支准备。兼容 ABI 有相应转换,不应直接假设所有平台上的结构布局相同。
对于本文的 QUERYBUF,常规错误不会继续走成功回写路径。即使内部填充已经成功,最终回写仍可能因为目标地址不可写而失败。只有系统调用成功返回,应用才应使用本次查询结果。
对应实现:__verify_planes_array()、check_array_args()、video_get_user()、video_usercopy() 中的数组替换与恢复,以及 __fill_v4l2_buffer() 的多平面分支。
5. offset 从哪里来:它是在逻辑空间里给平面排号
5.1 偏移早在分配阶段就已经安排好
QUERYBUF 不会临时计算一个物理地址。MMAP 缓冲区建立时,__setup_offsets() 已经为各平面填写 m.offset:
staticvoid __setup_offsets(struct vb2_buffer *vb)
{
structvb2_queue *q = vb->vb2_queue;
unsignedint plane;
unsignedlong off = 0;
if (vb->index) {
structvb2_buffer *prev = q->bufs[vb->index - 1];
structvb2_plane *p = &prev->planes[prev->num_planes - 1];
off = PAGE_ALIGN(p->m.offset + p->length);
}
for (plane = 0; plane < vb->num_planes; ++plane) {
vb->planes[plane].m.offset = off;
dprintk(q, 3, "buffer %d, plane %d offset 0x%08lx\n",
vb->index, plane, off);
off += vb->planes[plane].length;
off = PAGE_ALIGN(off);
}
}
第一块缓冲区从零开始。若不是第一块,就找到前一个缓冲区的最后一个平面,把“原偏移加原长度”按页对齐,作为本块的开始位置。
同一缓冲区有多个平面时,每写入一个平面的起点,就加上该平面的逻辑长度,再按页对齐,留给下一个平面。
这些偏移组成的是用于匹配的逻辑空间。两个偏移相邻,不证明对应存储页物理连续;两块 vmalloc 存储也不必处在同一段内核虚拟区间中。
5.2 用非整页长度看清页对齐的作用
RGB24 主例的 921600 字节恰好是 4096 的整数倍,容易掩盖页对齐。另设一个只用于计算的双平面例子:每块的两个平面长度分别为 5000 和 3000 字节,页大小为 4096。
第一平面需要覆盖两页;第二平面的逻辑起点因此安排在 8192。再向后给下一个缓冲区排号,就得到:

图 4:这里的 5000 和 3000 只是解释偏移算法的长度,不是前面 RGB24 格式的另一套容量,也不是某个指定 YUV 格式。
应用要映射 buffer 1 的 plane 0,就使用查询得到的 offset = 12288、length = 5000。在 4096 字节页大小下,VMA 中的页偏移是 3;VB2 将它还原成字节偏移,再与各平面的选择值匹配。
5.3 offset 的三个使用约束
保持原值。 程序应使用 QUERYBUF 返回的偏移,不自行按索引乘长度计算。不同平面长度、页对齐和实现变化都会影响排号。
不要把它当成平面内部位移。 给某个返回值加一页,可能没有匹配项,也可能恰好选中了别的平面。本实现按起点精确匹配,不支持用这种方法任意选择同一平面的中间区域。
不要把它当成 DMA 地址。 设备访问地址属于设备地址空间,由相应内存后端和 DMA API 提供。本例的 offset 只为 mmap 选择对象服务。
还要留意名称相似的 mem_ops->cookie。它返回后端约定的信息,与 planes[p].m.offset 这种应用映射标识不是同一个接口。本篇不使用前者来解释 mmap。
6. mmap 的参数怎样进入内核?
6.1 应用真正执行的调用
单平面查询成功后,可以这样建立映射:
void *addr = mmap(NULL, b.length,
PROT_READ | PROT_WRITE,
MAP_SHARED, fd, (off_t)b.m.offset);
if (addr == MAP_FAILED) {
perror("mmap");
/* 只解除此前已经成功建立的映射。 */
}
第一个参数为 NULL,让系统选择虚拟地址。length 和 offset 使用查询结果。这里选择共享映射,并请求读写权限;文件也相应以 O_RDWR 打开。
MAP_SHARED 表达对目标存储的共享映射语义,不是把四个缓冲区合并起来,也不是调用一次就为所有打开者建立地址。每次调用仍然针对当前进程中的一个映射区间。
失败判断必须使用 MAP_FAILED,不能用 addr == NULL 代替。普通应用应按接口约定映射完整平面,不主动依赖某个后端可能接受的短映射行为。
6.2 VMA 记录这次访问区间
通用内存管理根据调用参数准备一个 struct vm_area_struct,通常简称 VMA。它记录的是映射区间的管理信息,不是图像本身。
这条路径中重要的成员为:
vm_startvm_end | |
vm_pgoff | |
vm_flags | |
vm_file | |
vm_opsvm_private_data |
应用传给 mmap 的 offset 是字节数,而 vm_pgoff 是页数。VB2 入口通过左移 PAGE_SHIFT 将其还原。页大小不应写死到通用应用代码里;本篇数字例子中的 4096 均为明确假设。
6.3 文件入口只负责检查与转发
当前 V4L2 核心的映射包装如下:
staticintv4l2_mmap(struct file *filp, struct vm_area_struct *vm)
{
structvideo_device *vdev = video_devdata(filp);
int ret = -ENODEV;
if (!vdev->fops->mmap)
return -ENODEV;
if (video_is_registered(vdev))
ret = vdev->fops->mmap(filp, vm);
if (vdev->dev_debug & V4L2_DEV_DEBUG_FOP)
dprintk("%s: mmap (%d)\n",
video_device_node_name(vdev), ret);
return ret;
}
它检查是否有驱动映射回调,以及节点是否仍处于注册状态,然后转发。回调缺失或当前已注销时,本函数返回 -ENODEV;其他错误可能由更早的通用内存管理或后面的 VB2、后端产生。
VIMC 将映射回调接到 vb2_fop_mmap():
intvb2_fop_mmap(struct file *file, struct vm_area_struct *vma)
{
structvideo_device *vdev = video_devdata(file);
return vb2_mmap(vdev->queue, vma);
}
于是完整的入口关系是:
mmap 系统调用的通用内存管理路径
→ file->f_op->mmap,即 v4l2_mmap
→ vdev->fops->mmap,即 vb2_fop_mmap
→ vb2_mmap
→ q->mem_ops->mmap,即当前的 vb2_vmalloc_mmap
这条调用链不进入 video_ioctl2(),也不使用 struct v4l2_buffer 作为映射回调参数。查询所得的描述,已经被应用转换成了 mmap 的长度、偏移等参数。
7. vb2_mmap():怎样从 offset 找到正确平面

图 5:权限检查失败时尚未持有队列的映射锁;进入锁内以后,无论查找、范围检查还是后端失败,都经过统一的解锁出口。
7.1 先检查映射属性
函数开头还原偏移:
unsignedlong off = vma->vm_pgoff << PAGE_SHIFT;
接着要求 VM_SHARED。对于 CAPTURE,至少需要 VM_READ;对于 OUTPUT,至少需要 VM_WRITE。不满足时返回 -EINVAL,这部分发生在获取 q->mmap_lock 之前。
这是当前 VB2 核心的检查条件,不是完整的应用兼容性建议。V4L2 mmap 接口建议使用 PROT_READ | PROT_WRITE;通用文件权限、其他内存后端也可能带来额外限制。示例程序使用接口建议的组合。
7.2 获取 q->mmap_lock 后,再查找对象
核心取得内部映射互斥锁,再调用:
ret = __find_plane_by_offset(q, off, &buffer, &plane);
if (ret)
goto unlock;
查找函数的完整实现如下:
staticint __find_plane_by_offset(struct vb2_queue *q, unsignedlong off,
unsignedint *_buffer, unsignedint *_plane)
{
structvb2_buffer *vb;
unsignedint buffer, plane;
/*
* Sanity checks to ensure the lock is held, MEMORY_MMAP is
* used and fileio isn't active.
*/
lockdep_assert_held(&q->mmap_lock);
if (q->memory != VB2_MEMORY_MMAP) {
dprintk(q, 1, "queue is not currently set up for mmap\n");
return -EINVAL;
}
if (vb2_fileio_is_active(q)) {
dprintk(q, 1, "file io in progress\n");
return -EBUSY;
}
/*
* Go over all buffers and their planes, comparing the given offset
* with an offset assigned to each plane. If a match is found,
* return its buffer and plane numbers.
*/
for (buffer = 0; buffer < q->num_buffers; ++buffer) {
vb = q->bufs[buffer];
for (plane = 0; plane < vb->num_planes; ++plane) {
if (vb->planes[plane].m.offset == off) {
*_buffer = buffer;
*_plane = plane;
return0;
}
}
}
return -EINVAL;
}
lockdep_assert_held() 用于检查持锁约定,不会代替调用方获取这把锁。
接下来先确认当前队列采用 MMAP,且没有活动的 fileio 辅助流,再遍历有效缓冲区及其平面。只有 m.offset == off 才是匹配,不会把传入值除以某个固定长度来猜索引。
队列中没有缓冲区时,遍历不到任何匹配项,最终返回 -EINVAL。此函数也服务于相关的其他入口;本篇限定启用 MMU 的普通文件 mmap 路径,不将无 MMU 的 get_unmapped_area 分支混入图中。
7.3 检查的是页级范围是否越界
找到对象以后,核心执行:
length = PAGE_ALIGN(vb->planes[plane].length);
if (length < (vma->vm_end - vma->vm_start)) {
dprintk(q, 1,
"MMAP invalid, as it would overflow buffer length\n");
ret = -EINVAL;
goto unlock;
}
vb->planes[plane].length 仍然是逻辑容量;分配时传给后端的大小已经页对齐,因此这里也按相同边界比较 VMA 范围。
前面的 5000 字节平面会覆盖 8192 字节的页级区间。应用按查询值传入 5000,通用内存管理按页建立 VMA,这与此处检查相容。
但需要区分接口约定与实现条件:V4L2 应用应当原样使用查询返回的完整长度;这份核心代码只比较是否越界,并没有要求长度严格相等,所以不能宣称“所有短映射一定在这里失败”。接受某种参数组合,不等于它就是可移植的使用方式。
页尾多出来的空间也不是另一段有效图像。处理图像仍然以容量、实际载荷和格式约定为准。
7.4 为什么进入后端以前把 vm_pgoff 改成零?
vma->vm_pgoff = 0;
ret = call_memop(vb, mmap, vb->planes[plane].mem_priv, vma);
因为选择工作已经完成。VB2 已经根据队列级 offset 找到了具体平面,并把这个平面的 mem_priv 交给后端。后端应该从该平面存储的起点建立映射,不能再次把 12288 之类的队列级选择值当成平面内部位移。
这是两个连续步骤:先选对象,再映射对象。 把两种偏移混在一起,会从错误的位置开始映射,或者让后端错误地判断容量不足。
7.5 后端返回以后,先解锁再返回
unlock:
mutex_unlock(&q->mmap_lock);
if (ret)
return ret;
dprintk(q, 3, "buffer %d, plane %d successfully mapped\n", buffer, plane);
return0;
}
后端的成功与失败都经过 unlock。此处返回的 0 是文件映射回调的成功状态,不是应用最后得到的映射地址。
通用内存管理还要完成映射登记和返回。它可以在驱动回调之后继续检查并在失败时回滚,所以“后端打印成功”与“应用一定取得地址”也不是同一个时间点。
对应实现:vb2_mmap()、__find_plane_by_offset();通用 VMA 和后续错误回滚用 Linux v6.1 的 mm/mmap.c 补充解释,具体架构系统调用入口不在本篇固定。
8. vb2_vmalloc_mmap():把已有存储页接进应用地址空间
8.1 后端收到的是哪个对象?
mem_priv 指向 struct vb2_vmalloc_buf。这个对象已经在前一节的分配过程中建立,其中 vaddr 保存内核虚拟地址,size 保存后端分配大小,refcount 记录后端存储持有关系。
因此,下面的映射函数并没有再次调用 vmalloc_user():
staticintvb2_vmalloc_mmap(void *buf_priv, struct vm_area_struct *vma)
{
structvb2_vmalloc_buf *buf = buf_priv;
int ret;
if (!buf) {
pr_err("No memory to map\n");
return -EINVAL;
}
ret = remap_vmalloc_range(vma, buf->vaddr, 0);
if (ret) {
pr_err("Remapping vmalloc memory, error: %d\n", ret);
return ret;
}
/*
* Make sure that vm_areas for 2 buffers won't be merged together
*/
vma->vm_flags |= VM_DONTEXPAND;
/*
* Use common vm_area operations to track buffer refcount.
*/
vma->vm_private_data = &buf->handler;
vma->vm_ops = &vb2_common_vm_ops;
vma->vm_ops->open(vma);
return0;
}
空后端对象会返回 -EINVAL。正常情况下,它先调用 remap_vmalloc_range(),成功后才设置 VMA 的回调与私有关联,并增加初次映射引用。
8.2 remap_vmalloc_range() 不是一次整帧 memcpy
vmalloc 建立的是连续的内核虚拟区间,其底层物理页不必连续。映射辅助函数需要把这段虚拟区间所关联的页,接到应用的 VMA 中。
在用于补充解释的 Linux v6.1 通用实现里,这条路径验证 vmalloc 区域、映射资格、地址对齐及范围,再逐页通过 vmalloc_to_page() 取得页,调用 vm_insert_page() 建立对应的应用映射。
它操作页的关联,不把每个像素读取后再写入另一幅图像。成功后,图 1 中的 K 和 A 可以访问同一组存储页。某些错误可能发生在部分页已经处理以后,通用内存管理负责撤销失败的映射范围,应用不能使用 MAP_FAILED 对应的地址。
这段说明止于后端调用的通用映射契约,不把 v6.1 的整棵内存管理实现视为本文 VB2 源码的版本证明。
8.3 vm_ops->open() 管的是映射引用

图 6:左侧为本次映射过程,右侧为安装的关联和后续关闭回调。虚拟区间没有再申请一份完整像素存储。
后端在成功映射后设置 VM_DONTEXPAND,限制这类映射扩展;这不是存储引用计数。vm_private_data 和 vm_ops 则用于安排后续的持有与释放。
分配阶段已将 handler 的成员指向相应对象:
buf->handler.refcount = &buf->refcount;
buf->handler.put = vb2_vmalloc_put;
buf->handler.arg = buf;
因此,VMA 保存的 &buf->handler 能够找到计数器、释放函数和作为释放参数的后端对象。
vb2_common_vm_open() 的核心动作是:
structvb2_vmarea_handler *h = vma->vm_private_data;
refcount_inc(h->refcount);
首次建立映射时,后端显式调用一次 vm_ops->open(vma),取得这次映射的持有关系。以后如果产生映射继承或拆分等额外 VMA,通用内存管理还可能再次调用对应回调。
这里的 open 不是再次打开 /dev/videoX,不会额外创建一个 v4l2_fh,也不会重新申请缓冲区池。
8.4 解除映射怎样归还引用
关闭回调的核心为:
structvb2_vmarea_handler *h = vma->vm_private_data;
h->put(h->arg);
对应的后端释放函数如下:
staticvoidvb2_vmalloc_put(void *buf_priv)
{
structvb2_vmalloc_buf *buf = buf_priv;
if (refcount_dec_and_test(&buf->refcount)) {
vfree(buf->vaddr);
kfree(buf);
}
}
每次 put 归还一份后端持有关系。只有计数归零,才释放像素存储和后端对象。这里的计数不是 fd 数量,也不是 struct file 的引用计数,更不是缓冲区已经采集过多少帧。
9. 映射建立以后,还要理解它怎样结束
9.1 一次映射的最简单引用过程
只看一个平面,没有导出、继承或拆分时:分配后后端引用为 1;mmap 成功增加到 2;解除映射降回 1;最后队列释放自己的引用,才归零。
当前实现还声明支持 V4L2_BUF_CAP_SUPPORTS_ORPHANED_BUFS。在允许释放池的状态下,也可以先解除队列对存储的持有,让仍存在的映射继续保留旧存储。

图 7:这张图特意采用“先释放池、后解除映射”的顺序,解释孤儿缓冲区。示例程序采用更容易管理的“先解除映射、再释放池”。
此时,旧队列管理对象已经退出,不代表映射背后的页也已经释放。反过来,映射仍然存在,也不代表旧索引还能用于新的队列操作。
9.2 新的 index 0 不会自动接管旧映射
释放旧池再申请新池时,index 和 offset 可能再次出现相同数字,但它们属于新建的对象。旧映射仍关联旧存储,不会因为编号复用就自动改到新缓冲区。
重新建立池以后,应重新查询、重新安排映射,并让后续队列操作使用新一轮的描述。用旧地址处理新 index,只会把两轮生命周期混在一起。
对其他驱动,是否允许存在映射时释放池,应查看实际能力和返回值。不能把当前支持孤儿缓冲区的行为推广到全部设备。
9.3 close 也不等于所有映射已经解除
映射除了持有后端存储,也通常通过 vma->vm_file 持有文件对象。当前 vmalloc 后端没有替换这项文件关联。因此,关闭某个 fd 不一定立即触发最终的设备 release,也不会等价于对全部映射执行 munmap。
这里有两条不同的持有链:一条保护打开文件对象,另一条通过 VMA handler 保护后端存储。排查释放延后时,要分别检查;不能只统计执行了几次 close。
正常准备阶段的清理顺序是:解除已经成功建立的映射,释放已申请的缓冲区池,最后关闭 fd。已经进入采集运行阶段的程序,还需要在这之前正确停止实际访问与数据流。
10. owner、锁和状态标志分别保证什么?
10.1 QUERYBUF 不转移缓冲区使用权
查询不会把 QUEUED 对象取回,也不会像 DQBUF 那样交付一个完成结果。运行期间调用 QUERYBUF,可以观察接口定义的状态,但不能据此接管正在由设备使用的存储。
__fill_v4l2_buffer() 根据内部状态生成 QUEUED、DONE、ERROR 等标志。当前实现还通过 vb2_buffer_in_use() 判断是否设置 V4L2_BUF_FLAG_MAPPED。
这个辅助函数检查各平面的后端引用情况:存在 mem_priv,且 num_users() 大于 1,就认为有额外持有者。它不是一张按当前 fd 记录“是否映射过”的表;导出等其他额外引用也可能影响计数。
因此,MAPPED 不证明当前进程拥有某个有效地址,更不证明此刻可以安全读写图像。应用自身的映射表与后续 QBUF/DQBUF 交接仍然必要。状态查询也不应当被当成冻结硬件的原子快照。
10.2 owner 不是所有访问的统一开关
vb2_ioctl_querybuf() 明确不检查 owner;vb2_fop_mmap() 也没有 vb2_queue_is_busy() 这一关。它们仍受节点访问权限、标准入口、队列类型、索引、映射属性和后端约束限制。
不能因为 owner 已被设置,就断言其他已打开的上下文一定无法查询或映射这些存储。owner 用于协调特定队列操作,并不是完整的内存访问隔离机制。
本篇不重新展开文件句柄的构造,只需要将 file->private_data 看作这些队列归属判断使用的身份。多次映射同一平面也不会创造多套采集硬件或独立图像流。
10.3 QUERYBUF 的操作锁与映射锁保护不同区间
标准 QUERYBUF 带有 INFO_FL_QUEUE。在本文节点级队列路径中,标准分发优先使用非空的队列操作锁,否则按其选择规则回到节点锁。VIMC 将两者指向同一个 vcapture->lock。
vb2_querybuf() 本身不重新取得这把锁。绕过标准 ioctl 层直接调用辅助函数的代码,需要满足相应串行化前提。
mmap 不经过该 ioctl 分发。vb2_mmap() 自己获取的是 q->mmap_lock,它保护查找对象、检查映射范围和进入后端这一段,配合缓冲区池的相关管理操作。它也不同于通用内存管理中保护进程地址空间的锁,不能因为名字相似就混成一把。
10.4 两次系统调用之间仍然可能重建池

图 8:这是需要由调用方协调的并发场景,不是建议的准备顺序。另一执行路径只有满足相应 owner、状态和同步条件,才可能进行重建。
QUERYBUF 返回以后,其操作锁已经释放;mmap 后来查找的是那一刻的当前池。如果另一个允许操作同一队列的执行路径恰好释放并重建池,旧 offset 可能不再有效,也可能恰好命中新池中的某个平面。
内部锁保证单段操作的必要同步,不会自动把“查询、映射、使用、重建”合成一个跨多个系统调用的事务。应用需要让这些阶段遵循一致的生命周期安排。
11. 一次走完:从查询结果到可访问地址
仍以 buffer 1 为例,假设前面四块 RGB24 均已建立,页大小为 4096,准备期间没有池重建:
应用查询 index = 1
→ VB2 在 q->bufs[1] 找到管理对象
→ 返回 length = 921600,offset = 921600
应用 mmap(length = 921600, offset = 921600)
→ 通用 MM 准备 VMA,vm_pgoff = 225
→ VB2 将页偏移还原成 921600 字节
→ 匹配 buffer 1 / plane 0
→ 检查长度,清 vm_pgoff,调用 vmalloc 后端
→ 后端映射已有页,安装 VMA 回调,增加存储引用
→ 逐层返回;系统调用成功后,应用得到起始地址 A
保存 A 和查询返回的容量,后续才能根据 DQBUF 返回的 index 找回对应映射。多平面时保存的是 index → 每个 plane 的地址与容量,不是只有一个地址的数组。
到此仍没有一帧完成结果。VIMC 的新分配存储来自 vmalloc_user(),其初始内容清零;看见零不能用来判断图像采集是否正常。本篇没有 QBUF、STREAMON 或 DQBUF,实际数据交接留到后面的文章。
12. 实验程序:只准备映射,不启动采集
下面的完整程序读取当前格式,申请 MMAP 缓冲区,逐个查询并映射,然后清理。它兼容单平面和多平面 CAPTURE,不执行 S_FMT,不读写像素,也不启动数据流。
程序应在空闲测试节点运行。即使不启动采集,open 和 REQBUFS 也可能影响设备资源;不要在另一个程序正在使用的生产链路上随意运行。
// SPDX-License-Identifier: MIT
#define _POSIX_C_SOURCE 200809L
#define _FILE_OFFSET_BITS 64
#include<errno.h>
#include<fcntl.h>
#include<inttypes.h>
#include<stdbool.h>
#include<stdint.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/ioctl.h>
#include<sys/mman.h>
#include<time.h>
#include<unistd.h>
#include<linux/videodev2.h>
structmapping {
void *addr;
size_t length;
bool established;
};
structbuffer_map {
structmappingplane[VIDEO_MAX_PLANES];
};
staticintxioctl(int fd, unsignedlong cmd, void *arg)
{
int ret;
do {
ret = ioctl(fd, cmd, arg);
} while (ret < 0 && errno == EINTR);
return ret;
}
intmain(int argc, char **argv)
{
structv4l2_capabilitycap = {0};
structv4l2_formatfmt = {0};
structv4l2_requestbuffersreq = {0};
structbuffer_map *maps = NULL;
constchar *path;
uint32_t caps, count = 0;
bool multi = false, pool_created = false;
int fd = -1, status = EXIT_FAILURE;
if (argc > 2 || (argc == 2 && !strcmp(argv[1], "--help"))) {
fprintf(stderr, "Usage: %s [/dev/videoX]\n", argv[0]);
return argc > 2 ? EXIT_FAILURE : EXIT_SUCCESS;
}
path = argc == 2 ? argv[1] : "/dev/video0";
fd = open(path, O_RDWR | O_NONBLOCK | O_CLOEXEC);
if (fd < 0) { perror("open"); goto out; }
if (xioctl(fd, VIDIOC_QUERYCAP, &cap) < 0) {
perror("QUERYCAP"); goto out;
}
caps = (cap.capabilities & V4L2_CAP_DEVICE_CAPS)
? cap.device_caps : cap.capabilities;
if (!(caps & V4L2_CAP_STREAMING)) {
fprintf(stderr, "Streaming-buffer API is not advertised.\n");
goto out;
}
if (caps & V4L2_CAP_VIDEO_CAPTURE_MPLANE) {
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
multi = true;
} elseif (caps & V4L2_CAP_VIDEO_CAPTURE) {
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
} else {
fprintf(stderr, "A supported CAPTURE queue is required.\n");
goto out;
}
if (xioctl(fd, VIDIOC_G_FMT, &fmt) < 0) {
perror("G_FMT"); goto out;
}
printf("Using current format, type=%u; no S_FMT.\n", fmt.type);
req.type = fmt.type;
req.memory = V4L2_MEMORY_MMAP;
req.count = 4;
if (xioctl(fd, VIDIOC_REQBUFS, &req) < 0) {
perror("REQBUFS"); goto out;
}
count = req.count;
pool_created = count != 0;
printf("REQBUFS returned %u buffers.\n", count);
if (!count) {
fprintf(stderr, "No buffers returned.\n");
goto out;
}
maps = calloc(count, sizeof(*maps));
if (!maps) { perror("calloc"); goto out; }
for (uint32_t i = 0; i < count; ++i) {
structv4l2_bufferb = {0};
structv4l2_planeplanes[VIDEO_MAX_PLANES] = {0};
unsignedint nplanes;
b.type = fmt.type;
b.memory = V4L2_MEMORY_MMAP;
b.index = i;
if (multi) {
b.m.planes = planes;
b.length = VIDEO_MAX_PLANES;
}
if (xioctl(fd, VIDIOC_QUERYBUF, &b) < 0) {
perror("QUERYBUF"); goto out;
}
if (b.index != i || b.type != fmt.type ||
b.memory != V4L2_MEMORY_MMAP) {
fprintf(stderr, "Unexpected buffer identity or mode.\n");
goto out;
}
nplanes = multi ? b.length : 1;
if (!nplanes || nplanes > VIDEO_MAX_PLANES) {
fprintf(stderr, "Invalid plane count: %u\n", nplanes);
goto out;
}
printf("QUERYBUF index=%u, planes=%u, flags=%#x\n",
i, nplanes, b.flags);
for (unsignedint p = 0; p < nplanes; ++p) {
size_t len = multi ? planes[p].length : b.length;
uint32_t off = multi
? planes[p].m.mem_offset : b.m.offset;
void *addr;
if (!len) {
fprintf(stderr, "Zero plane length.\n");
goto out;
}
addr = mmap(NULL, len, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, (off_t)off);
if (addr == MAP_FAILED) { perror("mmap"); goto out; }
maps[i].plane[p].addr = addr;
maps[i].plane[p].length = len;
maps[i].plane[p].established = true;
printf(" plane=%u length=%zu offset=%" PRIu32
" address=%p\n", p, len, off, addr);
}
}
puts("All mappings established. No pixel access, QBUF or STREAMON.");
status = EXIT_SUCCESS;
out:
if (maps) {
/* 只解除已经成功的映射;长度来自对应的查询结果。 */
for (uint32_t i = count; i > 0; --i) {
for (unsignedint p = VIDEO_MAX_PLANES; p > 0; --p) {
structmapping *m = &maps[i - 1].plane[p - 1];
if (m->established && munmap(m->addr, m->length) < 0) {
perror("munmap");
status = EXIT_FAILURE;
}
}
}
free(maps);
}
if (pool_created) {
structv4l2_requestbuffersrelease = {0};
release.type = fmt.type;
release.memory = V4L2_MEMORY_MMAP;
if (xioctl(fd, VIDIOC_REQBUFS, &release) < 0) {
perror("REQBUFS(0)");
status = EXIT_FAILURE;
}
}
/* 没有启动流;Linux 上不因 close 报错而再次 close 同一个 fd。 */
if (fd >= 0 && close(fd) < 0) {
perror("close");
status = EXIT_FAILURE;
}
return status;
}
编译与运行:
cc -std=c11 -Wall -Wextra -Werror -O2 \
query_mmap_probe.c -o query_mmap_probe
./query_mmap_probe /dev/video0
_FILE_OFFSET_BITS=64 让支持该特性的 C 库使用宽文件偏移接口,避免把较大的无符号 offset 误转成窄的有符号值。循环次数使用实际返回数量,平面循环使用 QUERYBUF 返回的实际平面数;所有映射都保留自己的长度和成功标记。
某次 mmap 失败时,只解除此前已经成功的映射,不对 MAP_FAILED 调用 munmap。解除映射后,再释放成功建立的池,最后关闭设备。程序把成功条件定义为完成这一套准备与清理,不以某个固定地址或像素值判断结果。
配套程序完成了编译和受控错误路径检查,但没有在真实视频设备上执行采集或并发验证。
13. 排查失败时,先确认停在哪一层
QUERYBUF 和 mmap 不共享同一套参数检查。把错误放回具体层次,才能避免从一个 errno 猜整个硬件状态。
EINVAL | |
EFAULT | vb2_querybuf |
ENODEV | |
EACCES | |
EINVAL | |
EBUSY | |
ENOMEM | |
调试时可先在 vb2_ioctl_querybuf()、__fill_v4l2_buffer()、vb2_mmap()、vb2_vmalloc_mmap() 观察是否进入和返回,再核对 index、平面号、逻辑长度、VMA 范围与 offset。不要用 QUERYBUF 的状态轮询代替实际完成出队,也不要通过向映射写花纹来“验证”一个还在被设备使用的缓冲区。
14. 本篇结束时,哪些关系已经建立?
REQBUFS 建立的管理对象和像素存储仍然存在。QUERYBUF 把对象描述送回应用,mmap 依据描述选择平面,为它建立应用地址,并增加相应持有关系。
这里没有新建第二套图像池,也没有通过一个系统调用就完成采集。真正建立的是:
缓冲区索引
→ 查询到的平面描述
→ 用 offset 选中既有存储
→ 在本进程中建立映射
→ 保存可用于后续处理的地址和容量
下一篇进入 QBUF 时,问题才变成:这些已经存在的缓冲区,怎样从应用的准备阶段交给 VB2,又在什么条件下交给驱动处理。
实现与接口索引
本篇的 VB2 主体按固定 bufs[]、num_buffers 及所展示函数体分析。源码片段中的检查和返回顺序是讨论依据;相邻版本若修改字段或内存管理接口,需要重新核对相应实现。
drivers/media/test-drivers/vimc/vimc-capture.c | vimc_capture_fopsvimc_capture_ioctl_ops、队列与后端连接 |
drivers/media/common/videobuf2/videobuf2-v4l2.c | __fill_v4l2_buffer()、映射包装 |
drivers/media/common/videobuf2/videobuf2-core.c | |
drivers/media/common/videobuf2/videobuf2-vmalloc.c | |
drivers/media/common/videobuf2/videobuf2-memops.c | vb2_common_vm_open()vb2_common_vm_close() |
drivers/media/v4l2-core/v4l2-dev.c | v4l2_mmap() |
drivers/media/v4l2-core/v4l2-ioctl.c |
接口语义与通用内存管理补充对照 Linux v6.1。下面的接口文档用于确认应用约定,通用 MM 实现用于解释 VMA、页映射及引用,不替代本文逐段分析的 VB2 函数体。