前两篇我们回答了“为什么要有 blob”和“它在整栈的哪个位置”。从这一篇起,我们正式进入规范层。
guest 想要一块 blob 时,它到底告诉了 host 哪些信息?答案浓缩成三个参数:
blob_mem、blob_flags、blob_id。这三者相互正交,共同构成整个 blob机制的“语义骨架”。理解了它们,你就理解了后端为什么会产出四种截然不同的存储形态。
1、先看命令长什么样
guest 内核发出的命令是 VIRTIO_GPU_CMD_RESOURCE_CREATE_BLOB,它携带的关键字段可以简化为:
struct virtio_gpu_resource_create_blob {
uint32_t resource_id; /* 资源身份证:res_id */
uint32_t blob_mem; /* 维度一:存储在哪 */
uint32_t blob_flags; /* 维度二:可见性 / 共享语义 */
uint64_t blob_id; /* 维度三:引用哪块已有 host 内存 */
uint64_t size; /* 存储大小 */
/* 后随 guest 页的 iovec 数组(可选,详见第 6 篇) */
};
在 host 侧,virglrenderer 把它接成 virgl_renderer_resource_create_blob_args,然后进入 virglrenderer.c 的校验与分发。本篇文章,我们就围绕中间那三个字段展开。
2、维度一:blob_mem —— 存储在哪
blob_mem 回答最根本的问题:这块内存的“真身”存在哪一侧? 它有三个取值。
| 取值 | 含义 | 谁提供存储 |
|---|---|---|
VIRTIO_GPU_BLOB_MEM_GUEST | 存储就是 guest 的物理页 | guest(iovec 描述,第 6 篇详解) |
VIRTIO_GPU_BLOB_MEM_HOST3D | 存储由 host 分配 | host 后端(GPU 显存 / Vulkan memory) |
VIRTIO_GPU_BLOB_MEM_HOST3D_GUEST | 两者都有 | host 分配 + guest 镜像 |
在 virglrenderer 里,这个三选一直接被翻译成两个布尔量(见 virglrenderer.c):
switch (args->blob_mem) {
case VIRGL_RENDERER_BLOB_MEM_GUEST:
has_host_storage = false; has_guest_storage = true; break;
case VIRGL_RENDERER_BLOB_MEM_HOST3D:
has_host_storage = true; has_guest_storage = false; break;
case VIRGL_RENDERER_BLOB_MEM_HOST3D_GUEST:
has_host_storage = true; has_guest_storage = true; break;
}
这两个布尔量决定了后续的走向:
- 只有 guest 存储(
GUEST):核心根本不需要问后端,直接用 guest 的 iovec 建资源(virgl_resource_create_from_iov),走的是最短路径。 - 有 host 存储(
HOST3D/HOST3D_GUEST):核心必须调ctx->get_blob(...)让后端去分配真实的 host 存储——这才是 blob 机制的“重头戏”。
一句话记忆:
blob_mem决定“要不要惊动后端”。
3、维度二:blob_flags —— 可见性与共享语义
blob_mem 决定存储在哪,blob_flags 就决定这块存储能被谁、以什么方式看见。它是一组可叠加的位标志:
| flag | 诉求 | 对后端的硬约束 |
|---|---|---|
USE_MAPPABLE | host 存储要能映射进 guest 地址空间 | 必须能 mmap(memfd / dmabuf / shm) |
USE_SHAREABLE | 可跨 context 共享 | 不能用会失效的 opaque handle |
USE_CROSS_DEVICE | 要能被别的设备使用 | 必须导出成 dma-buf |
这三个 flag 每一个都不是“建议”,而是倒逼后端选择存储介质的强约束。举两个例子体会一下:
例一:CROSS_DEVICE 强制 dma-buf。
在 venus 后端(vkr_device_memory.c)里能清楚看到这条约束落地:
if (blob_flags & VIRGL_RENDERER_BLOB_FLAG_USE_CROSS_DEVICE) {
if (!can_export_dma_buf) {
vkr_log("mem cannot export to dma_buf for cross device blob sharing");
return false;
}
fd_type = VIRGL_RESOURCE_FD_DMABUF; /* 别无选择,只能 dmabuf */
}
因为只有 dma-buf 这种内核对象才能被 display、编码器、别的进程认识。opaque fd 出了原设备就失去意义。
例二:SHAREABLE 禁用 opaque handle。
opaque handle(比如 GEM handle)是某个 context 私有的整数,一旦离开原 context/进程就变成悬空的野值。所以规范规定:一旦要 SHAREABLE,后端就不能用 opaque handle,必须退到真正的内核对象 fd。这条约束在 virgl_resource.h 的注释里写得很直白。
一句话记忆:
blob_flags决定“存储介质必须是什么形态”。
4、维度三:blob_id —— 引用一块已有的 host 内存
前两个维度描述“新建一块存储”,而 blob_id 解决另一类问题:guest 想引用 host 侧此前已经建立、正等待被“具象化”成资源的一块内存。
最典型的场景是 venus(Vulkan):
- guest 应用先
vkAllocateMemory,venus 在 host 侧真正分配了一块VkDeviceMemory,并给它编号; - 之后 guest 才决定“把这块 memory 变成一个 virtio-gpu 资源”,于是发
RESOURCE_CREATE_BLOB,用blob_id指向第 1 步那块 memory; - 后端凭
blob_id找回那块 memory,导出 fd,填进 blob。
所以 blob_id 本质是host 对象的一次性提货凭证。规范特别强调它的“一次性”——get_blob 的注释(virgl_context.h)写道:
get_blob is a one-time thing. The context object might be destroyed or reject subsequent get_blob calls.
也就是说,同一个 blob_id 提货一次后就作废,不能重复兑现,避免两个资源指向同一块存储造成所有权混乱。
一句话记忆:
blob_id决定“兑现哪一张此前开出的提货凭证”。
5、三维合起来:一张决策矩阵
三个维度正交,但真正决定“后端产出什么”的,是它们的组合。我们把常见组合摊平成一张矩阵:
把这张矩阵和第 1 篇文末那张“四种存储形态”的剧透图对照,你会发现它们其实是同一件事的两个视角:
| 规范输入组合 | 后端典型产出 | 承载它的 union 成员 |
|---|---|---|
| HOST3D + CROSS_DEVICE | dma-buf fd | u.fd |
| HOST3D + MAPPABLE(opaque) | opaque fd + Vulkan UUID | u.fd + vulkan_info |
| HOST3D,同 context | GEM opaque handle | u.opaque_handle |
| HOST3D + SVM 扩展 | 虚拟地址 | u.va_handle |
| GUEST | (不经后端,用 iovec) | —— |
这张对照表,就是第 11 篇讲 virgl_context_blob 时的“入口”——结构体里那些看似杂乱的 union 分支,全都是这张矩阵的产物。
6、本篇小结与下一站
这一篇我们拆开了 blob 的“语义骨架”:
blob_mem决定存储在哪、要不要惊动后端(GUEST / HOST3D / HOST3D_GUEST);blob_flags决定存储介质必须是什么形态(MAPPABLE / SHAREABLE / CROSS_DEVICE 都是强约束);blob_id是一张一次性提货凭证,用来引用 host 侧已有的内存对象;- 三者的组合,直接决定了后端产出四种存储形态中的哪一种。
下一篇(第 4 篇):我们顺着 MAPPABLE 这条线往下走——host 分配的内存究竟怎么“出现”在 guest 里,让 guest 用户态访问得到?这就要引出 host-visible 内存区,以及 RESOURCE_MAP_BLOB / UNMAP_BLOB 这对命令,还有那个一路要传到 hypervisor 页表的 map_info。 这是跨域(host/guest)共享的机制之一,全专栏的底层核心,不要错过。

482

被折叠的 条评论
为什么被折叠?



