【原创】Linux虚拟化KVM-Qemu分析(五)以内存虚拟化

背景

  • Read the fucking source code! --By 鲁迅
  • A picture is worth a thousand words. --By 高尔基

说明:数据结构

  1. KVM版本:5.9.1
  2. QEMU版本:5.0.0
  3. 工具:Source Insight 3.5, Visio
  4. 文章同步在博客园:https://www.cnblogs.com/LoyenWang/

1. 概述

《Linux虚拟化KVM-Qemu分析(二)之ARMv8虚拟化》文中描述过内存虚拟化大致框架,再来回顾一下:app

  1. 非虚拟化下的内存的访问

  • CPU访问物理内存前,须要先创建页表映射(虚拟地址到物理地址的映射),最终经过查表的方式来完成访问。在ARMv8中,内核页表基地址存放在TTBR1_EL1中,用户空间页表基地址存放在TTBR0_EL0中;
  1. 虚拟化下的内存访问

  • 虚拟化状况下,内存的访问会分为两个StageHypervisor经过Stage 2来控制虚拟机的内存视图,控制虚拟机是否能够访问某块物理内存,进而达到隔离的目的;
  • Stage 1VA(Virtual Address)->IPA(Intermediate Physical Address),Host的操做系统控制Stage 1的转换;
  • Stage 2IPA(Intermediate Physical Address)->PA(Physical Address),Hypervisor控制Stage 2的转换;

猛一看上边两个图,好像明白了啥,仔细一想,啥也不明白,本文的目标就是将这个过程讲明白。框架

在开始细节讲解以前,须要先描述几个概念:函数

gva - guest virtual address
gpa - guest physical address
hva - host virtual address
hpa - host physical address

  • Guest OS中的虚拟地址到物理地址的映射,就是典型的常规操做,参考以前的内存管理模块系列文章;

铺垫了这么久,来到了本文的两个主题:工具

  1. GPA->HVA;
  2. HVA->HPA;

开始吧!this

2. GPA->HVA

还记得上一篇文章《Linux虚拟化KVM-Qemu分析(四)之CPU虚拟化(2)》中的Sample Code吗?
KVM-Qemu方案中,GPA->HVA的转换,是经过ioctl中的KVM_SET_USER_MEMORY_REGION命令来实现的,以下图:spa

找到了入口,让咱们进一步揭开神秘的面纱。操作系统

2.1 数据结构

关键的数据结构以下:
debug

  • 虚拟机使用slot来组织物理内存,每一个slot对应一个struct kvm_memory_slot,一个虚拟机的全部slot构成了它的物理地址空间;
  • 用户态使用struct kvm_userspace_memory_region来设置内存slot,在内核中使用struct kvm_memslots结构来将kvm_memory_slot组织起来;
  • struct kvm_userspace_memory_region结构体中,包含了slot的ID号用于查找对应的slot,此外还包含了物理内存起始地址及大小,以及HVA地址,HVA地址是在用户进程地址空间中分配的,也就是Qemu进程地址空间中的一段区域;

2.2 流程分析

数据结构部分已经罗列了大致的关系,那么在KVM_SET_USER_MEMORY_REGION时,围绕的操做就是slots的建立、删除,更新等操做,话很少说,来图了:3d

  • 当用户要设置内存区域时,最终会调用到__kvm_set_memory_region函数,在该函数中完成全部的逻辑处理;
  • __kvm_set_memory_region函数,首先会对传入的struct kvm_userspace_memory_region的各个字段进行合法性检测判断,主要是包括了地址的对齐,范围的检测等;
  • 根据用户传递的slot索引号,去查找虚拟机中对应的slot,查找的结果只有两种:1)找到一个现有的slot;2)找不到则新建一个slot;
  • 若是传入的参数中memory_size为0,那么会将对应slot进行删除操做;
  • 根据用户传入的参数,设置slot的处理方式:KVM_MR_CREATEKVM_MR_MOVEKVM_MEM_READONLY
  • 根据用户传递的参数决定是否须要分配脏页的bitmap,标识页是否可用;
  • 最终调用kvm_set_memslot来设置和更新slot信息;

2.2.1 kvm_set_memslot

具体的memslot的设置在kvm_set_memslot函数中完成,slot的操做流程以下:

  • 首先分配一个新的memslots,并将原来的memslots内容复制到新的memslots中;
  • 若是针对slot的操做是删除或者移动,首先根据旧的slot id号从memslots中找到原来的slot,将该slot设置成不可用状态,再将memslots安装回去。这个安装的意思,就是RCU的assignment操做,不理解这个的,建议去看看以前的RCU系列文章。因为slot不可用了,须要解除stage2的映射;
  • kvm_arch_prepare_memory_region函数,用于处理新的slot可能跨越多个用户进程VMA区域的问题,若是为设备区域,还须要将该区域映射到Guest IPA中;
  • update_memslots用于更新整个memslotsmemslots基于PFN来进行排序的,添加、删除、移动等操做都是基于这个条件。因为都是有序的,所以能够选择二分法来进行查找操做;
  • 将添加新的slot后的memslots安装回KVM中;
  • kvfree用于将原来的memslots释放掉;

2.2.2 kvm_delete_memslot

kvm_delete_memslot函数,实际就是调用的kvm_set_memslot函数,只是slot的操做设置成KVM_MR_DELETE而已,再也不赘述。

3. HVA->HPA

光有了GPA->HVA,彷佛仍是跟Hypervisor没有太大关系,究竟是怎么去访问物理内存的呢?貌似也没有看到去创建页表映射啊?
跟我走吧,带着问题出发!

以前内存管理相关文章中提到过,用户态程序中分配虚拟地址vma后,实际与物理内存的映射是在page fault时进行的。那么一样的道理,咱们能够顺着这个思路去查找是否HVA->HPA的映射也是在异常处理的过程当中建立的?答案是显然的。

回顾一下前文《Linux虚拟化KVM-Qemu分析(四)之CPU虚拟化(2)》的一张图片:

  • 当用户态触发kvm_arch_vcpu_ioctl_run时,会让Guest OS去跑在Hypervisor上,当Guest OS中出现异常退出到Host时,此时handle_exit将对退出的缘由进行处理;

异常处理函数arm_exit_handlers以下,具体调用选择哪一个处理函数,是根据ESR_EL2, Exception Syndrome Register(EL2)中的值来肯定的。

static exit_handle_fn arm_exit_handlers[] = {
	[0 ... ESR_ELx_EC_MAX]	= kvm_handle_unknown_ec,
	[ESR_ELx_EC_WFx]	= kvm_handle_wfx,
	[ESR_ELx_EC_CP15_32]	= kvm_handle_cp15_32,
	[ESR_ELx_EC_CP15_64]	= kvm_handle_cp15_64,
	[ESR_ELx_EC_CP14_MR]	= kvm_handle_cp14_32,
	[ESR_ELx_EC_CP14_LS]	= kvm_handle_cp14_load_store,
	[ESR_ELx_EC_CP14_64]	= kvm_handle_cp14_64,
	[ESR_ELx_EC_HVC32]	= handle_hvc,
	[ESR_ELx_EC_SMC32]	= handle_smc,
	[ESR_ELx_EC_HVC64]	= handle_hvc,
	[ESR_ELx_EC_SMC64]	= handle_smc,
	[ESR_ELx_EC_SYS64]	= kvm_handle_sys_reg,
	[ESR_ELx_EC_SVE]	= handle_sve,
	[ESR_ELx_EC_IABT_LOW]	= kvm_handle_guest_abort,
	[ESR_ELx_EC_DABT_LOW]	= kvm_handle_guest_abort,
	[ESR_ELx_EC_SOFTSTP_LOW]= kvm_handle_guest_debug,
	[ESR_ELx_EC_WATCHPT_LOW]= kvm_handle_guest_debug,
	[ESR_ELx_EC_BREAKPT_LOW]= kvm_handle_guest_debug,
	[ESR_ELx_EC_BKPT32]	= kvm_handle_guest_debug,
	[ESR_ELx_EC_BRK64]	= kvm_handle_guest_debug,
	[ESR_ELx_EC_FP_ASIMD]	= handle_no_fpsimd,
	[ESR_ELx_EC_PAC]	= kvm_handle_ptrauth,
};

用你那双水汪汪的大眼睛扫描一下这个函数表,发现ESR_ELx_EC_DABT_LOWESR_ELx_EC_IABT_LOW两个异常,这不就是指令异常和数据异常吗,咱们大胆的猜想,HVA->HPA映射的创建就在kvm_handle_guest_abort函数中。

3.1 kvm_handle_guest_abort

先来补充点知识点,能够更方便的理解接下里的内容:

  1. Guest OS在执行到敏感指令时,产生EL2异常,CPU切换模式并跳转到EL2el1_syncarch/arm64/kvm/hyp/entry-hyp.S)异常入口;
  2. CPU的ESR_EL2寄存器记录了异常产生的缘由;
  3. Guest退出到kvm后,kvm根据异常产生的缘由进行对应的处理。

简要看一下ESR_EL2寄存器:

  • EC:Exception class,异常类,用于标识异常的缘由;
  • ISS:Instruction Specific Syndrome,ISS域定义了更详细的异常细节;
  • kvm_handle_guest_abort函数中,多处须要对异常进行判断处理;

kvm_handle_guest_abort函数,处理地址访问异常,能够分为两类:

  1. 常规内存访问异常,包括未创建页表映射、读写权限等;
  2. IO内存访问异常,IO的模拟一般须要Qemu来进行模拟;

先看一下kvm_handle_guest_abort函数的注释吧:

/**
 * kvm_handle_guest_abort - handles all 2nd stage aborts
 *
 * Any abort that gets to the host is almost guaranteed to be caused by a
 * missing second stage translation table entry, which can mean that either the
 * guest simply needs more memory and we must allocate an appropriate page or it
 * can mean that the guest tried to access I/O memory, which is emulated by user
 * space. The distinction is based on the IPA causing the fault and whether this
 * memory region has been registered as standard RAM by user space.
 */
  • 到达Host的abort都是因为缺少Stage 2页表转换条目致使的,这个多是Guest须要分配更多内存而必须为其分配内存页,或者也多是Guest尝试去访问IO空间,IO操做由用户空间来模拟的。二者的区别是触发异常的IPA地址是否已经在用户空间中注册为标准的RAM;

调用流程来了:

  • kvm_vcpu_trap_get_fault_type用于获取ESR_EL2的数据异常和指令异常的fault status code,也就是ESR_EL2的ISS域;
  • kvm_vcpu_get_fault_ipa用于获取触发异常的IPA地址;
  • kvm_vcpu_trap_is_iabt用于获取异常类,也就是ESR_EL2EC,而且判断是否为ESR_ELx_IABT_LOW,也就是指令异常类型;
  • kvm_vcpu_dabt_isextabt用于判断是否为同步外部异常,同步外部异常的状况下,若是支持RAS,Host能处理该异常,不须要将异常注入给Guest;
  • 异常若是不是FSC_FAULTFSC_PERMFSC_ACCESS三种类型的话,直接返回错误;
  • gfn_to_memslotgfn_to_hva_memslot_prot这两个函数,是根据IPA去获取到对应的memslot和HVA地址,这个地方就对应到了上文中第二章节中地址关系的创建了,因为创建了链接关系,即可以经过IPA去找到对应的HVA;
  • 若是注册了RAM,能获取到正确的HVA,若是是IO内存访问,那么HVA将会被设置成KVM_HVA_ERR_BADkvm_is_error_hva或者(write_fault && !writable)表明两种错误:1)指令错误,向Guest注入指令异常;2)IO访问错误,IO访问又存在两种状况:2.1)Cache维护指令,则直接跳过该指令;2.2)正常的IO操做指令,调用io_mem_abort进行IO模拟操做;
  • handle_access_fault用于处理访问权限问题,若是内存页没法访问,则对其权限进行更新;
  • user_mem_abort,用于分配更多的内存,实际上就是完成Stage 2页表映射的创建,根据异常的IPA地址,已经对应的HVA,创建映射,细节的地方就不表了。

前因后果摸清楚了,那就草草收场吧,下回见了。

参考

《Arm Architecture Registers Armv8, for Armv8-A architecture profile》

欢迎关注我的公众号,不按期分享技术文章。

相关文章
相关标签/搜索