0. 本文定位
VFIO 设备直通让虚拟机直接访问物理 PCI 设备,绕过 QEMU 的软件模拟以获得接近原生的 I/O 性能。但"直接访问"四个字的背后,是 QEMU、KVM、Host 内核三套系统协同维护的复杂映射关系——Guest 看到的只是一个 PCI BAR 地址,这个地址要经过两层页表翻译才能到达物理设备。
本文是这个分析系列的第一篇:基础参考文章。定位是只讲正常链路——把 QEMU 和 KVM 各自维护的数据结构、它们之间的关联方式、以及从 BAR mmap 到 S2 页表建立的完整流程梳理清楚。内容包括:
- 四种地址空间(GVA/GPA/HVA/HPA)的定义与区别
- 三套页表(Guest S1、Host S1、Host S2)的协作关系与时序
- QEMU 的 MemoryRegion 树与 VFIO BAR 的三层 MR 嵌套结构
- FlatView 展平机制与 KVM memslot 注册流程
- 各数据结构的 size 字段在正常场景下的取值
本文不涉及 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. 三套页表
VFIO 直通场景涉及三套页表,VHE 模式下 Host 运行在 EL2,Guest 运行在 EL1。
| 寄存器 | 翻译 | 管理者 | 角色 |
|---|---|---|---|
| TTBR1_EL1 | GVA → GPA | Guest OS | Guest 自己的页表 |
| TTBR0_EL2 | HVA → HPA | Host 内核 | KVM 解析 HPA 时的"脚手架" |
| VTTBR_EL2 | GPA → HPA | KVM | 硬件 Stage-2 翻译 |
2.1 关键认知:ARM64 S2 是 Demand-paged
ioctl(KVM_SET_USER_MEMORY_REGION) 创建 memslot 时,KVM
仅校验 HVA/VMA 合法性,不填 S2 PTE。真正的 S2 PTE 在
Guest 首次访问 GPA 时,由 Stage-2 fault handler 按需建立。
2.2 Runtime 翻译路径
Guest 访问设备时硬件走两阶段翻译,Host S1 不参与运行时翻译:
- Stage 1 (TTBR1_EL1):GVA → GPA,由 Guest OS 的 ioremap 建立
- Stage 2 (VTTBR_EL2):GPA → HPA,由 KVM 管理(demand-paged)
Host S1 (TTBR0_EL2) 唯一被用到的时刻:KVM 处理 Stage-2 fault 需要解析
HPA 时,通过 get_user_pages_unlocked(HVA) 沿 Host S1
查到物理页帧号。(即,只在建立S2页表的时候有用)
2.3 页表建立时序
三套页表的建立有严格的先后顺序:
- Phase 1:QEMU
mmap(VFIO fd)→ Host 建立 VMA,Host S1 PTE 仍为空(lazy) - Phase 2:QEMU
ioctl(KVM_SET_USER_MEMORY_REGION)→ KVM 利用Host VMA 创建 memslot,仅校验,不填 S2 页表,S2 页表等fault的时候按需建立。 - Phase 3:Guest 内
ioremap(GPA)→ 填 Guest S1 PTE - Runtime:Guest 首次访问 GPA → Stage-2 fault:
user_mem_abort()查 memslot 得 HVAget_user_pages_unlocked(HVA)→ Host S1 page fault →vfio_pci_mmap_fault()填 Host S1 PTEkvm_pgtable_stage2_map()填 S2 PTE(属性DEVICE_nGnRE)
详细时序见附录 A。
3. QEMU 侧:MemoryRegion 树
从本节开始,我们自顶向下梳理 QEMU 进程中维护的数据结构。QEMU 使用一棵 MemoryRegion 树(以下简称 MR 树)来描述 Guest 的整个物理地址空间。
MR 树拓扑对比:
3.1 MR 的三种类型
每个 MemoryRegion 描述 GPA
空间中的一段区域,核心字段如下(include/exec/memory.h):
1 | struct MemoryRegion { |
| subregions | ram_block | 典型例子 | 说明 |
|---|---|---|---|
| 无 | 有 | mmaps[0].mem, mach-virt.ram |
叶子节点,ram_block->host =
HVA |
| 无 | 无 | GIC, UART 等纯 IO MR | 纯 IO MR 的叶子节点只通过 ops 回调处理
MMIO |
| 有 | 无 | bar->mr, region.mem |
纯容器/IO 层,通过子 MR 定义内容 |
| 有 | 有 | 被 overlay 切割的 RAM | 罕见 |
只有叶子 ram_device MR 才能触发 KVM memslot
注册,也只有叶子节点才不会再挂有subregions——中间的容器
MR、alias MR、IO MR 没有 ram_block,在 memslot
创建阶段会被直接跳过。
subregions→ 树结构:这里是因为,每个MR都是三级结构,所以只有叶子MR才会存这个MR的实际数据,也只有叶子MR的subregions才会是空的。负责"这个 GPA 区间包含哪些子区域"ram_block→ 数据后端:ram_block->host是 HVA 指针。负责"实际数据在宿主机的哪个地址"
MR 叶子节点(L3) 按
ram、ram_device、ops
三个标志的组合分为三种核心类型,它们从根本上决定了 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
的同级子区域
从bar->mr 到 bar.region.mem 到 bar.region.mem.mmaps[0].mem,就是ram_device MR树的三层结构:
1 | pcie-mmio → GPEX io_mmio |
有 Root Port 时:
每个 Root Port 是一个 PCI Bridge,拥有独立的
sec_bus.address_space_mem。Bridge 通过
pci_bridge_init_alias在父总线地址空间中创建一个 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 |
两种拓扑的核心差异在于:无 Root Port 时,多个 VF 的 BAR 在同一个 GPA 窗口中竞争空间;有 Root Port 时,每个 VF 被隔离在独立的 bridge window 中。
4. QEMU 侧:VFIO BAR 的三层 MR 嵌套
VFIO BAR 的数据结构包含 VFIOBAR →
VFIORegion → VFIOMmap 三层,正好对应
subregions 链上由浅入深的三个 MR 节点(L1→L2→L3):
VFIOBAR:持有bar->mr(L1,纯容器,无 ram_block,无 ops)VFIORegion:持有region->mem(L2,IO 层,ops=vfio_region_ops,无 ram_block)VFIOMmap:内嵌mmaps[0].mem(L3,ram_device 叶子,ram_block->host=HVA,真正与 KVM memslot 对接的一层)
1 | struct VFIOBAR { |
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。多个 mmaps 仅出现在 sparse BAR 场景(BAR 内部存在不连续的可映射区域,如 GPU VRAM 被寄存器窗口打断)。本文后续均以
nr_mmaps = 1为前提。
4.2 mmap 成功 vs 失败 —— BAR 结构的两种形态
QEMU mmap BAR的时候,可能会成功,也可能失败:
- BAR mmap 成功时,BAR 的 MR 会有三层 MR,且
mmaps[0].mem挂载了ram_block。- 也就是会存 Host VMA 返回的 HVA。可以用于后面引导建立 S2 映射。
- 可以直接执行 BAR 的 read/write,由 guest 驱动通过系统调用直接访问。
- BAR mmap 失败时,BAR 的 MR 只有两层 MR,且
mmaps[0].mem挂载了ram_block。- 也就是会存
NULL。不能用于引导建立 S2 映射 - 此时的叶子节点
ops变量注册了ops->read/write回调函数,guest 驱动访问 BAR 时会触发 VM exit,由 QEMU 通过 pread/pwrite 借助 host vfio 的pread(VFIO_FD)/pwrite(VFIO_FD)走慢路径访问设备。
- 也就是会存
叶子节点的各种属性和处理流程参考3.1 MR 的三种类型。
MMIO 快/慢 路径访问流程参考附录 B:ops 回调与 MMIO 慢路径派发。
注意:
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 地址空间。这个流程分三个阶段,横跨 QEMU
初始化和 Guest 运行时。
4.3.1 [QEMU EL0] pci_register_bar:PCI 台账固化
QEMU 初始化时,vfio_bar_register 的最后一步调用
pci_register_bar。此时 Guest
尚未启动,这一步只做元数据记录——不涉及任何 GPA
分配,bar->mr 还挂在"空中"。
每个 PCI BAR 对应一个
PCIIORegion(hw/pci/pci.c:1321-1333):
1 | pcibus_t size = memory_region_size(bar->mr); // 从已建好的 MR 读大小 |
r->size 的值来自【Host
VFIO】驱动的VFIO_DEVICE_GET_REGION_INFO,赋值后不再改变(不受后续
MR size 变化的影响):
1 | [QEMU EL0] |
wmask 写入虚拟 PCI 配置空间后,Guest 可通过 PCI 协议探测
BAR 大小。
4.3.2 [Guest EL1] PCI 枚举:读大小 + 分配 GPA + 写 BAR
Guest OS 启动后,PCI 子系统扫描总线,对每个 BAR 执行三步:
- 读 BAR 大小:写全 1 到 BAR 配置寄存器 → VM exit →
QEMU 模拟硬件写0的行为,返回
val & wmask→ Guest 推断 BAR = 32KB - 分配 GPA:
pbus_assign_resources_sorted→pci_assign_resource→ 在 PCI MMIO 窗口内找对齐的可用区间 → 算出 GPA - 写 BAR
寄存器:
pci_write_config_dword(dev, BAR2, GPA)→ VM exit → QEMU
4.3.3 [QEMU EL0] pci_update_mappings:BAR MR 挂入 GPA
Guest 写 BAR 寄存器触发 VM exit,QEMU 的
pci_default_write_config 更新配置空间后调用
pci_update_mappings()(hw/pci/pci.c:1511-1553):
1 | new_addr = pci_bar_address(d, i, r->type, r->size); // 读 Guest 写的 GPA + 对齐校验 |
pci_register_bar 是初始化时"建台账",pci_update_mappings 是运行时"挂牌子"——前者记录元数据,后者把 bar->mr 挂到 Guest 分配的 GPA 区间上。 MR 树变化后标记 FlatView 脏,下一节讲后续的 FlatView 重建和 memslot 注册。
5. QEMU 侧:FlatView 展平与 memslot 注册
MR 树经 pci_update_mappings 挂入 GPA
后,只是标记了"脏",真正的 FlatView 重建和 memslot 注册由后续的
vfio_sub_page_bar_update_mapping →
memory_region_transaction_commit 触发。
本节自底向上讲三层逻辑:FlatView 怎样渲染、渲染前 MR 能否扩张、渲染后 section 怎样进入 KVM。
包括渲染->MR扩展->重叠处理->构建section->KVM MemorySlot的流程见附录 C。
5.1 FlatView 与 FlatRange
MR 树是嵌套结构,KVM 需要按 GPA 线性查找。
QEMU 将 MR 树展开为 FlatView,FlatVIew是一组按 GPA
排序、互不重叠的 FlatRange 数组。
这里相当于把MR作为脚手架来创建 FlatRange。然后FlatRange再产生ADD/DEL事件,生成对应的section,section再注册到KVM创建kvm MemorySlot。
1 | // memory.h:1278 |
qemu自动监听发现MR树更改了,会调用generate_memory_topology()
递归遍历 MR 树 → render_memory_region() 渲染生成
FlatRange[] → flatview_simplify()
合并相邻同类区间(也就是截断重叠部分)。
5.2 FlatView 渲染
render_memory_region()保证 FlatRange
数组互不重叠,且该函数在构建 FlatView
时,用的是mr size来构建,而不是vfio size。渲染一个
MR 时:
- 先递归渲染 subregions(QTAILQ 顺序,同 priority 时后插入的排前面)
- 再渲染自身到空隙(gap-filling:
Render the region itself into any gaps left by the current view)
场景一:BAR 不重叠
两个 VF 的 BAR2 GPA 区间互不干扰。以 VF0 BAR2=32KB@0x8000_200000、VF1 BAR2=32KB@0x8000_400000 为例(间隔 2MB,无重叠)。
MR 树: 1
2
3GPEX io_mmio
├── VF0 BAR2 [GPA 0x8000_200000, size=32KB] (MR)
└── VF1 BAR2 [GPA 0x8000_400000, size=32KB] (MR)
render_memory_region() 遍历 subregions 后,生成的 FlatRange 数组:
1
2FlatRange[0]: { .mr=VF0 BAR2, .addr={start=0x8000_200000, size=32KB} }
FlatRange[1]: { .mr=VF1 BAR2, .addr={start=0x8000_400000, size=32KB} }
场景二:BAR 重叠 — 裁剪
两个 VF 的 BAR2 GPA 区间有交集。假定在64KB的PAGESIZE环境下,有:
- VF0:BAR2 物理大小 32KB,GPA=0x8000_200000(GPA 64KB 对齐)→
vfio_sub_page_bar_update_mapping扩张 MR 到 64KB,范围 [0x8000_200000, 0x8000_20ffff] - VF1:BAR2 物理大小 32KB,GPA=0x8000_208000(低 16 位=0x8000,不对齐)→ 不扩张,MR 保持 32KB,范围 [0x8000_208000, 0x8000_20ffff]
重叠区间:[0x8000_208000, 0x8000_20ffff] = 32KB。
VF1 后插入(Guest 枚举顺序),在 QTAILQ 头部,先渲染。
MR 树: 1
2
3GPEX io_mmio
├── VF1 BAR2 [GPA 0x8000_208000, size=32KB] (后插入→QTAILQ头部, 未扩张)
└── VF0 BAR2 [GPA 0x8000_200000, size=64KB] (先插入→QTAILQ尾部, 已扩张)
render_memory_region() 先渲染 VF1(QTAILQ 头部),再渲染 VF0 到空隙:
1
2
3
4Step 1: 渲染 VF1 BAR2 → FlatRange[0]: { .mr=VF1, .addr={0x8000_208000, 32KB} }
Step 2: 渲染 VF0 BAR2 → gap-filling:
[0x8000_200000, 0x8000_208000) 是空隙 → FlatRange[1]: { .mr=VF0, .addr={0x8000_200000, 32KB} }
0x8000_208000 之后已被 VF1 占用 → VF0 其余 32KB 被裁剪!
最终 FlatRange 数组: 1
2FlatRange[0]: { .mr=VF1 BAR2, .addr={start=0x8000_208000, size=32KB} } ← 完整, MR.size=32KB
FlatRange[1]: { .mr=VF0 BAR2, .addr={start=0x8000_200000, size=32KB} } ← 被裁剪! MR.size=64KB, FlatRange.size=32KB
关键:
VF0 MR 的 size 仍是 64KB,但 FlatRange 的 addr.size
已经是裁剪后的值(32KB)。后续 MemoryRegionSection 的 size
来自 FlatRange,不是来自 MR,所以也是 32KB。
这个时候,如果PAGESIZE是64KB的话就有问题了,后续的 kvm_align_section 会把这个 section 过滤掉,导致 memslot 没有创建
5.3 vfio_sub_page_bar_update_mapping:FlatView 重建前的扩张机会
在 memory_region_transaction_commit 触发 FlatView
重建之前,还有一次修改 MR size 的机会:
vfio_pci_write_config 在
pci_default_write_config之后调用
vfio_sub_page_bar_update_mapping()尝试扩展BAR 的 MR
size到64KB:
核心逻辑:
1 | if (bar_addr != PCI_BAR_UNMAPPED && |
扩展成功的话,通过 memory_region_set_size 将新 size=64KB
写入三级 MR:
1 | memory_region_transaction_begin(); |
两种情况:
- BAR 基地址 64KB 对齐(如
0x8000_200000):扩张成功 → 三级 MR 的 size 全部变为 64KB → FlatView 渲染后 FlatRange 也是 64KB - BAR 基地址不对齐(如
0x8000_208000,低 16 位 =0x8000≠ 0):!(bar_addr & ~PAGE_MASK)为 false → size 保持 32KB → 三级 MR 的 size 仍是 32KB
关于
qemu_real_host_page_size():这个值来自 Host 内核的PAGE_SIZE(sysconf(_SC_PAGESIZE)),ARM64 64KB 页内核下固定返回 64KB。它和 BAR 本身的大小无关——它代表着"Host 能管理的最小内存页粒度",也是 KVM memslot 的最小粒度。
注意 r->size(PCIIORegion)不受影响——它在
pci_register_bar 时就固化了,之后的 MR size
变化不会回写给它。
5.4 kvm_align_section:决定 section 是否进入 KVM memslot
memory_region_transaction_commit 触发 FlatView
重建后,QEMU 对比新旧 FlatView,通过 MemoryListener
机制将差异派发给 KVM:
1 | transaction commit |
kvm_set_phys_mem()有两个过滤点:
过滤点 1:!memory_region_is_ram(mr)
IO 类型 MR(如 L2
region.mem,ram=false)直接跳过——mmap 失败时
BAR 只剩 IO MR,不会注册 memslot 给 KVM。
过滤点
2:kvm_align_section(kvm-all.c:266)
将 section 的 GPA 向上对齐到 PAGE_SIZE 边界,截断超出部分:
1 | aligned = ROUND_UP(section->offset_within_address_space, qemu_real_host_page_size()); |
这里kvm_align_section失败后还有一次扩展尝试,扩展失败后才是真的不能创建
memslot:
两种典型结果:
| 场景 | section.size | section GPA | delta | 结果 |
|---|---|---|---|---|
| 扩张成功 | 64KB | 64KB 对齐 (0x8000_200000) | 0 | 返回 64KB → 通过 |
| 扩张失败 | 32KB | 不对齐 (0x8000_208000) | 0x8000 = 32KB | delta ≥ size → 返回 0 → 跳过 |
通过kvm_align_section过滤后,QEMU 组装
KVMSlot 发起
ioctl(KVM_SET_USER_MEMORY_REGION):
1 | typedef struct KVMSlot { |
KVMSlot 是快照——记录 ioctl 发送到内核那一刻的
GPA/HVA/size 三元组,不持有 FlatView 或 FlatRange 的引用。
6. KVM 侧:memslot 与 S2 页表
上一章从 QEMU 侧梳理了 MR 树 → FlatView → KVMSlot 的链路。KVMSlot
通过 ioctl(KVM_SET_USER_MEMORY_REGION) 发送到内核后,由 KVM
接管。本章说明 KVM 如何组织这些 memslot 以及它们如何驱动 S2
页表的建立。
6.1 KVM memslot 的三级嵌套结构
KVM 中 memslot 的组织形式是三层嵌套:
1 | struct kvm (一个 VM 实例) |
三层结构的职责:
| 层 | 类型 | 说明 |
|---|---|---|
kvm->memslots[] |
指针数组,通常 1 个元素 | memslots[0] 指向唯一的 kvm_memslots
容器 |
kvm_memslots |
容器结构体 | 内含 memslot 数组 + used_slots 计数 +
generation 版本号 |
kvm_memory_slot |
单个 memslot | 描述一段连续 GPA 区间 |
memslot 数量上限:KVM_MEM_SLOTS_NUM = 32767。每个 VM
可以有最多 32767 个 memslot,远超实际需要。
6.2 kvm_memory_slot 结构体
1 | struct kvm_memory_slot { |
以 VF0 BAR2 为例(GPA=0x8000_200000,size=64KB):
1 | base_gfn = 0x8000_2000, // GPA 0x8000_200000 >> 16 |
memslot 不存 HPA,存的是 HVA。 KVM 需要 HPA 时通过
get_user_pages(userspace_addr) 动态查 Host
S1(TTBR0_EL2)页表获取。
6.3 memslot 与 S2 页表的关系
ioctl(KVM_SET_USER_MEMORY_REGION) 只是创建 KVM memslot
数据结构——它不填 S2 PTE。实际建立 GPA→HPA 的 S2
页表发生在 S2 fault 时,以 memslot 为脚手架:
1 | ioctl(KVM_SET_USER_MEMORY_REGION, {GPA, HVA, size}) |
每个 memslot 对应 S2 页表中的一段 GPA→HPA 映射区间。注意这个过程与 2.3 页表建立时序 的 Phase 2 和 Runtime 阶段一致——memslot 创建时只是"登记",真正的页表建立由 Guest 访问触发。
6.4 QEMU KVMSlot vs KVM kvm_memory_slot
QEMU 内部也维护了一份 memslot 缓存叫 KVMSlot,与内核的
kvm_memory_slot 通过 ioctl 同步:
1 | QEMU [EL0] KVMSlot (缓存): KVM [EL2] kvm_memory_slot (权威): |
QEMU 的 KVMSlot 只是本地缓存——QEMU
不需要每次都查内核状态,直接用缓存的 start_addr +
memory_size 就知道当前 memslot 的 GPA 范围,用于后续比对
FlatView 变化。
7. 完整数据链路
前面的章节是"纵向"的——每章聚焦一层(QEMU MR 树、FlatView、KVM memslot)。本章做"横向"串联,把从 QEMU 初始化到 S2 页表建立的完整流程按时间顺序串起来。
7.1 流程概览
1 | 【qemu】QEMU侧向host发起ioctl(VFIO_DEVICE_GET_REGION_INFO) |
7.2 全量数据结构关系图
关键区分:
| 符号 | PlantUML 含义 | 代码层面 |
|---|---|---|
◆ (实心菱形) |
组合(composition),值嵌入 | mmaps[0].mem 直接嵌入
VFIOMmap,VFIOBAR 值数组嵌入
VFIOPCIDevice |
→ (箭头) |
关联(association),指针 | bar->mr 是堆上的 MemoryRegion* |
⇢ (虚线) |
依赖(dependency),数据流动 | RAMBlock.host 的值来自 mmap
返回值;KVMSlot 由 MemoryRegionSection
组装 |
关键要点(均在 7.1 流程中对应):
- HVA 的唯一来源:
mmaps[i].mmap→ 经memory_region_init_ram_device_ptr()存入L3.ram_block->host - GPA 的来源:Guest 分配(PCI 枚举)→ 写 BAR 寄存器 →
QEMU
pci_update_mappings()读出,缓存到r->addr,挂入 GPEX io_mmio → 展平后在FlatRange.addr.start - FlatView
的叶子节点:
render_memory_region先渲染 subregions,被 subregions 占满的区域不再渲染父 MR。正常 vfio-pci 场景下只有 L3(ram_device)进 FlatView(L3 占满 L2,L2 无空隙可填)。mmap 失败时 L2 变为叶子节点(IO 类型,terminates=true),也会进 FlatView——render_memory_region中只检查terminates,不检查mr->ram - 只有 ram_device 类型进
memslot:
kvm_set_phys_mem检查memory_region_is_ram(),IO 类型(ram=false)直接跳过。进 FlatView ≠ 进 memslot,两道独立的关卡
从 MR 树到 KVM S2 页表的 L3→FlatRange→KVMSlot→kvm_memory_slot 数据流 ASCII 图见附录 D。
8. 正常场景走一遍
以单 VF、vfio-pci 驱动、BAR2 物理大小 64KB、Host PAGE_SIZE=64KB 为例,按时间顺序走一遍完整链路。原理细节引用前文章节。
① QEMU 初始化(4.3.1)
1 | ioctl(VFIO_DEVICE_GET_REGION_INFO) → info.size=64KB |
② Guest PCI 枚举(4.3.2)
1 | Guest 写 BAR = 全 F → QEMU 返回 wmask → Guest 推断 BAR2=64KB |
③ BAR MR 挂入 PCI 总线 + 扩张(4.3.3 + 5.3)
1 | vfio_pci_write_config → pci_default_write_config → pci_update_mappings() |
④ FlatView 展平 + KVM memslot 注册(5.2 + 5.4)
1 | render_memory_region() → FlatRange: {.mr=L3, .addr={0x8000_200000, 64KB}} |
1 | __kvm_set_memory_region → kvm_check_memslot_overlap → 无重叠 ✓ |
⑥ Guest 首次访问 → S2 按需建立(2.3 + 6.3)
1 | Guest: ioremap(GPA 0x8000_200000) → 填 Guest S1 PTE (TTBR1_EL1) |
⑦ 正常访问
1 | Guest 再次 ldr [GVA]: |
正常场景下所有 size 字段均为 64KB(详见 7.2 类图标注),mmap 成功,ram_device MR 创建,扩张生效,FlatView 完整,memslot 建立,S2 就绪。
附录 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。
附录 C:BAR 重叠场景下的完整调用链
以下是以 Guest 写入 VF2 BAR2 基地址 0x8000408000(不对齐
64KB)为例的完整调用链,展示了 BAR 重叠时 memslot
注册失败的每一步。本篇为基础文章,此附录供后续问题篇索引。
1 | Guest 写入 VF2 BAR2 配置空间 |
附录 D:从 L3 到 KVM S2 页表的 ASCII 数据流图
以下 ASCII 流程图展示正常场景下(BAR 扩张成功,64KB 对齐),数据如何从 QEMU 的 L3 MR 叶子节点一步步流到 KVM 的 S2 PTE:
1 | QEMU EL0 KVM EL2 |