1. 前言
ARM架构为了支持虚拟化做了些扩展,称为虚拟化扩展(Virtualization Extensions)。原先为VT-x创建的KVM(Linux-based Kernel Virtual Machine)适配了ARM体系结构,引入了KVM/ARM (the Linux ARM hypervisor)。KVM/ARM没有在hypervisor中引入复杂的核心功能,而是利用了Linux内核中现有的功能进行适配。KVM/ARM与Linux集成,在可移植性和硬件支持方面会有所收益。其实也可以开发独立的bare metal hypervisor,这样可能会有更好地性能,但这种方法在ARM上不太好使。
在下面,我们将描述KVM/ARM的设计如何使得它可以从现有kernel的集成中获益,同时利用硬件虚拟化特性。
2. Split-mode虚拟化
KVM/ARM利用了现有的kernel功能,例如调度器(scheduler),但KVM/ARM如果直接在EL2中运行Linux kernel,会存在两个问题。
第一,linux中与底层体系结构相关的代码是为在kernel模式下工作而编写的,不经过修改就不能在EL2中运行,因为EL2是一个完全不同于普通kernel模式的CPU模式。Linux kernel社区不太可能接受在EL2中运行kernel所需的重大更改。更重要的是,为了保持与没有EL2的硬件的兼容性并将Linux作为Guest OS运行,必须编写低级代码以在两种模式下工作,这可能会导致缓慢而复杂的代码路径。举个简单的例子,页表错误处理程序需要获取导致页表错误的虚拟地址。在EL2中,这个地址存储在与kernel模式不同的寄存器中。
第二,在EL2中运行整个内核会对本机性能产生不利影响。例如,EL2有自己独立的地址空间,但是kernel模式使用两个页表基本寄存器(TTBR)在user地址空间和kernel地址空间之间提供熟悉的3GB/1GB分割,而EL2使用单个页表寄存器,因此不能直接访问地址空间的user空间部分。经常使用的访问user内存的函数需要kernel显式地将用户空间数据映射到kernel地址空间,然后执行必要的拆解和TLB维护操作,从而导致ARM的性能不好。
综上所述,KVM/ARM引入了split-mode虚拟化,这种一种新的hypervisor设计方法,它将核心hypervisor分开,以便它可以跨不同的特权CPU模式运行,从而利用每种CPU模式提供的特定优势和功能。KVM/ARM使用split-mode虚拟化来利用EL2支持的ARM硬件虚拟化,同时利用在kernel模式下运行的现有Linux kernel服务。Split-mode虚拟化允许KVM/ARM与Linux kernel集成,而无需对现有代码库进行重大修改。
这是通过将hypervisor分成两个组件来实现的:lowvisor和highvisor,如图1所示。
图1 KVM/ARM系统架构
Lowvisor的设计是利用EL2中提供的硬件虚拟化支持来提供三个关键功能。首先,lowvisor通过适当的硬件配置来设置正确的执行上下文,并在不同的执行上下文之间实施保护和隔离。Lowvisor直接与硬件保护功能交互,因此非常关键,且代码库需要保持绝对最小。其次,lowvisor从VM执行上下文切到到host执行上下文,反之亦然。Host执行上下文用于运行hypervisor和host Linux kernel。我们将执行上下文称为一个世界,将从一个世界切换到另一个世界称为世界切换(world switch),因为系统的整个状态都发生了变化。由于lowvisor是唯一在EL2中运行的组件,因此只有它可以负责执行世界切换所需的硬件重新配置。第三,lowvisor提供了一个虚拟化Trap处理程序,它处理必须Trap到hypervisor的中断和异常。所有发送到hypervisor的Trap必须首先发送到lowvisor。在世界切换到highvisor完成后,lowvisor只执行所需的最少量的处理,并将要完成的大部分工作推迟到highvisor。
Highvisor作为host linux kernel的一部分在kernel模式下运行。因此,它可以直接利用现有的linux功能,例如调度器,并且可以利用标准的kernel软件数据结构和机制来实现其功能,例如locking机制和memory分配功能。它使高层功能更容易在highvisor中实现。例如,虽然lowvisor提供低级Trap处理程序和低级机制来从一个世界切换到另一个世界,但highvisor处理来自VM和的stage-2页表错误并执行指令仿真。请注意,部分VM以kernel模式运行,就像highvisor一样,但是启用了stage-2转换和Trap到EL2。
因为hypervisor在kernel模式和EL2之间是分开的,所以在VM和highvisor之间的切换涉及到多个模式转换。在运行VM时,向highvisor发出Trap将首先向EL2中lowvisor发出Trap。然后lowvisor将发起另一个Trap去运行highvisor。类似地,从highvisor切换到VM需要从kernel模式切换到EL2,然后切换到VM。因此,在切换到或切换出highvisor时,split-mode虚拟化会产生双重Trap成本。在ARM上,执行这些模式转换到EL2或从EL2转换的唯一方法就是通过Trap。不过,这个额外的Trap在ARM上并不是一个显著的性能成本。
KVM/ARM使用内存映射接口根据需要在highvisor和lowvisor之间共享数据。由于内存管理可能很复杂,它利用了linux中现有内存管理子系统的功能来管理highvisor和lowvisor的内存。但是管理lowvisor的内存涉及到额外的挑战,它需要管理EL2的单独地址空间。一种简单的方法是重用host kernel的页表,并在EL2中使用它们来使地址空间相同。不过这样有问题,因为EL2使用和kernel模式不同的页表格式。因此,highvisor显式地管理EL2页表,将在EL2种执行的任何代码以及highvisor和lowvisor之间共享的任何数据结构映射到EL2和kernel模式中的相同虚拟地址。
3. CPU虚拟化
为了虚拟化CPU,KVM/ARM必须向VM提供一个接口,该接口本质上与底层的实际硬件相同,同时确保hypervisor仍然控制硬件。它包括确保在虚拟机中运行的软件必须与在物理CPU上运行的软件具有对相同寄存器状态的一致访问,以及确保切换VM运行中,与hypervisor及其host kernel相关联的物理硬件状态是一致的。为了不影响VM隔离,寄存器状态可以简单地通过保存VM状态和从VM切换到host时从内存恢复主机状态进行上下文切换,反之亦然。KVM/ARM将对所有其它敏感状态的访问配置为Trap到EL2,因此hypervisor可以模拟它。
表1为在kernel模式和user模式下运行的软件可以看到的寄存器状态,以及KVM/ARM对每个寄存器组的虚拟化方法。Lowvisor有自己专用的配置寄存器,仅供EL2使用。只要硬件支持,在世界切换期间,KVM/ARM上下文会切换寄存器,因为它允许VM直接访问硬件。例如,VM可以直接对stage-1页表基本寄存器进行编程,而不需要Trap到hypervisor,这在大多数Guest OS中是相当常见的操作。KVM/ARM在敏感指令和访问硬件状态时执行Trap和模拟,这些状态可能会影响hypervisor或将有关硬件的信息泄漏给VM,违反了虚拟化原理。例如。如果VM执行导致CPU断电的WFI指令,KVM/ARM会Trap,这样的操作应该只由hypervisor执行,以保持对硬件的控制。
表1 VM和Host的状态
在kernel或user模式下运行虚拟机与在kernel或user模式下运行hypervisor之间的区别取决于EL2在世界切换期间如何配置virtualization extensions。从host切换到VM的过程如下:
1.将所有host GP寄存器存到EL2的堆栈中;
2.给VM配置vGIC;
3.给VM配置timers;
4.将所有host特定的配置寄存器存到EL2堆栈中;
5.将VM的配置寄存器加载到硬件,这个操作不会影响当前执行,因为EL2使用它自己的配置寄存器,这些寄存器从host状态中分离出来。
6.配置EL2以Trap浮点操作(VFP寄存器的lazy context switching),trap中断,trap CPU halt指令(WFI/WFE),trap SMC指令,Trap特定配置寄存器访问和trao调试寄存器访问;
7.将虚拟机特定的ID写入shadow ID寄存器中,这由ARM虚拟化扩展定义,并由虚拟机访问,以代替ID寄存器中的硬件值;
8.设置stage-2页表基寄存器(VTTBR)并启用stage-2地址转换;
9.恢复所有guest GP寄存器;
10.进入user模式或kernel模式;
CPU将留在虚拟机世界,直到有event发生,触发进入EL2的trap。这样的事件可能由上面提到的任何Trap、stage-2页表错误或硬件中断引起。由于event需要highvisor的服务,要么模拟虚拟机预期的硬件行为,要么服务于设备中断,KVM/ARM必须执行另一个世界切换到highvisor和它的host。这需要先到lowvisor,然后再到highvisor。从VM世界切换到host世界需要以下步骤:
1.保存所有VM GP寄存器;
2.关闭stage-2转换;
3.配置EL2不Trap任何寄存器访问和指令;
4.保存所有VM特定的配置寄存器;
5.加载host的配置寄存器到硬件;
6.配置host的timers;
7.保存VM特定的vGIC状态;
8.恢复所有host GP寄存器
9.进入kernel模式;
4. 内存虚拟化
如图2所示,ARM提供了stage-2页表来将Guest物理地址转换为host物理地址。KVM/ARM通过启用stage-2转换,为在虚拟机中运行时为所有内存访问提供内存虚拟化。Stage-2转换只能在EL2中配置,它的使用对VM是完全透明的。Highvisor管理stage-2翻译页表,只允许访问专门为VM分配的内存。其它访问将导致stage-2页表错误,从而Trap到hypervisor。该机制确保虚拟机不能访问hypervisor或其它虚拟机的内存,包括任何敏感数据。在highvisor和lowvisor中运行时禁用stage-2转换,因为highvisor完全控制整个系统并直接管理host物理地址。当hypervisor切换到VM时,它启用stage-2转换并相应地配置stage-2页表基寄存器。尽管highvisor和VM共享相同的CPU模式,但stage-2转换确保highvisor不受VM的任何访问。
图2 ARM的两级页表
KVM/ARM是一个split-mode虚拟化来利用现有的kernel内存分配、页表引用计数和页表操作代码。KVM/ARM通过考虑出错的gPA来处理stage-2页表出错,以及该地址是否属于VM内存映射,KVM/ARM通过简单地调用一个现有的kernel函数(如get_user_pages)为VM分配一个页表,并将分配的页表映射到stage-2页表中的VM。
5. I/O虚拟化
KVM/ARM利用现有的QEMU和Virtio user空间设备模拟来提供I/O虚拟化。在硬件层面上,ARM架构上的所有I/O机制都是基于对MMIO设备区域的load/store操作。除了直接分配给VM的设备,所有硬件MMIO区域不能被虚拟机访问。KVM/ARM使用stage-2转换来确保不能从虚拟机直接访问物理设备。在为VM分配的RAM区域之外的任何访问都将被hypervisor Trap,hypervisor可以根据fault地址将访问路由到QEMU中的特定模拟设备。
6. 中断虚拟化
KVM/ARM利用其与Linux的集成来重用现有的设备驱动程序和相关功能,包括处理中断。当在虚拟机中运行时,KVM/ARM配置CPU将所有硬件中断Trap到EL2。在每个中断中,它执行到hypervisor的世界切换,host处理中断,这样hypervisor就可以保留完全控制硬件资源。当在host和highvisor中运行时,中断直接Trap到kernel模式,避免通过EL2的开销。在这两种情况下,所有硬件中断处理都是通过重用Linux现有的中断处理功能在host中完成的。
但是,虚拟机必须以虚拟中断的形式接收来自模拟设备的通知,multicore Guest OSs必须能够从一个虚拟core发送虚拟IPIs到另一个虚拟xore。KVM/ARM通过vGIC向虚拟机注入虚拟中断,减少EL2的Trap数量。通过编程vGIC hypervisor CPU控制接口中的list registers,虚拟中断被提升到虚拟CPU。KVM/ARM配置stage-2页表,来防止虚拟机访问控制接口,只允许访问vGIC虚拟CPU接口,保证只有hypervisor可以编程控制接口,虚拟机可以直接访问vGIC虚拟接口。然而,Guest OS仍然会尝试访问GIC分发器来配置GIC,并讲IPI从一个虚拟core发送到另一个虚拟core。这样的访问将Trap到hypervisor,hypervisor必须模拟分发程序。
KVM/ARM引入了虚拟分发器,这是GIC分发器的一个软件模型,作为highvisor的一部分。虚拟分发器向User空间公开接口,因此user空间中的模拟设备可以向虚拟分发器发起虚拟中断,并向VM公开与物理GIC分发器相同的MMIO接口。虚拟分发器保存关于每个中断状态的内部软件状态,并在调度VM时使用此状态,编程list registers以注入虚拟中断。例如,如果虚拟CPU0向虚拟CPU1发送一个IPI,分发程序将编程虚拟CPU1的list registers以引发一个虚拟IPI中断给虚拟CPU1。
理想情况下,虚拟分发程序只在必要时访问硬件list registers,因为设备MMIO操作通常比cache内存访问慢得多。当调度不同的VM在物理core上运行时,需要list registers的完整上下文切换,但当简单地在VM和hypervisor之间切换时,则不一定需要上下文切换。例如,如果没有挂起的虚拟中断,则不需要访问任何list register。注意,一旦hypervisor在切换到VM时将一个虚拟中断写入一个list register,那么在切换会hypervisor时,它还必须读回这个list register,因为list register描述了虚拟中断的状态。KVM/ARM的初始未优化版本使用了一种简单的方法,该方法完全切换所有vGIC状态,包括每个世界开关上的list register。
7. 计时器虚拟化
读取计数器和编程计时器是许多操作系统中用于进程调度和定期轮询设备状态的常见操作。例如,Linux读取一个计数器来确定进程是否已经过期,并编程计数器来确保进程没有超出其允许的时间片段。由于各种原因,应用程序工作负载也经常使用计时器。为每个这样的操作Trap到hypervisor可能会导致明显的性能开销,并且允许VM直接访问计时硬件通常意味着放弃对硬件资源的定时控制,因为VM可以禁用计时器并在较长的时间内控制CPU。
KVM/ARM利用ARM的通用定时器的硬件虚拟化特性来允许虚拟机直接访问可读计数器和编程计时器,而不会引起Trap到EL2,同时确保hypervisor仍然控制硬件。由于使用EL2控制对物理计时器的访问,因此任何控制EL2模式的软件都可以访问物理计时器。KVM/ARM通过使用hypervisor中的物理计时器和不允许从虚拟机访问物理计时器来维护硬件控制。作为Guest OS运行的Linux kernel只能访问虚拟计时器,因此可以直接访问计时器硬件,而不会被hypervisor Trap住。
不过由于体系结构的限制,虚拟计时器不能直接引发虚拟中断,而总是引发硬件中断,而硬件中断会Trap到hypervisor。KVM/ARM检测虚拟机的虚拟计时器是否到期,并向虚拟机注入相应的虚拟中断,在highvisor中执行所有硬件ACK和EOI操作。硬件只为每个物理CPU提供一个虚拟计时器,多个虚拟CPU可以再这个硬件实例上复用。为了在这种情况下支持虚拟计时器,KVM/ARM在虚拟机进入hypervisor时检测未过期的计时器,并利用现有的OS功能在虚拟计时器本来触发的时候编程一个软件计时器,让虚拟机处于运行状态。当这样的软件计时器触发时,将执行一个回调函数,该函数使用上面描述的虚拟分发器向VM引发一个虚拟计时器中断。
8. ARM体系结构的改进
根据KVM/ARM开发人员的经验,对ARM架构进行了一系列改进,以避免对KVM/ARM等type-2 hypervisor进行split-mode虚拟化的需要。这些改进包括Virtualization Host Extensions(VHE),它现在是ARM 64-bit架构新版本ARMv8.1的一部分。VHE允许将设计在EL1中运行的OS运行在EL2中,而无需对OS源代码进行实质性修改。
VHE是通过添加一个新的控制位E2H来提供的,该位在系统启动时安装使用VHE的type-2 hypervisor是设置的。如果该位没有设置,ARMv8.1在硬件虚拟化方面与ARMv8相同,保留了与现有hypervisor的向后兼容性。如果该位设置了,那么VHE将开启三个主要特性。
首先,VHE扩展了EL2,向CPU添加额外的物理寄存器状态,这样EL1中可用的任何寄存器和功能也可以在EL2中使用。例如,EL1有两个寄存器:TTBR0_EL1和TTBR1_EL1。第一个用于查找页表中较低VA范围内的虚拟地址,第二个用于较高VA范围内的虚拟地址。它提供了一种在user空间和kernel空间之间分隔虚拟空间的方便、高效地方法。但是,如果没有VHE,EL2只有一个页表基寄存器TTBR0_EL2,这使得在EL2中运行时支持EL1的分隔VA空间存在问题。对于VHE,EL2将有第二个页表基寄存器TTBR1_EL2,从而可以支持分隔VA空间EL2的方式与EL1相同。用于启用与host集成的type-2 hypervisor操作系统去支持在EL2种分割VA空间,这是运行EL2种的host OS所必需的,以便管理user空间和kernel之间的VA空间。
其次,VHE提供了一种机制来透明地访问额外的EL2寄存器状态。简单地提供额外的EL2寄存器不足以在EL2中运行未修改的OS,因为现有的操作系统被写入访问EL1寄存器。例如,Linux使用了TTBR1_EL1,不影响EL2种运行的translation系统。提供额外的寄存器TTBR1_EL2仍然需要修改Linux,以便分别在EL2中运行时使用TTBR1_EL2而不是TTBR1_EL1。为了避免迫使OS供应商给软件增加这种额外的复杂性,VHE允许未经修改的软件在EL2种执行,并使用EL1寄存器访问函数指令编码透明的访问EL2寄存器。例如,当前的操作系统软件用指令MRS x1,TTBR1_EL1读取TTBR1_EL1寄存器。对于VHE,软件仍然执行相同的指令,但是硬件实际访问的是TTBR1_EL2寄存器。只要设置了E2H bit,访问EL1寄存器实际上访问了EL2寄存器,从而透明地重写了对EL2的寄存器访问。添加了一组新的特殊指令来访问EL2中EL1寄存器,hypervisor可以使用这些指令在将EL1中运行的VM之间进行切换。例如,如果hypervisor希望访问Guest的TTBR1_EL1,它将使用指令MRS x1,TTBR_EL21。
第三,VHE扩展了EL2的memory translation能力。在ARMv8种,EL2和EL1使用不同的页表格式,因此编写在EL1种运行的软件必须经过修改才能在EL2中运行。在ARMv8.1中,当设置了E2H位时,EL2页表格式现在与EL1格式兼容。因此,以前在EL1中运行的OS现在可以在EL2中运行而无需修改,因为它可以使用相同的EL1页表格式。
图3显示type-1和type-2 hypervisor程序如何映射到具有VHE的体系结构。
图3 虚拟化扩展(VHE)
Type-1 hypervisor(如Xen)可以明确地设计为在EL2中运行,而无序任何额外的支持,它不设置VHE引入的E2H位,并且EL2的行为与ARMv8完全相同。Type-2 hypervisor(如KVM/ARM)在系统启动时设置了E2H位,host OS kernel只在EL2中运行,不会在EL1中运行。Type-2 hypervisor kernel可以在EL2中不加修改地运行,因为VHE为每个EL1寄存器提供了一个等效的EL2寄存器,并透明地将EL1寄存器访问从EL2重写为EL2寄存器访问,而且EL1和EL2之间的页表格式现在是兼容的。从host user空间到host kernel的转换直接从EL0到EL2进行,例如处理系统调用,如图7.4的箭头所示。从VM到hypervisor的转换现在无须上下文切换EL1状态,因为hypervisor不使用EL1。