夜雨聆风学习资料网

ARTICLE · 1123220

V4L2 源码深度解析(五):申请 4 个缓冲区,内核到底分配了什么?

V4L2 源码深度解析(五):申请 4 个缓冲区,内核到底分配了什么?

上一节结束时,VIMC 的采集节点已经注册,队列也已经初始化。驱动知道自己支持什么格式、使用哪一种内存后端,以及以后收到缓冲区时应该调用哪些函数。但队列里还没有真正用来存放图像的缓冲区。

现在,应用发出一个很小的请求:

structv4l2_requestbuffersreq = {0};
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_MMAP;
req.count = 4;

int ret = ioctl(fd, VIDIOC_REQBUFS, &req);

这个结构体没有图像宽度,没有高度,也没有每个缓冲区的地址。内核却要据此准备好几组可以容纳图像的存储。大小从哪里来?一次分配会创建多少种对象?如果前三块成功、第四块失败,这次申请算不算成功?

本篇就沿着这次请求往下读。先把申请的含义讲清楚,再依次进入接口层、队列核心、驱动布局回调和 vmalloc 分配器。结束时,缓冲区池已经建立;查询和映射留到下一篇。

1. 申请的不是四帧图像,而是四个可重复使用的位置

1.1 先确定每个位置要有多大

延续 VIMC 的默认格式:640 × 480、RGB24,每个像素占 3 字节,行尾没有额外填充。驱动保存的格式信息为:

bytesperline = 640 × 3 = 1920 字节
sizeimage    = 1920 × 480 = 921600 字节

sizeimage 是这个格式所需的存储容量。请求 4 个缓冲区,就是希望准备 4 组这样的存储;后续采集可以轮流使用它们。buffer 0 用完以后还会再次使用,不是只对应第 0 帧。

如果四块都建立成功,仅像素区域的逻辑容量就是:

4 × 921600 = 3686400 字节 = 3.515625 MiB

这还没有计入缓冲区管理结构、分配器私有对象和页表等开销。这个例子采用固定大小的线性 RGB 布局;其他格式应使用驱动协商后的容量,不能一律按宽高乘每像素字节数计算。

1.2 REQBUFS 为什么不再传宽高

格式是在更早的阶段确定的。应用可以通过 S_FMT 协商,也可以通过 G_FMT 读取已经存在的默认格式。VIMC 将当前格式保存在 vcapture->format 中。

REQBUFS 负责的是“按照当前格式,建立多少个缓冲区”。如果它同时再携带一套独立宽高,就会产生两份需要保持一致的配置。这里的实现选择读取驱动已经保存的格式,由驱动的 queue_setup() 给出容量要求。

格式与申请之间因此有一项实际约束:申请过程中读到的格式必须稳定。VIMC 将节点操作锁和队列操作锁都指向 vcapture->lock,并在已有缓冲区时拒绝随意修改格式。锁与状态检查共同保证布局的一致性。

1.3 还需要一份记录每块存储的信息

只有像素内存,框架仍然不知道它属于哪个队列、编号是多少、有几个平面、每个平面多大。因此,每个缓冲区还需要一个管理对象。

对本篇选定的 vmalloc 后端,一块缓冲区建立后包含三类资源:

  • 缓冲区管理对象:保存索引、状态、平面描述和驱动扩展。
  • 平面分配器对象:保存这个平面的 CPU 地址、分配大小和存储引用。
  • 实际存储:用于后续写入图像的内存页。

三者的大小不同,分配者不同,释放方式也不同。后面每经过一层函数,都要看清这次拿到的是哪一种对象。

图 1:按分配结果组织的示意,不是逐函数调用图。假设请求 4 个且全部成功;尚未映射到应用,也没有采集结果。

本篇沿普通 CAPTURE、MMAP、vmalloc 分支分析,使用 min_buffers_needed 和固定 bufs[] 的这份实现。VIMC 也能选择其他内存后端;其余模式只在涉及分支时说明区别,不混入本次分配过程。

实现对照:vimc-capture.c 中的 fmt_default、vimc_capture_add()、vimc_capture_s_fmt_vid_cap()。

2. 六个函数不是一条不返回的直线

请求经过前面已经分析的标准 ioctl 入口,最终来到驱动提供的 vidioc_reqbufs。VIMC 将这个成员接到 vb2_ioctl_reqbufs()。

最适合阅读的调用关系是:

vb2_ioctl_reqbufs()
  └─ vb2_core_reqbufs()
       ├─ 调用 queue_setup 回调
       │    └─ vimc_capture_queue_setup()
       ├─ __vb2_queue_alloc()
       │    └─ 每个 MMAP 缓冲区:__vb2_buf_mem_alloc()
       │         └─ 每个平面:mem_ops->alloc()
       │              └─ vb2_vmalloc_alloc()
       ├─ 分配数量不足但达到下限时,再次调用 queue_setup
       └─ 接受实际数量,或回收本轮已经建立的对象

vimc_capture_queue_setup() 返回以后,执行位置回到 vb2_core_reqbufs(),由核心继续发起分配。它并不直接调用 __vb2_queue_alloc()。同样,分配器返回一个平面对象后,也要逐层返回,直到核心拿到完整成功的缓冲区数量。

图 2:实线表示调用,返回值标在函数框内。布局回调与分配函数由核心在不同阶段调用;数量不足时可能再次进入同一个布局回调。

三个参数类别也要在入口处认清:

type 选择当前操作的队列类型;memory 选择这次采用 MMAP、USERPTR 还是 DMA-BUF;count 表示希望建立的缓冲区数量。一个双平面缓冲区仍只占用一个 count,平面数由另一组参数描述。

这一份接口辅助函数直接调用 vb2_core_reqbufs(),**不经过名称相近的 vb2_reqbufs()**。后者是另一条以队列为参数的辅助入口,没有当前这层基于 struct file 的 owner 检查。读调用链时必须以实际调用为准。

实现对照:v4l2-ioctl.c 的 v4l_reqbufs();vimc_capture_ioctl_ops;videobuf2-v4l2.c 的两个 REQBUFS 辅助入口。

3. vb2_ioctl_reqbufs:先判断这次请求能不能进入队列

下面保留完整函数的可执行语句,只去掉说明性注释并调整缩进。

intvb2_ioctl_reqbufs(struct file *file, void *priv,
              struct v4l2_requestbuffers *p)

{
structvideo_device *vdev = video_devdata(file);
int res = vb2_verify_memory_type(vdev->queue, p->memory, p->type);
    u32 flags = p->flags;

    fill_buf_caps(vdev->queue, &p->capabilities);
    validate_memory_flags(vdev->queue, p->memory, &flags);
    p->flags = flags;
if (res)
return res;
if (vb2_queue_is_busy(vdev->queue, file))
return -EBUSY;
    res = vb2_core_reqbufs(vdev->queue, p->memory, p->flags, &p->count);

if (res == 0)
        vdev->queue->owner = p->count ? file->private_data : NULL;
return res;
}

3.1 验证模式,也验证实际能力

vb2_verify_memory_type() 检查的不只是枚举值。它先确认 memory 是允许的内存方式,再确认请求的 type 与队列配置一致。

对 MMAP,它还要求队列声明 VB2_MMAP,并提供 alloc、put、mmap 三个内存操作。申请时虽然不会立即调用 mmap,框架仍然检查这是一套能够完成后续访问与释放的配置。少了其中一项,就不是完整的 MMAP 能力。

最后还会检查 read/write 辅助流是否占用了队列。这些检查发生在真正分配像素存储之前;对应失败通常是 EINVAL 或 EBUSY,不能一看到 REQBUFS 失败就判断“内存不够”。

3.2 capabilities 与 flags 分别表达什么

fill_buf_caps() 填写的是队列支持的缓冲区接口能力,例如 MMAP、USERPTR、DMA-BUF,以及是否支持释放队列后保留额外持有的存储。它不是图像像素格式列表。

validate_memory_flags() 则整理本次申请的内存提示。当前实现中,队列未开放缓存提示,或者不是 MMAP 时,会清零这些标志;其他情况下仅保留已知的非一致性内存提示。

本篇请求结构先清零,因此 flags == 0。不需要为了“优化性能”随意设置 V4L2_MEMORY_FLAG_NON_COHERENT。这个位不会替代后续 CPU 与设备之间的缓存交接。

函数在检查 res 之前已经填写了部分返回字段,但最终错误是否带回这些字段,还取决于外层 ioctl 回写规则。不能把“在内核里赋过值”当成“应用一定收到这个值”。

3.3 owner 只负责当前操作的归属检查

队列已有 owner,且它不同于当前 file->private_data 时,vb2_queue_is_busy() 会拒绝这次申请。独立打开和 dup 的关系已经在第三篇讲过;此处只需要把 private_data 理解成打开上下文身份。

检查通过以后,接口层进入核心。核心返回 0,才根据返回的 p->count 更新 owner:数量非零,记录当前上下文;数量为零,清空 owner。核心失败则不执行这次赋值。

这并不是“只有 owner 才能调用节点上的一切接口”。owner 检查由特定辅助入口执行,本篇只讨论它如何约束缓冲区申请。

3.4 为什么 count 要传地址

&p->count 使核心能把实际成功数量写回同一个请求对象。比如应用请求 4 个,核心可能建立 3 个,也可能根据最低要求把请求 1 个调整为 2 个。成功返回后,后续操作应使用返回数量。

这里修改的是外层 ioctl 已经准备好的内核参数对象,不是直接在驱动深处解引用应用地址。最终仍由外层完成回写。

实现对照:vb2_verify_memory_type()、fill_buf_caps()、validate_memory_flags()、vb2_ioctl_reqbufs();owner 的辅助判断见配套 videobuf2-v4l2.h。

4. vb2_core_reqbufs:先整理当前状态,再决定怎样分配

核心函数既能建立第一批缓冲区,也能释放或重建已有缓冲区池。为了理解后面的分配,先看它在前半段完成哪些准备。

4.1 正在采集时,不能直接换掉池

入口首先检查:

if (q->streaming) {
    dprintk(q, 1, "streaming active\n");
return -EBUSY;
}

if (q->waiting_in_dqbuf && *count) {
    dprintk(q, 1, "another dup()ped fd is waiting for a buffer\n");
return -EBUSY;
}

第一项拒绝 streaming 状态下的所有 REQBUFS,包括 count == 0。对这条实现,应先通过停止接口结束流,再释放或重建缓冲区池,不要用 REQBUFS(0) 无条件替代 STREAMOFF。

第二项检查是否存在正在等待 DQBUF 的执行路径。它只拒绝非零重建;不是统计打开了多少个 fd,而是防止在等待者仍使用当前队列时替换缓冲区安排。

4.2 第一轮申请与再次申请在哪里分开

随后有一个条件分支:

if (*count == 0 || q->num_buffers != 0 ||
    (q->memory != VB2_MEMORY_UNKNOWN && q->memory != memory) ||
    !verify_coherency_flags(q, non_coherent_mem)) {
/* 整理原队列、释放原有缓冲区的分支。 */
}

这是条件节选,花括号内的说明不是原函数的完整实现。

请求数量为零、原池非空、已选内存方式不同,或者内存一致性属性不匹配,都可能进入这个分支。分支中的主要动作是:取得 mmap_lock,取消已有排队安排,调用 __vb2_queue_free() 清理旧对象,释放锁并检查返回值。

这里还要取消“已经准备或排队,但尚未 STREAMON”的对象。未启动采集,不代表不存在需要收回的队列状态。

旧池清理成功后,如果请求数量为零,核心立即返回,不再进入布局回调和新分配。非零请求则继续建立新池。再次 REQBUFS 的含义是重新建立池,不是向旧池追加几个对象。

4.3 核心先给数量划定一个范围

在新一轮分配前,源码执行:

WARN_ON(q->min_buffers_needed > VB2_MAX_FRAME);
num_buffers = max_t(unsignedint, *count, q->min_buffers_needed);
num_buffers = min_t(unsignedint, num_buffers, VB2_MAX_FRAME);
memset(q->alloc_devs, 0, sizeof(q->alloc_devs));

这里的 num_buffers 是准备交给驱动的目标数量,还不是成功数量。

VIMC 把 min_buffers_needed 设为 2。因此请求 1 个时,核心首先按 2 个考虑;请求 4 个时仍按 4 个考虑,前提是没有超过当前实现的上界。min_buffers_needed 也参与后续启动判断,但在这里已经影响分配数量,不能认为它只与 STREAMON 有关。

VB2_MAX_FRAME 是当前内核实现的容量边界,不应把一个版本的数值写成永远固定的应用接口保证。

4.4 回调之前先设置 memory

接下来先在 mmap_lock 下更新 q->memory,再设置相应的一致性属性:

mutex_lock(&q->mmap_lock);
q->memory = memory;
mutex_unlock(&q->mmap_lock);
set_queue_coherency(q, non_coherent_mem);

这样,驱动在 queue_setup() 中读取 q->memory,看到的就是本轮申请模式,而不是上一次或初始的 UNKNOWN。

注意锁的范围:这几条语句不意味着整个核心函数始终持有 mmap_lock。布局回调和接下来的长时间分配,不在这一段内部锁里。具体锁范围在后文一起核对。

图 3:只画核心的前半段。清理错误必须先释放实际持有的锁;零数量成功返回接口层以后,仍会按规则清空 owner。

实现对照:vb2_core_reqbufs()、set_queue_coherency()、verify_coherency_flags()、__vb2_queue_free()。

5. vimc_capture_queue_setup:图像大小从当前格式里取出来

5.1 核心把需要确定的量交给驱动

第一次进入布局回调时,核心准备的 num_planes 为 0,plane_sizes[] 清零,然后调用:

ret = call_qop(q, queue_setup, q, &num_buffers, &num_planes,
              plane_sizes, q->alloc_devs);

在 VIMC 配置下,这次调用最终进入:

staticintvimc_capture_queue_setup(struct vb2_queue *vq, unsignedint *nbuffers,
unsignedint *nplanes, unsignedint sizes[],
                struct device *alloc_devs[])

{
structvimc_capture_device *vcapture = vb2_get_drv_priv(vq);

if (*nplanes)
return sizes[0] < vcapture->format.sizeimage ? -EINVAL : 0;

    *nplanes = 1;
    sizes[0] = vcapture->format.sizeimage;

return0;
}

call_qop 是调用包装。忽略可选的调试计数,实际连接来自 q->ops->queue_setup,并不是框架根据“vimc”这个名字自动搜索函数。

5.2 每个输出参数要回答一个不同的问题

nbuffers 指向目标缓冲区数量。驱动可以根据设备要求确认或调整它,但 VIMC 这段代码没有修改这个值。默认最低数量 2 来自队列配置和核心处理,不是这个回调偷偷把数量改成 2。

nplanes 指向每个缓冲区的内存平面数。首次 REQBUFS 以 0 进入,表示需要驱动给出布局;VIMC 设置为 1。

sizes[] 保存每个平面的容量要求。这里将 sizes[0] 设为 vcapture->format.sizeimage,所以当前默认格式会给出 921600 字节。

alloc_devs[] 允许驱动按平面指定分配设备。VIMC 不填写它,前面核心又已经将数组清零,因此后续选择默认的 q->dev。

这四种信息共同描述“应该怎样分配”,但回调里并没有分配像素页,也没有返回像素地址。

5.3 从 queue 找到当前格式的路径

vb2_get_drv_priv(vq) 取出队列的 drv_priv。初始化时,VIMC 已经把它设置为 vcapture。所以实际的数据路径是:

当前队列 q
  → q->drv_priv
  → vcapture
  → vcapture->format.sizeimage
  → sizes[0]

这里不通过 file->private_data 找格式。前者是设备级对象,后者是打开级上下文,刚好承担不同的任务。

5.4 为什么还有 nplanes 非零的分支

同一个 queue_setup() 也供其他建立路径使用。*nplanes 非零时,调用方已经提供了平面布局,VIMC 在这里检查 sizes[0] 能否容纳当前图像;容量不足返回 -EINVAL。

本次普通 REQBUFS 首次调用不是这条分支。稍后分配数量不足时,核心会再次将 num_planes 置零,因此第二次数量复核仍然保持 REQBUFS 的调用形式。不能把“第二次调用”自动解释成“进入 nplanes 非零分支”。

5.5 核心会检查输出,但驱动仍要正确计算

回调成功后,核心检查平面数非零,并逐项检查容量非零。正确驱动还必须保证平面数没有超过数组容量,尺寸计算没有溢出,整个布局与当前格式一致。

这份实现不能替驱动证明所有输出都安全;尤其不要假定非零检查会自动纠正过大的平面数,或者恢复已经在早期乘法中溢出的 sizeimage。

实现对照:vimc_capture_queue_setup()、vimc_capture_add();vb2_core_reqbufs() 的第一次 queue_setup 与返回检查。

6. __vb2_queue_alloc:先给每一块存储建立管理对象

布局确定以后,核心调用:

allocated_buffers =
    __vb2_queue_alloc(q, memory, num_buffers, num_planes, plane_sizes);

这个函数返回的是完整建立的缓冲区数量。它不以负错误码直接告诉外层某一次分配为什么失败,这一点会影响后面的错误处理。

6.1 外层循环负责缓冲区数量

先看从数量限制到登记槽位的连续节选:

num_buffers = min_t(unsignedint, num_buffers,
                    VB2_MAX_FRAME - q->num_buffers);

for (buffer = 0; buffer < num_buffers; ++buffer) {
    vb = kzalloc(q->buf_struct_size, GFP_KERNEL);
if (!vb) {
        dprintk(q, 1, "memory alloc for buffer struct failed\n");
break;
    }

    vb->state = VB2_BUF_STATE_DEQUEUED;
    vb->vb2_queue = q;
    vb->num_planes = num_planes;
    vb->index = q->num_buffers + buffer;
    vb->type = q->type;
    vb->memory = memory;
    init_buffer_cache_hints(q, vb);
for (plane = 0; plane < num_planes; ++plane) {
        vb->planes[plane].length = plane_sizes[plane];
        vb->planes[plane].min_length = plane_sizes[plane];
    }
    call_void_bufop(q, init_buffer, vb);

    q->bufs[vb->index] = vb;

节选到登记槽位为止,循环的 MMAP 分支接在后面。

函数再次限制数量,是因为它也会服务于追加对象等其他路径;不能让“已有数量 + 本轮数量”越过表容量。本次 REQBUFS 已经清理旧池,索引通常从 0 开始,而完整表达式仍是 q->num_buffers + buffer。

6.2 kzalloc 分配了多大的对象

q->buf_struct_size 决定这次小对象的大小。VIMC 设置的是:

q->buf_struct_size = sizeof(struct vimc_capture_buffer);

其中内嵌了 vb2_v4l2_buffer,再向内包含通用的 vb2_buffer。一次分配即可容纳这几层管理信息以及驱动自己的链表成员,不需要再分别分配三份管理结构。

这次分配的大小随结构布局而定,不随 640 × 480 的像素数量增长。真正需要 921600 字节的像素存储,还没有在这里建立。

6.3 新对象为什么是 DEQUEUED

新建对象尚未通过 QBUF 提交,所以初始状态是 VB2_BUF_STATE_DEQUEUED。这里的名字描述当前归属状态,不要求它以前已经出队过一次。

length 记录这次建立的平面容量,min_length 保存最低容量要求,供后续外部存储等路径核验。它们此时来自同一个 plane_sizes[p],以后承担的检查职责可以不同。

6.4 init_buffer 和 buf_init 不是同一个回调

call_void_bufop(q, init_buffer, vb) 进入接口适配层。V4L2 的这项回调将扩展对象中的 request_fd 设为 -1,属于接口初值准备。

后面可能调用的 q->ops->buf_init 则属于驱动,用于在像素存储建立以后准备驱动附加状态。名称相似,但调用位置、回调表和资源前提都不同。

VIMC 的队列操作表没有设置 buf_init,所以本篇实例在那个位置不执行一个额外的 VIMC 初始化函数。通用代码保留这项可选能力,不代表每个驱动都会用到。

6.5 MMAP 分支才继续申请像素存储

循环的后半段如下。说明性注释已去掉:

if (memory == VB2_MEMORY_MMAP) {
        ret = __vb2_buf_mem_alloc(vb);
if (ret) {
            dprintk(q, 1, "failed allocating memory for buffer %d\n",
                    buffer);
            q->bufs[vb->index] = NULL;
            kfree(vb);
break;
        }
        __setup_offsets(vb);
        ret = call_vb_qop(vb, buf_init, vb);
if (ret) {
            dprintk(q, 1, "buffer %d %p initialization failed\n",
                    buffer, vb);
            __vb2_buf_mem_free(vb);
            q->bufs[vb->index] = NULL;
            kfree(vb);
break;
        }
    }
}

USERPTR 和 DMA-BUF 也需要管理对象,但其存储来自外部,不在这段 MMAP 分支中统一调用 alloc。本篇不把三种模式混作同一种像素分配。

MMAP 平面存储成功后,__setup_offsets() 安排后续查询与映射使用的偏移标识;这一步尚未建立应用映射。下一篇再分析偏移怎样被 QUERYBUF 返回、被 mmap 使用。

图 4:一个缓冲区只有全部平面成功、可选驱动初始化也成功,才会计入成功数量。发生失败后,外层循环停止,不跳过这个索引继续分配下一块。

实现对照:__vb2_queue_alloc()、__init_vb2_v4l2_buffer()、vimc_capture_qops。

7. __vb2_buf_mem_alloc:逐个平面申请,逐个平面回收

7.1 内层循环负责平面数量

一个缓冲区有几个平面,就在这里调用几次内存后端。完整可执行语句如下:

staticint __vb2_buf_mem_alloc(struct vb2_buffer *vb)
{
structvb2_queue *q = vb->vb2_queue;
void *mem_priv;
int plane;
int ret = -ENOMEM;

for (plane = 0; plane < vb->num_planes; ++plane) {

unsignedlong size = PAGE_ALIGN(vb->planes[plane].length);

if (size < vb->planes[plane].length)
gotofree;

        mem_priv = call_ptr_memop(alloc,
                      vb,
                      q->alloc_devs[plane] ? : q->dev,
                      size);
if (IS_ERR_OR_NULL(mem_priv)) {
if (mem_priv)
                ret = PTR_ERR(mem_priv);
gotofree;
        }

        vb->planes[plane].mem_priv = mem_priv;
    }

return0;
free:

for (; plane > 0; --plane) {
        call_void_memop(vb, put, vb->planes[plane - 1].mem_priv);
        vb->planes[plane - 1].mem_priv = NULL;
    }

return ret;
}

本篇 VIMC 只有一个平面,因此每个缓冲区进入后端一次。换成双平面驱动时,4 个缓冲区对应外层 4 次、内层每次 2 次;不是申请 8 个缓冲区。

7.2 为什么在这里做 PAGE_ALIGN

内存映射按页管理,所以传给 MMAP 后端的大小必须覆盖整页。PAGE_ALIGN(length) 将逻辑容量向上取整到页边界。

假定页大小为 4096 字节。一个用于说明对齐的 5000 字节平面,需要后端提供 8192 字节;平面描述的 length 仍然是 5000,不会因为后端收到 8192 就被这段代码改写。

本篇 RGB24 的 921600 字节恰好是 225 页,所以在 4096 字节页大小下,没有额外的页尾取整容量。四块像素区域合计对应 900 个基础页大小的容量;这不约束内存管理器内部采用哪种页分配组合。

逻辑容量、分配时的页对齐长度、管理结构大小,应分别记录。 映射篇还会在这个基础上解释 VMA 的范围。

7.3 页对齐也可能溢出

源码用 size < length 检查向上取整发生回绕的情况。如果一个错误尺寸接近类型上界,取整后反而得到很小的数,就不能继续按这个小尺寸分配。

这个检查只保护当前的页对齐运算。若驱动更早计算宽、高和字节数时已经溢出,最后得到一个看似正常的小 length,这里无法反推出原本应该多大。

7.4 怎样选择分配设备

表达式 q->alloc_devs[plane] ? : q->dev 使用 GNU C 的简写:当前平面有专用设备,就使用它;否则采用队列默认设备。

call_ptr_memop(alloc, vb, dev, size) 最终调用所选后端的 alloc(vb, dev, size)。不同后端可以根据设备的 DMA 限制工作;但本篇的 vmalloc 分配函数没有使用 dev 去建立设备 DMA 映射。不能把“传入了一个 device 指针”自动解释成“已经取得 DMA 地址”。

7.5 返回的是后端对象,不是像素地址

成功后,返回值保存到 vb->planes[plane].mem_priv。它是后端的私有管理对象。后续 put、mmap、vaddr 等操作再把同一个对象交回该后端解释。

错误可能表现为 NULL 或 ERR_PTR(-errno)。IS_ERR_OR_NULL() 同时识别这两种形式,PTR_ERR() 只在存在错误指针时提取错误码。不能只检查 mem_priv == NULL,否则可能把编码了错误值的指针当成有效对象。

7.6 当前平面失败,只先回收当前缓冲区

假设一个三平面缓冲区的 plane 0、plane 1 成功,plane 2 失败。free 分支先对已经成功的平面逆序调用 put,再清空 mem_priv,最后返回错误。

回到 __vb2_queue_alloc() 后,当前不完整的管理对象被释放,槽位清空,外层循环停止。更早完整建立的其他缓冲区暂时保留,交由最外层决定是否接受较少数量。

所以这是一种分层回滚:先撤销当前平面的半成品,再撤销当前缓冲区,最后才决定是否撤销整批。缺少一个平面的对象,绝不能计入“已经分配成功”。

实现对照:__vb2_buf_mem_alloc()、__vb2_buf_mem_free()、call_ptr_memop、call_void_memop。

8. vb2_vmalloc_alloc:像素存储终于在这一层建立

8.1 先看后端自己的管理对象

vmalloc 后端的对象定义如下:

structvb2_vmalloc_buf {
void *vaddr;
structframe_vector *vec;
enum dma_data_direction dma_dir;
unsignedlong size;
refcount_t refcount;
structvb2_vmarea_handlerhandler;
structdma_buf *dbuf;
};

这份结构也服务于该后端的其他内存路径,所以包含 vec、dbuf 等成员。本次 MMAP alloc 主要填写 vaddr、size、dma_dir、引用计数和 VMA 管理所需的 handler。

size 是前一层传来的页对齐容量,vaddr 是内核 CPU 访问这块存储的地址。结构自身只是记录这些信息的控制块,不是 921600 字节的像素区域。

8.2 完整分配过程

staticvoid *vb2_vmalloc_alloc(struct vb2_buffer *vb, struct device *dev,
unsignedlong size)

{
structvb2_vmalloc_buf *buf;

    buf = kzalloc(sizeof(*buf), GFP_KERNEL | vb->vb2_queue->gfp_flags);
if (!buf)
return ERR_PTR(-ENOMEM);

    buf->size = size;
    buf->vaddr = vmalloc_user(buf->size);
if (!buf->vaddr) {
        pr_debug("vmalloc of size %ld failed\n", buf->size);
        kfree(buf);
return ERR_PTR(-ENOMEM);
    }

    buf->dma_dir = vb->vb2_queue->dma_dir;
    buf->handler.refcount = &buf->refcount;
    buf->handler.put = vb2_vmalloc_put;
    buf->handler.arg = buf;

    refcount_set(&buf->refcount, 1);
return buf;
}

函数先分配 vb2_vmalloc_buf,再调用 vmalloc_user() 建立实际存储。第二次分配失败时,先释放刚才的控制块,再返回错误指针。

成功返回的仍是 buf,而不是 buf->vaddr。因此,前一层的 mem_priv 指向控制块;真正访问像素,需要通过后端的地址接口取得 vaddr。

图 5:实线按执行顺序连接,右侧错误出口只回收已经得到的资源。handler 在申请时只是配置,尚未执行 VMA 的打开回调。

8.3 vmalloc_user 的三个特征

它为内核提供连续的虚拟地址区间,底层物理页不要求全部连续。CPU 通过这段虚拟地址按顺序读写,并不意味着设备可以直接使用同一个地址发起 DMA。

它还把申请到的存储清零,并标记为允许后续映射的区域,避免把未初始化的旧内容暴露出去。名称里带有 user,不表示它已经替当前应用调用 mmap;应用虚拟地址仍要通过后续映射获得。

最后,清零只是初始化存储。这个时刻并没有拍到一帧图像,bytesused 也没有因此变成一帧有效结果。真正填帧仍发生在启动之后的处理路径。

这些通用语义可对照 Linux v6.1 的 mm/vmalloc.c 中 vmalloc_user();本篇不继续深入通用页分配器。

8.4 gfp_flags 在哪里生效

后端私有对象使用 GFP_KERNEL | q->gfp_flags 分配。但实际图像存储调用的是 vmalloc_user(buf->size),这里没有把同一个 gfp_flags 作为参数继续传入。

因此,不应笼统写成“给队列设置一个 GFP 标志,后端所有分配都按它执行”。需要分别查看控制块、实际存储和所选内存后端。

8.5 refcount 初值 1 表示谁持有存储

后端设置引用计数为 1,表示本次 alloc 建立的一份存储持有关系。它不是文件描述符个数,也不是 v4l2_fh 引用计数。

handler.refcount、handler.put 和 handler.arg 为后续 VMA 引用管理准备好入口。将来映射等路径可以取得额外引用;申请阶段尚未发生这些动作。

配对的释放函数是:

staticvoidvb2_vmalloc_put(void *buf_priv)
{
structvb2_vmalloc_buf *buf = buf_priv;

if (refcount_dec_and_test(&buf->refcount)) {
        vfree(buf->vaddr);
        kfree(buf);
    }
}

最后一份引用归零时,才先 vfree() 像素存储,再 kfree() 后端对象。当前新建池还没有映射或导出者,失败回滚通常通过一次 put 就能结束本次平面存储;已有其他持有关系的情况,需要按引用生命周期处理。

实现对照:vb2_vmalloc_alloc()、vb2_vmalloc_put()、vb2_vmalloc_memops;通用语义对照 vmalloc_user()。

9. 分到三块、第四块失败,为什么仍可能返回成功

到这里,执行位置回到了 __vb2_queue_alloc()。循环变量 buffer 恰好记录前面完整成功的数量,函数最终把它返回给 vb2_core_reqbufs()。

9.1 外层首先检查最低可工作数量

假设目标数量为 4,队列最低要求为 2:

完整成功 0 块:返回内存不足。
完整成功 1 块:低于最低要求,回收这一块并失败。
完整成功 2 或 3 块:达到最低要求,继续询问驱动是否接受。
完整成功 4 块:满足本轮目标,不需要第二次数量复核。

这次接口不是“4 个独立的申请,任何一个失败就必须全部失败”。核心允许驱动在能够继续工作的前提下,接受较少的完整缓冲区。

9.2 第二次 queue_setup 不再发起分配

相关语句如下:

if (!ret && allocated_buffers < num_buffers) {
    num_buffers = allocated_buffers;
    num_planes = 0;

    ret = call_qop(q, queue_setup, q, &num_buffers,
                  &num_planes, plane_sizes, q->alloc_devs);

if (!ret && allocated_buffers < num_buffers)
        ret = -ENOMEM;
}

原函数在这些语句之间有说明性注释,此处去掉以集中观察控制流。

核心把实际成功数作为第二次的输入,询问驱动能否接受。驱动返回错误,或者仍要求比实际更多的数量,就不能保留这一批作为成功结果。

num_planes 再次置零,是为了继续表明 REQBUFS 场景。这个调用不会重新申请像素存储,也不会因为驱动重新写了 sizes[],就把已经分好的内存自动扩成新大小。两次调用之间,格式和布局要求必须保持一致。

还有一个细节:如果已经分到 3 块,第二次回调把数量降低成 2,核心这里不会再裁掉第三块;最终回传的仍是 allocated_buffers。这个检查关注“现有数量是否足够”,不是第二轮重新制定并执行一份分配计划。

9.3 VIMC 对数量的态度

VIMC 的 queue_setup() 不修改数量,也不会因数量是 2 或 3 而拒绝。因此,在当前最低数量为 2、其他条件正常的前提下,分到 2 或 3 块都能被接受。

申请数量
本轮完整成功数
VIMC 场景下的结果
1
2
核心先按最低数量 2 申请,成功返回 2。
4
4
成功返回 4。
4
3
第二次回调接受,成功返回 3。
4
2
第二次回调接受,成功返回 2。
4
1
低于最低数量,释放并返回 -ENOMEM。
4
0
返回 -ENOMEM。

表格是对控制流的推演,不是说实际运行必然在哪一块分配失败。内存分配结果与运行环境有关。

图 6:部分成功需要先满足队列下限,再通过驱动复核。失败清理前也要暂时设置 q->num_buffers,让释放路径知道本轮对象范围。

9.4 原始分配错误不一定原样传到应用

平面分配失败时,内层可以保存 PTR_ERR(mem_priv);驱动 buf_init 也可以返回其他错误。但 __vb2_queue_alloc() 交给最外层的是成功数量,不是那个原始错误码。

因此,同一个内层失败可能最终表现为“成功返回更少数量”,也可能因为达不到下限而变成 ENOMEM。调试时要同时看失败位置和最后成功数量,而不能只用最终 errno 反推唯一原因。

实现对照:__vb2_queue_alloc() 的返回值;vb2_core_reqbufs() 的最低数量检查及第二次 queue_setup()。

10. 最后一次提交:什么时候这批对象算真正建立成功

10.1 为什么失败清理前也要写 num_buffers

完成数量复核后,核心执行:

mutex_lock(&q->mmap_lock);
q->num_buffers = allocated_buffers;

if (ret < 0) {
    __vb2_queue_free(q, allocated_buffers);
    mutex_unlock(&q->mmap_lock);
return ret;
}
mutex_unlock(&q->mmap_lock);

*count = allocated_buffers;
q->waiting_for_buffers = !q->is_output;

return0;

赋值在成功与失败分支之前,这是释放函数的工作方式决定的。__vb2_queue_free() 根据队列总数确定需要清理的尾部范围;若本轮对象已经分配,却仍把数量留为 0,它就无法按正常范围回收它们。

这里的赋值不代表最终一定成功。数量不足并被拒绝时,核心会在锁内立即回收本轮对象,数量归零,并把内存模式恢复为 UNKNOWN。

10.2 数组槽位出现指针,比最终数量提交更早

分配循环中已经逐项写入 q->bufs[index]。但映射路径按 q->num_buffers 确定可遍历范围,并使用同一把 mmap_lock。

因此,分配中途的槽位指针与最终可用池不是同一状态。驱动扩展路径应遵守框架的锁和范围规则,不能绕过它们直接访问半初始化槽位。

10.3 两类互斥锁分别保护哪一段

在本篇 VIMC 的标准 ioctl 路径中,REQBUFS 是队列类命令,分发层会选择配置好的队列操作锁。VIMC 的队列锁与节点锁实际指向同一个 vcapture->lock。

核心内部另外有 mmap_lock,用于与映射、缓冲区释放等路径协调。它只覆盖若干关键片段,不包住全部 queue_setup() 和像素分配过程。

图 7:以 VIMC 正常的非零申请为例。三段内部锁区间彼此分开;owner 更新仍在外层命令处理内,参数回写则发生在标准分发解锁之后。

直接从驱动调用 VB2 核心接口时,需要自行满足相应串行化约定;不要因为图里存在外层锁,就以为核心函数在所有入口都会自动获取它。

10.4 成功返回以后,队列里有什么

假定本轮首次申请 4 个、全部成功,且没有额外并发操作:

项目
此时的状态
q->num_buffers
4。
q->memory
MMAP。
q->bufs[0..3]
指向四个完整管理对象。
每个对象的 num_planes
VIMC 本例为 1。
平面 length / min_length
本例均为 921600 字节。
平面 mem_priv
指向对应的 vmalloc 后端对象。
后端 vaddr
指向已经建立的内核虚拟存储。
缓冲区状态
DEQUEUED
,尚未 QBUF。
owner
核心成功返回接口层后,记录当前打开上下文。
排队与采集
没有因 REQBUFS 自动启动,也没有自动建立应用映射。

waiting_for_buffers 对 CAPTURE 被设为真,表示后续仍在等待缓冲区提交;它不是“当前函数正在睡眠等一帧”的标志。

最后,外层 ioctl 还需把实际 count 等结果回写给应用。回写如果失败,应用可能得到 EFAULT,但已经成立的分配不会自动回滚。接口返回与设备状态更新仍要分开判断。

实现对照:vb2_core_reqbufs() 的数量提交;__vb2_queue_free();v4l2_ioctl_get_lock()、__video_do_ioctl()、video_usercopy()。

11. 失败处理要按资源层次看,不能只找一个 free

11.1 三层分配,三层回收

vmalloc 后端私有对象分配失败,直接返回错误指针;像素存储分配失败,后端先释放自己的私有对象。它不会替外层释放 vb2_buffer,因为那不是它创建的资源。

单个缓冲区中的某个平面失败,由 __vb2_buf_mem_alloc() 回收已经成功的平面。然后 __vb2_queue_alloc() 清空当前槽位并释放当前管理对象。

外层数量复核不接受剩余结果,才由 __vb2_queue_free() 回收已经完整成功的整批对象。这个顺序让每层只处理自己清楚的资源,再向上报告足够的信息。

11.2 可选 buf_init 自己失败时,要自己撤销半初始化资源

通用 MMAP 分配路径在平面建立以后调用驱动 buf_init。它若返回错误,核心释放当前平面与管理对象,**不会再针对这个失败对象调用一次驱动 buf_cleanup**。

因此,buf_init 内部先申请了附加资源、随后又失败时,必须自己回收这些半成品。已经完整初始化成功,后来因整批数量不足而被释放的对象,才走正常的 buf_cleanup 路径。

VIMC 本例没有设置这项回调,但分析通用核心时必须保留这个边界。否则照着文章新增驱动资源时,容易把失败回收寄托在一条不会执行的回调上。

11.3 重建失败,不会把旧池恢复回来

再次非零 REQBUFS 会先清理旧池。旧池已经解除,而新布局或分配随后失败,核心不会凭空恢复原来的缓冲区对象。

而且接口层只在核心成功时更新 owner,所以失败以后,也不能认为 owner、内存模式和数量全部被重置成“刚打开节点”的同一状态。应查看各字段的实际赋值位置。

这份实现支持把仍有映射或导出持有的存储与队列解除关联。旧存储可能继续存活,但它不再属于当前池,旧映射也不会自动变成新 buffer 0 的映射。完整映射生命周期在下一篇继续讨论。

11.4 REQBUFS 与 CREATE_BUFS 的边界

本篇的 REQBUFS 先处理旧池,再建立新池;CREATE_BUFS 可以请求增加缓冲区,并携带用于确定布局的格式信息。二者会复用某些内部函数,但不是同一个入口换了名字。

这也解释了 __vb2_queue_alloc() 为什么使用“现有数量 + 本轮索引”,以及为什么它再次检查剩余槽位容量。文章当前分析的是 REQBUFS 的这一次调用,不应把这里的“旧池已清理”前提套到所有调用者上。

实现对照:vb2_core_reqbufs()、vb2_core_create_bufs()、__vb2_queue_alloc()、__vb2_queue_free()、fill_buf_caps()。

12. 做一个只申请、不采集的观察实验

本篇的验证不需要提前实现 QBUF、STREAMON 或 mmap。使用已有格式,在空闲测试节点上请求数量,输出实际返回值,再释放缓冲区池,就能观察申请这一阶段。

配套 examples/reqbufs_probe.c 同时支持单平面和多平面采集类型,执行范围限定为 QUERYCAP → G_FMT → REQBUFS → REQBUFS(0) → close。它不设置新格式,不映射,不提交缓冲区,也不读取像素。

cc -std=c11 -Wall -Wextra -Werror -O2 \
   examples/reqbufs_probe.c -o reqbufs_probe

./reqbufs_probe /dev/video0 1
./reqbufs_probe /dev/video0 4

以上两次应分别在程序完成清理后运行。对应当前 VIMC 配置,请求 1 个而成功返回 2 个,是最低数量生效的表现,不是调用参数被错误修改。请求 4 个时,是否得到 4 个,仍以实际结果为准。

需要观察内部路径时,可以在测试内核中记录 queue_setup 的进入次数、输入数量、输出平面与大小、__vb2_queue_alloc() 的返回数,以及最终提交数量。分配减少的场景需要受控故障注入或模拟,不能通过随意耗尽系统内存来制造。

本程序会改变目标队列的缓冲区安排,不能在正在使用的采集链路上当作无副作用查询。没有实际设备测试时,编译成功只能证明程序通过编译,不能证明目标驱动运行正确。

三个问题检验是否读通

请求中没有宽高,921600 字节从哪里来? 来自驱动已经保存的当前格式,经 vimc_capture_queue_setup() 写入 sizes[0],再传给核心和内存后端。

为什么第四块失败,前三块不立即全部释放? 内层先回收第四块的半成品,把前三块作为完整成功数量交回外层;外层根据下限和驱动复核决定是否保留。

返回的 count 是 3,为什么不能继续按 4 次 QUERYBUF 循环? 因为池里实际只建立了三个可用索引,应用最初的期望不是成功数量。后面的查询和映射必须围绕返回结果组织。

13. 从请求到存储,把整件事连起来

现在回看这六个函数,就能看到一条有明确分工的过程。

vb2_ioctl_reqbufs() 处理请求是否合法、当前上下文是否可以申请,并在成功后记录 owner。vb2_core_reqbufs() 整理旧状态、确定候选数量,询问布局,组织分配和数量复核。vimc_capture_queue_setup() 从当前格式给出一个平面和所需容量。

接下来,__vb2_queue_alloc() 创建每个缓冲区的管理对象;__vb2_buf_mem_alloc() 逐平面计算页对齐容量并进入后端;vb2_vmalloc_alloc() 最终建立后端对象和真正的图像存储。结果逐层返回,核心只将满足要求的完整对象提交为新池。

函数
这一步得到什么
返回的是什么
vb2_ioctl_reqbufs()
请求校验、归属检查、成功后的 owner 更新。
状态码;请求对象中带实际数量。
vb2_core_reqbufs()
可用的新池,或按失败阶段清理后的状态。
状态码;通过 count 回传数量。
vimc_capture_queue_setup()
数量与布局要求,本例主要填写平面数和容量。
状态码;输出参数承载布局。
__vb2_queue_alloc()
一批完整管理对象及适用的存储。
完整成功的缓冲区数。
__vb2_buf_mem_alloc()
当前缓冲区的各平面存储关联。
0 或负错误码。
vb2_vmalloc_alloc()
当前平面的后端对象与像素存储。
后端对象指针或错误指针。

申请完成后,内核知道这些缓冲区在哪里、各有多大;应用目前只知道实际建立了几个。下一步需要通过 QUERYBUF 查询每个对象,再用 mmap 建立访问已有存储的映射。这是下一篇的起点。

实现与接口索引

本文直接展开 drivers/media/common/videobuf2/ 中的 videobuf2-v4l2.c、videobuf2-core.c、videobuf2-vmalloc.c,并结合 drivers/media/test-drivers/vimc/vimc-capture.c 中的布局与队列配置。命令入口和锁边界参照 v4l2-ioctl.c。

相关学习资料