linux arm 架构下的stage-2翻译机制
在linux上运行kvm虚拟机时,内核如何管理虚拟机的内存地址,如何实现虚拟机vcpu的一次访存,如何保证虚拟机只能访问特定的地址范围。这就需要stage-2翻译机制。这里介绍arm下的stage-2实现,x86下有intel EPT的类似实现。
一些虚拟机概念
- GVA: Guest Virtual Address,虚拟机的虚拟地址
- GPA: Guest Physical Address,虚拟机的物理地址,也是中间翻译地址IPA
- PA:Physical Address,物理地址
- VMID:虚拟机标识,对应一个 stage-2 页表
- vcpu:虚拟cpu,运行在 Guest EL1,访存时经过 stage-2 翻译
- stage-1: GVA→IPA,Guest OS 自己的页表完成
- stage-2: IPA→PA,由 KVM 管理的页表完成
- pfn:page frame number,页帧号,物理页的地址
关键寄存器
以下内容节选自 Arm Architecture Reference Manual Armv8, for Armv8-A architecture profile
VTTBR_EL2, Virtualization Translation Table Base Register
- 存放 stage-2 查表的页表基址 BADDR 和该页表的虚拟机标识 VMID
- 一个虚拟机对应一个 VMID 与 一个 BADDR,拥有独立的 stage-2 页表,从而硬件上实现虚拟机内存隔离
VTCR_EL2,Virtualization Translation Control Register
- 配置 stage-2 地址翻译的控制位
HCR_EL2, Hypervisor Configuration Register
- 虚拟化配置的总控置位,定义多种操作是否陷入EL2
- HCR_EL2.VM,启用 EL1&0 的 stage 2 转换
ESR_EL2, Exception Syndrome Register (EL2)
- 存放陷入到EL2的异常的信息
- Stage-2 翻译故障切换到EL2时填充
内核里的数据结构
struct kvm
// include/linux/kvm_host.h
struct kvm {
rwlock_t mmu_lock;
struct mutex slots_lock;
struct mm_struct *mm; /* 虚拟机用户态的地址空间 */
struct kvm_memslots __rcu *memslots[KVM_MAX_NR_ADDRESS_SPACES]; /* 当前生效的对每一个地址空间的内存槽 */
struct xarray vcpu_array; /* vcpu */
struct kvm_arch arch;
/*...*/
}
struct kvm_vcpu
// include/linux/kvm_host.h
struct kvm_vcpu {
struct kvm *kvm; /* 所属的kvm */
int vcpu_id; /* 用户态分配的cpu的id */
struct kvm_arch arch; /* 虚拟机架构相关 */
struct kvm_vcpu_arch arch; /* 虚拟机vcpu架构相关 */
/*...*/
};
struct kvm_arch
// arch/arm64/include/asm/kvm_host.h
struct kvm_arch {
struct kvm_s2_mmu mmu; /* 当前虚拟机的stage-2页表 */
struct kvm_s2_mmu *nested_mmus; /* 嵌套虚拟机使用的影子 s2 mmu 数组 */
struct vgic_dist vgic;
/*...*/
};
struct kvm_vcpu_arch
// arch/arm64/include/asm/kvm_host.h
struct kvm_vcpu_arch {
struct kvm_cpu_context ctxt; /* 虚拟机vcpu的上下文,包含所有系统寄存器的快照 */
struct kvm_s2_mmu *hw_mmu; /* 硬件下一次使用的 stage-2 页表 */
u64 hcr_el2; /* 虚拟机的异常处理控制位,HCR_EL2.VM在这里 */
/*...*/
};
在 vcpu 的架构体中保存有 stage-2 页表的指针
struct kvm_s2_mmu
// arch/arm64/include/asm/kvm_host.h
struct kvm_s2_mmu {
struct kvm_vmid vmid; /* 虚拟机标识,存放 VTTBR_EL2.VMID*/
phys_addr_t pgd_phys; /* 页表基址,存放 VTTBR_EL2.BADDR*/
struct kvm_pgtable *pgt; /* 页表抽象 */
u64 vtcr; /* 虚拟化控制寄存器,存放 VTCR_EL2*/
struct kvm_arch *arch; /* 所属的kvm_arch */
/*...*/
};
可以看到在这里就用上了上文提到的架构相关的寄存器 VTTBR_EL2 和 VTCR_EL2。
函数调用链
创建虚拟机时创建 stage-2 页表
部分函数调用链
ioctl(KVM_CREATE_VM)
|-- kvm_vm_ioctl_create_vm
|-- kvm_create_vm
|-- kvm_arch_init_vm
|-- kvm_init_stage2_mmu
|-- kvm_init_ipa_range
|-- kvm_pgtable_stage2_init
|-- __kvm_pgtable_stage2_init
kvm_arch_init_vm
// arch/arm64/kvm/arm.c
int kvm_arch_init_vm(struct kvm *kvm)
{
ret = kvm_init_stage2_mmu(kvm, &kvm->arch.mmu, type);
}
kvm_arch_init_vm:arm64 的 VM 的初始化,完成 stage-2 mmu 的初始化
kvm_init_stage2_mmu
// arch/arm64/kvm/mmu.c
int kvm_init_stage2_mmu(struct kvm *kvm, struct kvm_s2_mmu *mmu, unsigned long type)
{
err = kvm_init_ipa_range(mmu, type); /* 确定PA范围 */
pgt = kzalloc_obj(*pgt, GFP_KERNEL_ACCOUNT); /* 分配页表 */
mmu->arch = &kvm->arch;
err = KVM_PGT_FN(kvm_pgtable_stage2_init)(pgt, mmu, &kvm_s2_mm_ops); /* 初始化页表 */
mmu->pgt = pgt;
mmu->pgd_phys = __pa(pgt->pgd); /* 记录 pgd 的物理地址 */
}
至此完成了页表在内核的分配。pgd(Page Global Directory)是页表的根节点,作为 kvm 操作 stage-2 页表的入口。也是填入 VTTBR_EL2.BADDR 的值。
kvm_init_ipa_range
// arch/arm64/kvm/mmu.c
static int kvm_init_ipa_range(struct kvm_s2_mmu *mmu, unsigned long type)
{
u32 kvm_ipa_limit = get_kvm_ipa_limit(); /* 获取 IPA 的范围 */
mmfr0 = read_sanitised_ftr_reg(SYS_ID_AA64MMFR0_EL1);
mmfr1 = read_sanitised_ftr_reg(SYS_ID_AA64MMFR1_EL1);
mmu->vtcr = kvm_get_vtcr(mmfr0, mmfr1, phys_shift); /* 写入 VTCR_EL2 的值 */
}
__kvm_pgtable_stage2_init
// arch/arm64/kvm/hyp/pgtable.c
int __kvm_pgtable_stage2_init(struct kvm_pgtable *pgt, struct kvm_s2_mmu *mmu,
struct kvm_pgtable_mm_ops *mm_ops,
enum kvm_pgtable_stage2_flags flags,
kvm_pgtable_force_pte_cb_t force_pte_cb)
{
size_t pgd_sz;
u64 vtcr = mmu->vtcr;
u32 ia_bits = VTCR_EL2_IPA(vtcr); /* 获取 IPA 的宽度 */
u32 sl0 = FIELD_GET(VTCR_EL2_SL0_MASK, vtcr);
s8 start_level = VTCR_EL2_TGRAN_SL0_BASE - sl0; /* 获取起始的页表级别 */
pgd_sz = kvm_pgd_pages(ia_bits, start_level) * PAGE_SIZE; /* 计算 pgd 的页数 */
pgt->pgd = (kvm_pteref_t)mm_ops->zalloc_pages_exact(pgd_sz); /* 分配 pgd 的页 */
if (!pgt->pgd)
return -ENOMEM;
pgt->ia_bits = ia_bits; /* 写回 struct kvm_pgtable */
pgt->start_level = start_level;
pgt->mm_ops = mm_ops;
pgt->mmu = mmu;
pgt->flags = flags;
pgt->force_pte_cb = force_pte_cb;
/* Ensure zeroed PGD pages are visible to the hardware walker */
dsb(ishst);
return 0;
}
至此 stage-2 页表结构体已经建好但内容仍为空,等到 guest 发生缺页时再填充映射
guest 访存的 stage-2 翻译
vcpu首次运行时,将对应的值填入寄存器
- VTCR_EL2 ← mmu->vtcr
- VTTBR_EL2.BADDR ← mmu->pgd_phys
- VTTBR_EL2.VMID ← mmu->vmid.id
当 HCR_EL2.VM 置位,guest 的访存经过 stage-2 的翻译
mmu: stage1 (VA→IPA) → stage2 (IPA→PA) 发现 IPA 无映射,触发异常,硬件陷入 EL2
kvm 侧完成异常处理的分发,最终调用到 kvm 的缺页处理入口
部分函数调用链
kvm_handle_guest_abort
|-- kvm_vcpu_get_fault_ipa // 计算 ipa
|-- user_mem_abort
|-- kvm_s2_fault_pin_pfn // 获取主机的 pfn
|-- kvm_s2_fault_compute_prot
|-- kvm_s2_fault_map
|-- KVM_PGT_FN(kvm_pgtable_stage2_map)
user_mem_abort
// arch/arm64/kvm/hyp/pgtable.c
static int user_mem_abort(const struct kvm_s2_fault_desc *s2fd)
{
ret = kvm_s2_fault_pin_pfn(s2fd, &s2vi); /* 获取host的 pfn,得到物理页 */
ret = kvm_s2_fault_compute_prot(s2fd, &s2vi, &prot); /* 计算访问权限 */
return kvm_s2_fault_map(s2fd, &s2vi, prot, memcache); /* 映射到 stage-2 页表 */
}
在 kvm 侧完成缺页的解析并建立映射
完成后返回 vcpu 的执行流,geust 再次执行访存,此时 stage-2 命中,访存成功

浙公网安备 33010602011771号