前一篇文章,我们介绍了ARM内存的属性,算是一个小小的里程碑点,接下来我们会把注意力重新拉回虚拟化的赛道。我们从[V-05] 虚拟化基础-异常模型(Exception)(AArch64)之后,花了很多笔墨介绍了ARM体系的Cache和内存相关的机制作为我们进一步研究虚拟化的基础知识,今天终于回归到我们的虚拟化主线继续小马奔腾。内存空间的管理也是虚拟化技术体系中的核心课题之一,下面我们就进入内存虚拟化的世界。我们在前序文章[V-00] 虚拟化概论-思想篇 中介绍过,虚拟化的思想的核心要义就是:对底层的硬件资源在时间和空间上做分割,进行二次分配给GuestOS使用。完成这个资源再分配的角色就是Hypervisor,而它对内存资源以及其他设备资源的分配靠的就是对内存空间的精准的管理,如图1-1所示。图1-1 High-Level 虚拟化座舱系统架构
An ordinary OS manages the memory that the applications run in, and the memory where the OS is located. The hypervisor is responsible for memory management for itself and for the guest operating systems it manages. The entire physical memory is at the direct disposal of the hypervisor. The MMU in EL2 is used by the hypervisor to translate the virtual addresses that the hypervisor uses to address physical memory.
通过手册中的描述并结合图1-1,我们可以总结几点如下:(1) 首先要搞清楚,内存资源其实是内存空间资源,覆盖设备类型(IO)和Normal类型(SDRAM)。• 第一级:从App的视角向底层看去,它要使用内存资源需要受到当前运行的操作系统的控制,比如Linux系统中的Kernel。• 第二级:在虚拟化架构下光有第一级控制是不行的,因为内存资源是各个VM共享的,因此需要Hypervisor介入进行二次控制并分配。(3) 在虚拟化的架构下,ARM体系还是要依赖MMU完成VA到PA的翻译过程,那么MMU工作时依赖的VMSA所有机制都需要参与配合。(4) Hypervisor也只能管控分配给它的内存空间,对于其他空间比如TEE-OS所在Secure空间,它说了不算。上一节中我们已经搞清楚了在虚拟化软件框架下内存资源需要经过两级管控和分配才能最后交到使用者APP的手里(当然操作系统本身需要内存空间来存放操作指令和数据),这个就是内存资源虚拟化的目的。那么为了达到上述目的,在软件(Hypervisor、GuestOS)和硬件MMU的密切配合下完成整个VA到PA映射的过程就是内存虚拟化的核心原理。在我们继续讨论之前,如果读者对ARM的MMU工作原理和异常模型不了解,强烈建议先阅读以下两篇前序文章:MMU的功能就是帮助CPU管理整个存储体系,其中核心就是为PE-Core中issue出来的虚拟地址VA找到对应的物理地址,如图1-2所示。图1-2 MMU工作基本流程
在ARM架构中,异常处理机制是确保系统稳定运行的关键组成部分。当异常发生时,处理器会跳转到相应的异常向量地址,并执行异常处理例程。这允许软件适当地处理异常情况,并恢复系统的正常运行,如图1-3所示。图1-3 MMU工作基本流程
无论是ARM的手册中还是网上的很多大神们写地文章中,要么上来就把Stage-2的框图贴上来,要么就是直接把KVM的代码贴上来,总感觉这样缺少一点循序渐进的耐心。本文打算把这个过程切的更细一些,系统能帮助大家找到内存虚拟化的感觉。阅读过前序文章的同学应该已经清楚了,ARM的内存模型分为两种类型:Device 和 Normal。我们就以此分类,逐个介绍他们被虚拟化的过程。在ARM的VMSA体系下,一个GuestOS的虚拟内存空间布局一般如图1-4所示:图1-4 ARM架构Normal类型内存映射关系
无论是Kernel还是App大家都是工作在虚拟地址空间下的,当需要访问内存的时候,软件给CPU的永远都是一个虚拟地址VA。收到VA后,那么CPU的小弟MMU要做的第一件事儿就是要把VA映射到物理地址空间,并返回相应的PA。如果这个VA所在的区域是第一次被访问,肯定是找不到的,也就是说此时会发生地址翻译错误(Translation fault:The IA does not map onto a TTBR_ELx address range.)。犯错了当然要收到“惩罚”,此时CPU就会中断当前的工作,让GuestOS介入处理这个错误,也就是给这个VA分配一个物理地址空间的区域。GuestOS忙活完之后,整个系统的内存空间视野变成了这个样子,如图1-5所示。图1-5 ARM架构Normal类型内存映射关系(页表)
GuestOS为VA分配完一段内存区域之后还在自己的账本(页表)上做了详细的记录,那么此时似乎我们已经找到了VA对应的物理地址PA可以返回了。实际的情况是不行的,道理很简单,1.1小节中我们已经讨论过了,真正的物理地址是要经过两级分配(两级管控的),到这里拿到的PA还不是真正的CPU能够访问的物理地址,这也是为什么我们在图1-4和图-5上面打了一个问号。GuestOS为VA分配的这个物理地址在VMSA中被称之为IPA。In virtualization, we call the set of translations that are controlled by the OS, Stage 1. The Stage 1 tables translate virtual addresses to intermediate physical addresses (IPAs).
那么这里就有一个问题了,MMU是怎么知道GuestOS的Translation fault Handler返回的PA现在还不能直接返回给PE-Core?其实这是现代处理器中MMU的标配功能了,GuestOS处理完之后会给MMU发信号,MMU会根据相应系统寄存器的状态(EL2层已正确配置)判断此时的EL1/0内存空间工作在虚拟化上下文,那么MMU就要对当前的IPA进行有效性检查。回到我们举例的上下文就不用怀疑了,肯定是无效的,因为还没有给这个IPA分配相应的物理内存,那么此时还是不能够回到原来的代码FLOW的下一条指令继续执行,还是要继续抛出异常告诉系统还没有给VA找到对应的PA。这个异常其实还是Translation fault,只是这一次处理这个“错误”的不是GuestOS了,而是拥有更高权限Hypervisor(EL2)中的Translation fault Handler。Handler拿着MMU通过系统寄存器递过来的IPA,就要为它分配一块真是的物理内存区域了,过程和GuestOS类似,此时的内存视野就变成了如图1-6的样子。图1-6 ARM架构Normal类型内存映射关系(2级页表)
对于Normal类型的IPA在EL2中Hypervisor的帮助下终于分配到了物理内存,当然Hypervisor同样也要一下记录在账本(页表)上记录一笔。此时的MMU在经过内存的其他属性的检查后,就可以在图1-2的流程中交差了,把PA给到PE-Core的LSU模块了。此时,PE-Core就可以放心的在EL0/1访问这个物理地址了。Normal类型的内存虚拟化已经讲完了,那么设备类型的内存虚拟化有何不同吗?还需要单独介绍。其实,这一块单独列出来是因为这一块涉及到设备虚拟化的课题的重要基础,另外确实和Normal类型的内存虚拟化有些区别。对于设备类型的虚拟地址空间的内存来说,从VA到IPA的过程几乎和Normal类型一样,服从GuestOS和MMU的指导就行了。差异化发生在IPA到PA的过程,如图1-7所示:图1-7 ARM架构Device类型内存映射关系(IPA)
IPA过来的设备类型内存地址,到了Hypervisor的异常处理Handler里面处理起来就要变得慎重一些了,不是简单的分配一块物理内存就能解决的,要考虑各方面因素才能进行处理,但是大致就分成两种方式:(1) 分配一个真是的物理设备,此时只要将IPA和真是控制物理设备的PA在页表中对应起来就可以了,下一次GuestOS就可以直接通过PA使用这个物理设备了。比如我们可以吧USB控制器或者SPI控制器直接透传到一个GuestOS,此时这个USB控制器或者SPI控制器就会被这个GuestOS独占。(2) 分配一个虚拟的设备:这个虚拟的设备可以是和别GuestOS共享的一个物理设备,比如声卡或者显卡;也可以是一个完全需求的设备,系统中根本不存在的设备,比如一个UART控制器。要实现这种分配方式,就不能在页表中为对应的IPA分配真实的PA了,而是需要Hypervisor做到截获GusetOS对这个IPA的每次访问,并且通过自己(Hypervisor 模拟处理,EL2)或者路由访问到其他Device(类似QEMU模拟,EL0)进行处理。这里我们举一个手册中例子加以说明,如图1-8所示:• vCPU线程中的代码意图是将VA(virt_uart_rx_reg)所指向的串口控制器中的寄存器的内容读到系统的通用寄存器X0中。• MMU拿到这个VA后很快就在GuestOS的页表中找到了VA对应的IPA,此时MMU经过检查发现这个IPA是无效的地址,向系统抛出异常。• 此时Hypervisor开始介入处理,它拿到MMU在抛异常时在系统寄存器HPFAR_EL2和ESR_EL2中登记的信息,判断出这是个要向X0读入4个字节数据的操作。那么他就模拟Uart设备的操作,最后填写完X0后,返回到vCPU线程继续处理。图1-8 ARM架构Device类型内存映射关系(模拟)
经过上面的分析相信大家对基于ARM架构虚拟化技术中的内存虚拟化原理有了感觉,这里我们还是结合手册给出具体的定义:Starting with the address, Exception Model introduces the FAR_ELx registers. When dealing with stage 1 faults, these registers report the virtual address that triggered the exception. A virtual address is not helpful to a hypervisor, because the hypervisor would not usually know how the Guest OS has configured its virtual address space. For stage 2 faults, there is an additional register, HPFAR_EL2, which reports the IPA of the address that aborted. Because the IPA space is controlled by the hypervisor, it can use this information to determine the register that it needs to emulate.ARM架构下的内存虚拟化原理就是Hypervisor和GuestOS借助ARM的异常处理机制在相应的系统寄存器的配合下,在VMSA体系下完成VA经过IPA再到PA的两级(Stage-1 & Stage-2)地址翻译。本文我们从GuestOS的视角,揭示了内存虚拟化的基本原理,看上去好像也不是很难是不是,哈哈哈。其实,要深刻地理解内存虚拟化的基本原理,除了要具备虚拟化的基础知识外,还需要对ARM的架构体系特别是VMSA的知识体系有深刻理解才可以。下一篇文章,我们会再深入一些细节对内存虚拟化过程中涉及到一些细节做进一步的探讨,为大家后续理解设备虚拟化以及Hypervisor的详细结构打下基础。谢谢大家,请保持关注。[00] <aarch64_virtualization_100942_0100_en.pdf>[01] <Armv8-A-virtualization.pdf>[02] <learn_the_architecture_aarch64_virtualization.pdf>[03] <DDI0487K_a_a-profile_architecture_reference_manual.pdf>[04] <DEN0024A_v8_architecture_PG.pdf>[05] <learn_the_architecture_aarch64_memory_model.pdf>[06] <80-V-Kvm-ARM-zh0005_基于armv8的kvm实现分析(五)-内存虚拟化.pdf>[07] <80-V-KVM-k0005_Linux虚拟化KVM-Qemu分析(五)-内存虚拟化.pdf>[08] <79-LX-LK-z0002_奔跑吧Linux内核-V-2-卷1_基础架构.pdf>[09] <armv8_a_address_translation.pdf>
MMU - Memory Management UnitTLB - translation lookaside bufferVIPT - Virtual Index Physical TagVIVT - Virtual Index Virtual TagPIPT - Physical Index Physical TagIPS - Intermediate Physical SpaceIPA - Intermediate Physical AddressVMID - virtual machine identifierTLB - translation lookaside buffer(地址变换高速缓存)VTTBR_EL2 - Virtualization Translation Table Base Registers(ArmV8 寄存器)ASID - Address Space Identifier (ASID)