最近在调一个设备直通的性能问题时,我同时把QEMU和KVMTool两边对guest物理内存的管理代码翻了个底朝天。查着查着发现一个很有意思的现象:同样是把GPA映射到HVA,两边最终都落在KVM_SET_USER_MEMORY_REGION这个ioctl上,但前置的代码路径和心智模型却差了十万八千里。网上讨论qemu安装、Windows下WHPX、虚拟显卡svga的帖子不少,可一旦你往深了走——设备直通、热迁移、内存热插拔——最后一定绕不开GPA、IOVA、HVA这三个地址概念。这篇文章我就把两边在GPA到HVA映射这条链路上的异同掰开揉碎讲一遍,适合已经跑通过基本虚拟化环境、准备深挖KVM内存子系统的朋友。
1. 先厘清一组容易混淆的地址概念:GPA、IOVA、HVA到底谁是谁
1.1 一次内存访问在虚拟化环境中要经过几层翻译
很多初学者会把GPA和HVA当成同一个东西,其实不是。一次guest内部的内存访问,在KVM加速下大致要经过四层地址转换:
- GVA(Guest Virtual Address):guest进程看到的虚拟地址,由guest自己的页表负责。
- GPA(Guest Physical Address):guest物理地址,CPU通过EPT/NPT把GVA翻译成GPA。
- HVA(Host Virtual Address):宿主上VMM进程(比如qemu或kvmtool)地址空间里的虚拟地址。
- HPA(Host Physical Address):真正的物理内存,由宿主的进程页表把HVA翻译成HPA。
这里容易忽略的一个点是:KVM内核模块只负责两件事——通过EPT/NPT加速“GVA到GPA”的翻译,以及维护一张“GPA到HVA”的映射表。GPA到HVA的映射由VMM通过KVM_SET_USER_MEMORY_REGION注册给KVM,而HVA到HPA的映射,本质上是VMM自己用户态地址空间的普通页表映射,由宿主内核正常的内存管理来维护。
换句话说,KVM的memory slot就是一座桥:客体的物理地址要想落到宿主上真实的内存,必须先把这座桥架好。
1.2 IOVA在标题语境下到底指什么
标题里的IOVA会让人有点困惑,因为它在不同场景下至少有两个身份。
一个是IOMMU/设备视角的IOVA(I/O Virtual Address)。设备发起DMA时使用的是IOVA,IOMMU负责把IOVA翻译成HPA。设备直通(PCI passthrough/VFIO)场景下,VMM会尽量让设备看到的IOVA与GPA保持一致,这样guest驱动里写好的DMA地址可以直接用,省去guest内部再做一次地址转换。这是常见的“IOVA=GPA”策略。
另一个身份更贴近本文的语境:当设备由软件模拟时,设备模型看到的“客户机物理地址”其实也叫IOVA。一个模拟出来的网卡或显卡,它眼中的总线地址就是GPA。所以在这类讨论中,IOVA与GPA经常被混着用,本质上说的是同一件事。
所以在本文里,我统一把GPA当作用户侧地址,HVA当作宿主侧地址,IOVA则指的是设备/IOMMU视角下的地址。搞清楚了这层关系,后面再看两套VMM的映射实现才不会绕晕。
1.3 定义GPA到HVA映射的那个核心数据结构
不管QEMU还是KVMTool,最终都会申请一个struct kvm_userspace_memory_region然后发起ioctl:
c复制struct kvm_userspace_memory_region {
__u32 slot;
__u32 flags;
__u64 guest_phys_addr;
__u64 memory_size;
__u64 userspace_addr;
};
其中guest_phys_addr是GPA起始地址,userspace_addr是HVA起始地址,memory_size是这段区间的长度,flags里常用的有KVM_MEM_LOG_DIRTY_PAGES(脏页记录)和KVM_MEM_READONLY(只读)。
这个ioctl是KVM暴露给VMM最核心的内存接口。QEMU和KVMTool的所有差异,本质上都是在“怎么把用户态内存对象翻译成这个结构体”这件事上产生的。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. QEMU的映射路径:从MemoryRegion到KVMSlot中间隔了多少层
2.1 为什么要引入MemoryRegion和AddressSpace这套复杂模型
QEMU的地址管理是出了名的绕,但绕得有道理。它面对的设备模型太复杂了:PCI总线上的每个设备都有BAR空间、ROM空间,板卡上还有各种MMIO区域、保留区域,而且这些区域在运行时可能被重新分配、热插拔、别名映射。
如果像裸机一样直接用“一段连续物理内存”来描述整个guest地址空间,根本不可能。所以QEMU设计了两层抽象:
- MemoryRegion
