0. 背景
VFIO 设备直通让虚拟机直接访问物理 PCI 设备,绕开 QEMU 的软件模拟以获得接近原生的 I/O 性能。但"直接访问"四个字的背后,是 QEMU、KVM、Host 内核三套系统协同维护的复杂映射关系——Guest 看到的只是一个 PCI BAR 地址,这个地址要经过两层页表翻译才能到达物理设备。
ARM64 64KB PAGE_SIZE 环境下,这个问题更加棘手:页大小与 BAR 空间大小恰好同级(64KB),一旦多个 VF 的 BAR 被分配到相邻 GPA 区间,就容易出现空间重叠,导致 FlatView 渲染时 BAR 被截断、memslot 注册失败,最终 Guest 访问设备时触发 kernel panic。
本文是这个分析系列的第一篇,目标是把正常场景下的数据结构链路梳理清楚——四种地址空间的定义、三套页表的协作关系、QEMU 与 KVM 各自维护的关键结构体及其关联方式。只有先理清"正常情况下每一步发生了什么",才能在后续文章中分析"BAR 重叠时在哪一步出了什么问题"。
代码引用来自 openEuler QEMU(openeuler_qemu)和
openEuler
Kernel(openeuler/kernel),已逐一对照源码验证。
1. 四种地址空间
VFIO PCI 直通场景涉及四种地址,分属 Guest、QEMU、Host 内核三个主体管理。理解它们的区别是后续所有分析的前提。
| 地址 | 全称 | 谁分配 | 存在哪里 | 示例 |
|---|---|---|---|---|
| GVA | Guest Virtual Address | Guest 内核 (ioremap) | Guest S1 页表 PTE | 0xffff_8000_ca00_0300 |
| GPA | Guest Physical Address | QEMU 命令行(窗口基址) + Guest PCI 枚举(BAR 偏移) | BAR 配置寄存器、S2 PTE | 0x8000_2000_0300 |
| HVA | Host Virtual Address | Host 内核 (mmap 返回值) | QEMU ram_block->host |
0x7fff_a000_0300 |
| HPA | Host Physical Address | Host BIOS/PCI 枚举 | S2 PTE 的目标字段 | 0x0800_0000_0300 |
以 Guest 驱动访问设备 BAR2 内偏移 0x300
的寄存器为例,一条简单的 ldr 指令需要经过两次页表翻译:
1 | Guest 指令: ldr x0, [x3] (x3 = io_base + 0x300) |
四种地址的高位含义各自独立,互相之间没有数值上的映射关系——GVA 的高位来自 Guest 内核的 vmalloc 区,GPA 来自 QEMU 分配的 Guest 物理地址空间布局,HVA 来自 Host 进程的 mmap 区域,HPA 来自 Host 的物理内存布局。唯一不变的是低 16 位(64KB 页内偏移),在四级翻译中保持一致。
2. 三套页表的协作
四种地址需要三套页表来完成翻译。VHE 模式下 Host 内核运行在 EL2,Guest 运行在 EL1,运行时 Guest 访问设备需要 Guest S1 + Host S2 两套页表。但 KVM 建立 S2 时,还需要第三套——Host S1 用户页表——作为"脚手架"。
为什么要这个脚手架? KVM 不直接知道 HPA。QEMU 通过
ioctl(KVM_SET_USER_MEMORY_REGION) 把
{GPA, HVA, size} 三元组传给 KVM 后,KVM 拿到的是
HVA,必须调用 get_user_pages() 走 Host S1
页表(TTBR0_EL2)查到设备 BAR 的 HPA,才能把 GPA → HPA 填入
S2 PTE。如果 Host S1 尚未建立(mmap 后首次访问),会触发 page fault,由
VFIO 驱动的 fault 回调 io_remap_pfn_range 建立 Host S1
PTE。
| 寄存器 | 翻译关系 | 页表由谁管理 | 角色 |
|---|---|---|---|
| TTBR1_EL1 | GVA → GPA | Guest OS | Guest 自己的翻译 |
| TTBR0_EL2 | HVA → HPA | Host 内核 | S2 的"脚手架" |
| VTTBR_EL2 | GPA → HPA | KVM (Host 内核) | Guest 运行时的硬件路径 |
三套页表的建立有严格的先后顺序:
Phase 1 — QEMU mmap 获取 HVA(只建 VMA,Host S1
延迟建立) Phase 2 — KVM 建 memslot 时通过
get_user_pages 触发 Host S1 建立,再填 S2 PTE Phase
3 — Guest 内部执行 ioremap 建立 Guest S1
运行时只有 Guest S1 + Host S2 参与翻译,Host S1 不参与。详细流程见附录 A。
3. QEMU 侧:MemoryRegion 树
从本节开始,我们自顶向下梳理 QEMU 进程中维护的数据结构。QEMU 使用一棵 MemoryRegion 树(以下简称 MR 树)来描述 Guest 的整个物理地址空间。理解这棵树的结构是理解 BAR 空间分配的关键。
3.1 MR 的三种类型
每个 MemoryRegion 描述 GPA
空间中的一段区域,核心字段如下(include/exec/memory.h):
1 | struct MemoryRegion { |
MR树是一个三层结构,叶子节点才存着实际的region内容。待会下面会讲三层结构MemoryRegions树三层结构。
这里有一个容易混淆的地方:subregions 和
ram_block 是两个独立的维度,分别对应"拓扑"和"存储":
subregions→ 树结构:QTAILQ 链表,存子 MR。负责"这个 GPA 区间包含哪些子区域"ram_block→ 数据后端:ram_block->host是 HVA 指针。负责"实际数据在宿主机的哪个地址"
| subregions | ram_block | 典型例子 | 说明 |
|---|---|---|---|
| 有 | 无 | bar->mr, region.mem |
纯容器/IO 层,通过子 MR 定义内容 |
| 无 | 有 | mmaps[0].mem, mach-virt.ram |
叶子节点,ram_block->host =
HVA |
| 有 | 有 | 被 overlay 切割的 RAM | 罕见 |
| 无 | 无 | GIC, UART 等纯 IO MR | 纯 IO MR 的叶子节点只通过 ops 回调处理
MMIO |
只有叶子 ram_device MR 才能触发 KVM memslot
注册,也只有叶子节点才不会再挂有subregions——中间的容器
MR、alias MR、IO MR 没有 ram_block,在 memslot
创建阶段会被直接跳过。
按 ram、ram_device、ops
三个标志的组合,MR 叶子节点(L3) 分为三种核心类型,它们从根本上决定了
Guest 访问该区域时的处理路径:
| 类型 | 标志 | 有无 HVA | KVM 建 memslot? | Guest 访问路径 |
|---|---|---|---|---|
| RAM | ram=true, ram_device=false |
有 (QEMU malloc) | 是 | S2 命中 → 硬件直达 |
| RAM device | ram=true, ram_device=true |
有 (mmap 返回) | 是 | S2 命中 → 硬件直达 |
| IO | ops != NULL, ram=false |
没有 | 否 | S2 miss → VM exit → QEMU ops 回调 |
3.2 MR 树完整拓扑
下面展示从根到叶子的完整 MR 树。注意这棵树的每一层对应一个概念层级,我们分层阅读:
第一层:根容器
system_memory 是 QEMU
全局变量(system/memory.c),代表 Guest
的整个物理地址空间。它本身只是一个空容器——不存数据,不映射到任何实际内存,只通过子
MR 定义内容。
1 | system_memory (根容器, 覆盖整个 GPA 空间, ram_block=NULL) |
第二层:平台级子区域
根容器下挂载了 Guest 的所有平台设备地址空间——RAM、GIC、UART、以及 PCI 总线的 MMIO 窗口:
1 | system_memory |
其中 pcie-mmio 是一个 alias MR,把 GPEX
io_mmio(PCI host bridge 的 MMIO 地址空间)映射到
system_memory 的高地址区间。
第三层:PCI 设备 BAR
这一层的结构取决于是否配置了 PCIe Root Port,两者有本质区别:
无 Root Port 时——所有 VF 直接挂在 bus 0 上,BAR MR 都是 GPEX
io_mmio 的同级子区域。Guest 为各 BAR 分配的 GPA
如果间隔不够,MR 在 GPA 空间中就会重叠:
1 | pcie-mmio → GPEX io_mmio |
有 Root Port 时——每个 Root Port 是一个 PCI
Bridge(pci_bridge.c:382),拥有独立的
sec_bus.address_space_mem。Bridge 通过
pci_bridge_init_alias(pci_bridge.c:161)在父总线地址空间中创建一个
alias MR(bridge window),映射到自己的
sec_bus.address_space_mem。Guest 为每个 Root Port
分配独立的 bridge window,window 之间不重叠,因此不同 Root Port 下的 VF
BAR 不会在 GPA 空间中冲突:
1 | pcie-mmio → GPEX io_mmio |
这一拓扑差异是后续多 VF 场景中 BAR 重叠问题的前置条件:无 Root Port 时,多个 VF 的 BAR 在同一个 GPA 窗口中竞争空间;有 Root Port 时,每个 VF 被隔离在独立的 bridge window 中。
MR 树拓扑对比(PlantUML):
4. QEMU 侧:VFIO BAR 的三层 MR 嵌套
理解 VFIO BAR 数据结构的关键是区分两样东西:
- C 结构体(
VFIOBAR/VFIORegion/VFIOMmap)——它们是数据的容器,定义谁拥有谁 - MemoryRegion(MR 树节点)——它们定义 GPA
空间的拓扑,通过
subregions链表形成父子关系
容易混淆的根源是命名:mr、mem、mmap、ram_block
看起来相似但分属不同层级。
4.1 subregions 链:一个 BAR 的三个 MR 如何串联
下面以 VF 的 BAR2(64KB,非 sparse)为例,展示三个 MR 如何通过
subregions 和指针形成父子链。
图 A:指针/值 与 subregions 嵌套
1 | VFIOPCIDevice (pci.h:137) |
关于 mmaps 数量:绝大多数 PCI BAR 是连续的可映射区域,此时
nr_mmaps = 1,mmaps[0]覆盖整个 BAR(无论 BAR 多大——64KB、128KB、2MB 都只有一个 mmaps 条目)。多个 mmaps 仅出现在 sparse BAR 场景——某些设备的 BAR 内部存在不连续的可映射区域(如 GPU 的 VRAM 被寄存器窗口打断),此时每个 sparse area 各自对应一个 mmaps 条目。本文后续讨论均以nr_mmaps = 1为前提。
关键点:
| 连接 | 方式 | 代码位置 |
|---|---|---|
① bar->mr → L1 MR |
指针 (g_new0 堆分配) |
pci.c:1806 |
| ② L1.subregions 挂 L2 | memory_region_add_subregion(bar->mr, 0, bar->region.mem) |
pci.c:1812 |
③ bar->region.mem → L2 MR |
指针 (堆分配) | helpers.c:164 |
| ④ L2.subregions 挂 L3 | memory_region_add_subregion(region->mem, offset, &mmaps[i].mem) |
helpers.c:474 |
⑤ mmaps[i].mem → L3 MR |
值包含 (嵌入 VFIOMmap 内部) | helpers.c:469 |
| ⑥ L3.ram_block→HVA | mmaps[i].mmap 赋给 ram_block->host |
helpers.c:469-472 |
mmaps[1] 存在吗? 是的——当 BAR 是 sparse
类型时(nr_mmaps > 1),每个 sparse area 独立
mmap,各自生成一个叶子 MR 挂在 region.mem 的
subregions 下。非 sparse 时 nr_mmaps = 1。
4.2 mmap 成功 vs 失败 —— BAR 结构的两种形态
注意:
bar->mr虽然调用memory_region_init_io()但传入ops=NULL,所以 L1 是没有 IO 回调的纯容器。真正有 IO 回调的是 L2bar->region.mem(ops=vfio_region_ops)。
4.3 BAR 如何挂入 PCI 总线 MR 树
三个 MR 的内部嵌套(L1→L2→L3)建立后,L1 bar->mr
还需要挂入 PCI 总线的 MMIO 地址空间。这一步由
pci_update_mappings()(hw/pci/pci.c:1559)在
Guest 写入 BAR 寄存器时触发:
1 | // r = &d->io_regions[i] — PCIIORegion |
PCIIORegion 是 BAR 的"台账"——记录 BAR 的大小和 GPA
地址。它在
pci_register_bar()(pci.c:1323)中初始化:
1 | r->size = memory_region_size(bar->mr); // = 64KB, 之后不再改变 |
Guest PCI 枚举时写 BAR=全 1,QEMU 返回
val & wmask,Guest 由此推断 BAR
大小。r->size 决定了 Guest 为 BAR 保留的 GPA
窗口大小,也决定了多个 BAR 的 GPA 间隔——这是后续 BAR
重叠问题的根源。
5. QEMU 侧:FlatView 展平与 memslot 注册
5.1 FlatView 与 FlatRange
MR 树是嵌套结构,但 KVM 需要按 GPA 线性查找。QEMU 的解决方案是:把 MR
树展开成 FlatView——一组按 GPA 排序、互不重叠的
FlatRange 数组。
FlatView 是容器,FlatRange
是数组元素。两者是一对多关系:
1 | // memory.h:1278 — FlatView = FlatRange[] + 元数据 |
generate_memory_topology()(memory.c:756)调用
render_memory_region() 递归遍历 MR 树的
subregions 链表,生成 FlatRange[] 数组,再调用
flatview_simplify() 合并相邻同类区间。
5.2 FlatView 渲染规则与 BAR 重叠截断
FlatView 渲染时,子 MR 按 QTAILQ
链表顺序遍历(memory.c:646),高 priority 的先渲染占据
GPA,低 priority 的只填空隙(memory.c:666
注释:Render the region itself into any gaps left by the current view)。
QTAILQ 排序规则(memory.c:2699-2701):priority 降序,同
priority 时后插入的排前面(运算符是 >=,配合
INSERT_BEFORE)。
后续多 VF 场景中 BAR 重叠截断就发生在 FlatView
渲染这一步——当两个 BAR 的 GPA 区间重叠时,先渲染的 BAR
占据空间,后渲染的 BAR 被截断,导致 FlatRange 中的
addr.size < mr->size。
5.3 从 FlatView 到 KVM memslot 的完整调用链
FlatView 本身不直接参与 KVM memslot 创建——它通过 MemoryRegionSection 作为中介。
当 MR 树发生变更时,QEMU 重新生成 FlatView,然后 diff 新旧两个 FlatView,将差异转化为 MemoryRegionSection,派发给所有 MemoryListener(KVM 是其中一个)。调用链如下:
1 | MR 树变更 |
关键结论:
- FlatView 和 KVMSlot 没有直接引用关系。FlatView
决定"哪些 GPA 区间该映射",KVMSlot 负责"把数据打包发给内核"。中间的
MemoryRegionSection是桥梁——它同时持有FlatView *fv(上下文)和组装 KVMSlot 所需的全部字段(mr,size,offset_within_address_space) - kvm_region_add() 只排队、不执行——它把 section 塞进
transaction_add链表就返回了。真正的 memslot 创建发生在kvm_region_commit(),按"先删除后添加"的顺序批量提交
5.4 两个关键过滤点
链路中有两个位置可能导致 ram_device MR 被跳过、memslot 不被创建:
!memory_region_is_ram(mr)(kvm-all.c:1349):IO 类型 MR(ram=false)直接跳过——这解释了为什么 mmap 失败时bar->region.mem(L2)不会触发 memslot 注册kvm_align_section → 0(kvm-all.c:266):section size 小于 PAGE_SIZE 时返回 0——如果 BAR 被 FlatView 截断后剩余区间不足一个页,memslot 就不会被创建
QEMU 内部用 KVMSlot 结构缓存已注册的 memslot 信息:
1 | typedef struct KVMSlot { |
KVMSlot 不存 FlatView 指针,也不存 FlatRange
引用——它是"快照",记录的是 ioctl 发送到内核的那一刻的
GPA/HVA/size 三元组。
6. KVM 侧:memslot 与 S2 页表
Host 内核(EL2)中,kvm_memory_slot 是内核侧的
memslot,存
base_gfn(GPA>>PAGE_SHIFT)、npages(页数)、userspace_addr(HVA)。
memslot 不存 HPA——KVM 需要 HPA 时通过
get_user_pages(userspace_addr) 动态查 Host S1
页表获取。每个 memslot 对应 S2 页表中的一段 GPA→HPA 映射,KVM 在创建
memslot 的同时填入 S2 PTE。
7. 完整数据链路
从 QEMU MR 树到 KVM S2 页表的完整关联链路。先把全部数据结构的关系画在一张 PlantUML 类图上,再看数据如何从 L3 一步步流向 KVM:
全量数据结构关系图(PlantUML 类图):
关键区分:
| 符号 | PlantUML 含义 | 代码层面 |
|---|---|---|
◆ (实心菱形) |
组合(composition),值嵌入 | mmaps[0].mem 直接嵌入
VFIOMmap,VFIOBAR 值数组嵌入
VFIOPCIDevice |
→ (箭头) |
关联(association),指针 | bar->mr 是堆上的 MemoryRegion* |
⇢ (虚线) |
依赖(dependency),数据流动 | RAMBlock.host 的值来自 mmap
返回值;KVMSlot 由 MemoryRegionSection
组装 |
核心要点:
- HVA 的唯一来源:
mmaps[i].mmap→ 经memory_region_init_ram_device_ptr()存入L3.ram_block->host - GPA 的来源:
PCIIORegion.addr→pci_update_mappings()挂入 GPEX io_mmio → 展平后在FlatRange.addr.start - 只有 L3 进
FlatView:
render_memory_region先渲染 subregions,L3 占满区域后 L1/L2 无空隙可填 - 只有 L3 进 memslot:
kvm_set_phys_mem检查memory_region_is_ram(),L2 的ram=false直接跳过 - BAR 重叠的破坏链:两个 BAR 的 GPA 太近 →
render_memory_region()截断后渲染者 →FlatRange.addr.size < MR.size→kvm_align_section返回 0 → memslot 不创建
下面用 ASCII 流程图展示数据如何从 L3 一步步流向 KVM:
1 | QEMU EL0 KVM EL2 |
这条链路中,FlatView
渲染是第一个可能出问题的环节:如果 BAR 重叠导致 FlatRange
被截断,后续的 section size 就会小于预期,进而被
kvm_align_section 过滤掉,最终 memslot 不完整或缺失。
数据链路活动图:
8. 实例:正常场景下的取值
以 vfio-pci 驱动绑定一个 BAR2 物理大小为 64KB
的设备为例,64KB PAGE_SIZE 环境下,QEMU 启动后各结构体的 size
字段值全部一致:
| 结构体 | 字段 | 类型 | 值 | 来源 |
|---|---|---|---|---|
VFIORegion |
.size |
size_t |
64KB (0x10000) | ioctl 返回的 info.size |
VFIOBAR |
.size |
size_t |
64KB | bar->size = bar->region.size (pci.c:1785) |
VFIOMmap |
.size |
size_t |
64KB | mmaps[0].size = region->size (helpers.c:388) |
MemoryRegion (bar->mr) |
.size |
Int128 |
64KB | memory_region_init_io(..., bar->size)
(pci.c:1808) |
MemoryRegion (region->mem) |
.size |
Int128 |
64KB | region setup 时设定 |
MemoryRegion (mmaps[0].mem) |
.size |
Int128 |
64KB | memory_region_init_ram_device_ptr(..., mmaps[0].size)
(helpers.c:469) |
RAMBlock |
.size |
ram_addr_t |
64KB | qemu_ram_alloc_from_ptr(size=64KB, ...)
(memory.c:1719) |
PCIIORegion |
.size |
pcibus_t |
64KB | r->size = memory_region_size(bar->mr)
(pci.c:1323) |
| wmask | — | uint64_t |
0xFFFFFFFFFFFF0000 | ~(64KB - 1) (pci.c:1330) |
正常场景下,所有 size 字段均为 64KB,mmap 成功,ram_device MR 创建,FlatView 渲染完整,KVM memslot 建立,S2 页表就绪。Guest 访问 BAR2 时 S2 命中,硬件直达设备,无 VM exit。
后续文章将分析:当多个 VF 的 BAR 分配到相邻 GPA、且间隔不足以容纳完整 BAR 空间时,上述链路中的哪一步会出现异常、如何一步一步导致 Guest kernel panic。
附录 A:三套页表建立流程与运行时关系
时序图:
附录 B:ops 回调与 MMIO 慢路径派发
ops(MemoryRegionOps *)定义了该 MR 的 MMIO
读写回调。它与 ram 互斥——ram=true
走硬件直达,ops != NULL 走 QEMU 软件模拟:
1 | struct MemoryRegionOps { |
两类 MR 的根本区别:
ops = NULL, ram = true |
ops != NULL, ram = false |
|
|---|---|---|
| 例子 | mach-virt.ram, mmaps[0].mem |
region.mem, GIC, UART |
| Guest 访问路径 | S2 命中 → 硬件直达设备 | S2 miss → VM exit → QEMU ops 回调 |
| KVM 建 memslot? | 是 | 否 |
| 速度 | 快(无 VM exit) | 慢(每次都要退出到 QEMU) |
| 访问粒度限制 | 无(任意大小) | 受 ops->valid.max_access_size 限制(最大 8
字节) |
VFIO BAR 场景下快路径与慢路径的分界取决于 mmap 是否成功:
1 | mmap 成功时 (ram_device MR 存在): |
慢路径的完整派发流程:CPU 触发 Data Abort,硬件填写 ESR 寄存器给
KVM,KVM 通过 kvm_run 结构体把 MMIO 请求返回给 QEMU:
1 | Guest ldr → S2 miss → VM exit |
VFIO region ops(hw/vfio/helpers.c:260)的回调是
.read = vfio_region_read /
.write = vfio_region_write,内部调
pread(vfio_fd, ..., fd_offset+addr) /
pwrite(...)
完成设备访问。max_access_size = 8,慢路径只支持 1~8
字节单次访问。
慢路径的隐患:当 BAR2 mmap 失败后,只剩下
region.mem(IO 类型,ops->valid.max_access_size = 8)。Guest 执行ldp x0, x1, [x3]时是 128-bit(16 字节)原子操作,超出 VFIO region ops 的最大粒度,KVM 的 ISV=0 也无法解码指令 → 只能向 Guest 注入 SEA → kernel panic。