多平面 API 也可以描述只有一个内存平面的格式。循环次数必须使用返回的平面数,不能根据 4.3
|
| 成员 | 在本次映射中的作用 |
|---|---|
vm_start、vm_end |
应用虚拟区间的起点与末尾,末尾不包含在区间中 |
vm_pgoff |
以页为单位保存偏移,进入 VB2 时用于选择平面 |
vm_flags |
记录共享、读写等映射属性 |
vm_file |
关联映射对应的文件对象 |
vm_ops、vm_private_data |
后端随后安装映射生命周期回调和私有信息 |
应用传给 mmap 的 offset 是字节数,而 vm_pgoff 是页数。VB2 入口通过左移 PAGE_SHIFT 将其还原。页大小不应写死到通用应用代码里;本篇数字例子中的 4096 均为明确假设。
当前 V4L2 核心的映射包装如下:
static int v4l2_mmap(struct file *filp, struct vm_area_struct *vm)
{
struct video_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():
int vb2_fop_mmap(struct file *file, struct vm_area_struct *vma)
{
struct video_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 的长度、偏移等参数。
vb2_mmap():怎样从 offset 找到正确平面
图 5:权限检查失败时尚未持有队列的映射锁;进入锁内以后,无论查找、范围检查还是后端失败,都经过统一的解锁出口。
函数开头还原偏移:
unsigned long 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;通用文件权限、其他内存后端也可能带来额外限制。示例程序使用接口建议的组合。
q->mmap_lock 后,再查找对象核心取得内部映射互斥锁,再调用:
ret = __find_plane_by_offset(q, off, &buffer, &plane);
if (ret)
goto unlock;
查找函数的完整实现如下:
static int __find_plane_by_offset(struct vb2_queue *q, unsigned long off,
unsigned int *_buffer, unsigned int *_plane)
{
struct vb2_buffer *vb;
unsigned int 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;
return 0;
}
}
}
return -EINVAL;
}
lockdep_assert_held() 用于检查持锁约定,不会代替调用方获取这把锁。
接下来先确认当前队列采用 MMAP,且没有活动的 fileio 辅助流,再遍历有效缓冲区及其平面。只有 m.offset == off 才是匹配,不会把传入值除以某个固定长度来猜索引。
队列中没有缓冲区时,遍历不到任何匹配项,最终返回 -EINVAL。此函数也服务于相关的其他入口;本篇限定启用 MMU 的普通文件 mmap 路径,不将无 MMU 的 get_unmapped_area 分支混入图中。
找到对象以后,核心执行:
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 应用应当原样使用查询返回的完整长度;这份核心代码只比较是否越界,并没有要求长度严格相等,所以不能宣称“所有短映射一定在这里失败”。接受某种参数组合,不等于它就是可移植的使用方式。
页尾多出来的空间也不是另一段有效图像。处理图像仍然以容量、实际载荷和格式约定为准。
vm_pgoff 改成零?vma->vm_pgoff = 0;
ret = call_memop(vb, mmap, vb->planes[plane].mem_priv, vma);
因为选择工作已经完成。VB2 已经根据队列级 offset 找到了具体平面,并把这个平面的 mem_priv 交给后端。后端应该从该平面存储的起点建立映射,不能再次把 12288 之类的队列级选择值当成平面内部位移。
这是两个连续步骤:先选对象,再映射对象。 把两种偏移混在一起,会从错误的位置开始映射,或者让后端错误地判断容量不足。
unlock:
mutex_unlock(&q->mmap_lock);
if (ret)
return ret;
dprintk(q, 3, "buffer %d, plane %d successfully mapped\n", buffer, plane);
return 0;
}
后端的成功与失败都经过 unlock。此处返回的 0 是文件映射回调的成功状态,不是应用最后得到的映射地址。
通用内存管理还要完成映射登记和返回。它可以在驱动回调之后继续检查并在失败时回滚,所以“后端打印成功”与“应用一定取得地址”也不是同一个时间点。
对应实现:vb2_mmap()、__find_plane_by_offset();通用 VMA 和后续错误回滚用 Linux v6.1 的 mm/mmap.c 补充解释,具体架构系统调用入口不在本篇固定。
vb2_vmalloc_mmap():把已有存储页接进应用地址空间mem_priv 指向 struct vb2_vmalloc_buf。这个对象已经在前一节的分配过程中建立,其中 vaddr 保存内核虚拟地址,size 保存后端分配大小,refcount 记录后端存储持有关系。
因此,下面的映射函数并没有再次调用 vmalloc_user():
static int vb2_vmalloc_mmap(void *buf_priv, struct vm_area_struct *vma)
{
struct vb2_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);
return 0;
}
空后端对象会返回 -EINVAL。正常情况下,它先调用 remap_vmalloc_range(),成功后才设置 VMA 的回调与私有关联,并增加初次映射引用。
remap_vmalloc_range() 不是一次整帧 memcpyvmalloc 建立的是连续的内核虚拟区间,其底层物理页不必连续。映射辅助函数需要把这段虚拟区间所关联的页,接到应用的 VMA 中。
在用于补充解释的 Linux v6.1 通用实现里,这条路径验证 vmalloc 区域、映射资格、地址对齐及范围,再逐页通过 vmalloc_to_page() 取得页,调用 vm_insert_page() 建立对应的应用映射。
它操作页的关联,不把每个像素读取后再写入另一幅图像。成功后,图 1 中的 K 和 A 可以访问同一组存储页。某些错误可能发生在部分页已经处理以后,通用内存管理负责撤销失败的映射范围,应用不能使用 MAP_FAILED 对应的地址。
这段说明止于后端调用的通用映射契约,不把 v6.1 的整棵内存管理实现视为本文 VB2 源码的版本证明。
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() 的核心动作是:
struct vb2_vmarea_handler *h = vma->vm_private_data;
refcount_inc(h->refcount);
首次建立映射时,后端显式调用一次 vm_ops->open(vma),取得这次映射的持有关系。以后如果产生映射继承或拆分等额外 VMA,通用内存管理还可能再次调用对应回调。
这里的 open 不是再次打开 /dev/videoX,不会额外创建一个 v4l2_fh,也不会重新申请缓冲区池。
关闭回调的核心为:
struct vb2_vmarea_handler *h = vma->vm_private_data;
h->put(h->arg);
对应的后端释放函数如下:
static void vb2_vmalloc_put(void *buf_priv)
{
struct vb2_vmalloc_buf *buf = buf_priv;
if (refcount_dec_and_test(&buf->refcount)) {
vfree(buf->vaddr);
kfree(buf);
}
}
每次 put 归还一份后端持有关系。只有计数归零,才释放像素存储和后端对象。这里的计数不是 fd 数量,也不是 struct file 的引用计数,更不是缓冲区已经采集过多少帧。
只看一个平面,没有导出、继承或拆分时:分配后后端引用为 1;mmap 成功增加到 2;解除映射降回 1;最后队列释放自己的引用,才归零。
当前实现还声明支持 V4L2_BUF_CAP_SUPPORTS_ORPHANED_BUFS。在允许释放池的状态下,也可以先解除队列对存储的持有,让仍存在的映射继续保留旧存储。

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

图 8:这是需要由调用方协调的并发场景,不是建议的准备顺序。另一执行路径只有满足相应 owner、状态和同步条件,才可能进行重建。
QUERYBUF 返回以后,其操作锁已经释放;mmap 后来查找的是那一刻的当前池。如果另一个允许操作同一队列的执行路径恰好释放并重建池,旧 offset 可能不再有效,也可能恰好命中新池中的某个平面。
内部锁保证单段操作的必要同步,不会自动把“查询、映射、使用、重建”合成一个跨多个系统调用的事务。应用需要让这些阶段遵循一致的生命周期安排。
仍以 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,实际数据交接留到后面的文章。
下面的完整程序读取当前格式,申请 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>
struct mapping {
void *addr;
size_t length;
bool established;
};
struct buffer_map {
struct mapping plane[VIDEO_MAX_PLANES];
};
static int xioctl(int fd, unsigned long cmd, void *arg)
{
int ret;
do {
ret = ioctl(fd, cmd, arg);
} while (ret < 0 && errno == EINTR);
return ret;
}
int main(int argc, char **argv)
{
struct v4l2_capability cap = {0};
struct v4l2_format fmt = {0};
struct v4l2_requestbuffers req = {0};
struct buffer_map *maps = NULL;
const char *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;
} else if (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) {
struct v4l2_buffer b = {0};
struct v4l2_plane planes[VIDEO_MAX_PLANES] = {0};
unsigned int 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 (unsigned int 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 (unsigned int p = VIDEO_MAX_PLANES; p > 0; --p) {
struct mapping *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) {
struct v4l2_requestbuffers release = {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。解除映射后,再释放成功建立的池,最后关闭设备。程序把成功条件定义为完成这一套准备与清理,不以某个固定地址或像素值判断结果。
配套程序完成了编译和受控错误路径检查,但没有在真实视频设备上执行采集或并发验证。
QUERYBUF 和 mmap 不共享同一套参数检查。把错误放回具体层次,才能避免从一个 errno 猜整个硬件状态。
| 现象 | 首先核对的位置 |
|---|---|
QUERYBUF 返回 EINVAL |
队列类型、实际数量与索引、多平面数组容量;还需看更早的格式类型检查 |
QUERYBUF 返回 EFAULT |
主结构或描述数组的输入复制与结果回写;未必已经进入 vb2_querybuf |
mmap 返回 ENODEV |
V4L2 映射回调是否存在、节点注册状态;也应检查是否有其他下层同码返回 |
mmap 返回 EACCES |
文件打开方式、映射权限与通用 MM 的检查 |
mmap 返回 EINVAL |
共享属性、读写属性、内存模式、cookie、VMA 长度,或后端对范围的拒绝 |
mmap 返回 EBUSY |
当前 VB2 查找路径中的活动 fileio 等冲突;不能简单归因于“另一个进程打开了节点” |
mmap 返回 ENOMEM |
地址空间、页表或后端映射资源不足;并不说明第五篇的像素分配从未成功 |
| QUERYBUF 后仍没有图像 | 查询与映射只完成准备;继续核对后续提交、启动和完成过程 |
| 释放池后还有内存占用 | 是否仍有映射或导出持有存储,以及是否遗漏了配对清理 |
调试时可先在 vb2_ioctl_querybuf()、__fill_v4l2_buffer()、vb2_mmap()、vb2_vmalloc_mmap() 观察是否进入和返回,再核对 index、平面号、逻辑长度、VMA 范围与 offset。不要用 QUERYBUF 的状态轮询代替实际完成出队,也不要通过向映射写花纹来“验证”一个还在被设备使用的缓冲区。
REQBUFS 建立的管理对象和像素存储仍然存在。QUERYBUF 把对象描述送回应用,mmap 依据描述选择平面,为它建立应用地址,并增加相应持有关系。
这里没有新建第二套图像池,也没有通过一个系统调用就完成采集。真正建立的是:
缓冲区索引
→ 查询到的平面描述
→ 用 offset 选中既有存储
→ 在本进程中建立映射
→ 保存可用于后续处理的地址和容量
下一篇进入 QBUF 时,问题才变成:这些已经存在的缓冲区,怎样从应用的准备阶段交给 VB2,又在什么条件下交给驱动处理。
本篇的 VB2 主体按固定 bufs[]、num_buffers 及所展示函数体分析。源码片段中的检查和返回顺序是讨论依据;相邻版本若修改字段或内存管理接口,需要重新核对相应实现。
| 实现文件 | 本篇使用的函数或对象 |
|---|---|
drivers/media/test-drivers/vimc/vimc-capture.c |
vimc_capture_fops、vimc_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 |
QUERYBUF 适配、队列选锁、参数编组与回写 |
接口语义与通用内存管理补充对照 Linux v6.1。下面的接口文档用于确认应用约定,通用 MM 实现用于解释 VMA、页映射及引用,不替代本文逐段分析的 VB2 函数体。