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 命中,访存成功

posted @ 2026-09-14 00:01  Lanoz  阅读(18)  评论(0)    收藏  举报