本文为译文,原文链接如下:http://systems.cs.columbia.edu/files/wpid-asplos2014-kvm.pdf
KVM/ARM是利用LInux内核中现有的基础来实现的,如果重新设计和实现hypervisor复杂的核心功能,这将可能引入一些知名的bug。虽然单独的hypervisor设计方式具有更好的性能和更小的TCB,但是该种方式并不适合ARM架构。ARM硬件在很多方面都比x86更加多样化。不同的设备制造商通常以非标准的方式将硬件组件集成到ARM设备中。ARM硬件缺乏用于硬件探测的功能,例如标准BIOS或APCI总线,并且没有为安装低级软件建立相关机制。但是巨虎所有的ARM平台都支持Linux,通过将KVM/ARM与Linux进行集成,KVM/ARM可自动得进行版本迭代。而XEN则必须积极支持每个安装在XEN hypervisor的平台。例如,对于XEN需要支持的每个新的SoC,开发人员必须在核心Xen hypervisor中实现一个新的串行设备驱动程序。而KVM/ARM得益于其集成在Linux中,其可移植性和硬件支持方面,我们需要解决的一个关键问题是ARM硬件虚拟化扩展是被设计成完全独立于任何标准内核的功能。接下来我们将描述KVM/ARM的新设计,该设计使将有利于将其集成到现有的Linux内核中,同时是其支持硬件虚拟化。
完全以ARM的Hyp模式运行hypervisor是很有吸引力的,因为它是最具有特权的级别。然而,由于KVM/ARM是利用现有的内核为基础,如调度器,在HYP模式下运行KVM/ARM也就意味着Linux内核需要运行在HYP模式中。这样至少有两个问题点。第一,Linux中依赖于底层架构的代码是在内核模式下工作的,并且不能在未进行任何修改的情况下在HYP模式下运行,因为HYP模式与普通内核模式是完全不同的CPU模式。为了能在HYP模式下运行内核所需要的重大更改不太可能被Linux内核社区接受。更重要的是,保持没有HYP模式的硬件的兼容性将Linux作为guest操作系统运行,底层代码将不得不编写成能够在两种模式下运行,该种做法的潜在结果是执行效率变慢且复杂。一个简单的例子,一个页错误处理时需要获取到引起该页错误的虚拟地址。与内核模式相比,在HYP模式下,该地址是被保存在不同的寄存器中。
第二,在HYP模式下运行整个内核会对本机性能产生负面影响。例如,HYP模式有自己独立的地址空间。内核模式使用两个页表基寄存器来提供用户地址空间和内核地址空间,其将两个分为3GB/1GB进行分割,而HYP模式使用单个页表寄存器,因此不能直接访问地址的用户空间部分。当经常调用函数来访问用户内存时将会需要内核显式地将用户空间数据映射到内核地址空间,然后执行必要的拆卸和TLB的维护操作,从而导致ARM上的性能很差。
使用HYP模式运行Linux内核进行虚拟化时存在的这些问题在x86架构的虚拟化中并不存在。x86的root模式与其CPU特权模式正交。整个Linux内核可以作为系统管理程序在root模式下运行,因为相同的资源在non-root模式下和root模式下都可用。然而,考虑到ARM的广泛使用和LInux在ARM方面的优势,为ARM中一个有效的虚拟化解决方案至关重要,它可利用LInux并利用其虚拟化的硬件支持。
KVM/ARM引入了分裂模式虚拟化,这是一种新的hypervisor的设计,它将hypervisor的核心进行分割,以便它能跨不同的CPU特权模式运行,从而利用每个CPU模式体的特定优点和功能。KVM/ARM使用分裂模式来实现虚拟化,利用HYP模式启动ARM的硬件虚拟化支持,同事利用以内核模式运行的现有服务。在无需对现有代码库进行大规模修改的前提下,分裂模式虚拟化将KVM/ARM与Linux进行集成。
如图2所示,其是通过将hypervisor分解为两个组件(lowvisor和highvisor)来实现的。
lowvisor利用HYP模式下硬件虚拟化的支持来提供三个关键功能。第一,lowvisor通过适当的硬件配置,设置正确的执行上下文,并在不同的执行上下文之间提供保护和隔离。lowvisor直接与硬件保护功能交互,因此非常关键,其能保障代码库尽可能小的更改。第二,lowviso负责r从虚拟机执行上下文切换到主机上下文,反之亦然。主机执行上下文用于运行Highvisor和主机LInux内核。我们将执行上下文称为world, 将从一个world切换到另外一个world称为world的切换,因为系统的整个状态都发生了更改。由于lowvisor是在HYP模式下运行的唯一组件,因此只有它才能负责执行world切换所需要的硬件的重新配置。第三,lowvisor提供了一个虚拟化trap处理程序,它将处理必须陷入到HYP模式的中断和异常。lowvisor只执行最小的处理清楚,而大部分的工作将会被推迟到highvisor中来完成。
highvisor作为宿主Linux内核的一部分,其以内核模式运行。因此,它可以直接利用现有的Linux功能,如调度器,并可以使用标准内核软件数据结构和机制来实现其功能,如锁机制和内存分配函数。这使得在highvisor中更容易实现高级功能。例如,虽然lowvisor体用了一个低级的trap处理和两个world的切换功能,但是highvisor将处理虚拟主机的二级页错误并执行指令模拟。注意,部分虚拟机运行在内核模式下,就像highvisor那样,但是其支持二级转换和支持HYP模式。
由于hypervisor是跨内核模式和HYP模式划分的,所有在虚拟机和highvisor之间切换将涉及到多个模式的额转换。在运行虚拟机的过程中,highvisor所触发的trap首先会进入到HYP模式中的lowvisor。lowvisor将导致另外一个trap来运行highvisor。类似地,从highvisor到虚拟机需要从内核模式trap到HYP模式,然后切换回虚拟机。因此,从Highvisor中切出或者切入highvisor时,分裂模式虚拟化将需要两次trap操作。在ARM平台中,从HYP模式中切入或者切出的唯一方式就是trap,然而如第五章所说,这个额外的trap将会是一个显著的性能开销。
KVM/ARM使用内存映射接口在highvisor和lowvisor之间根据需要进行共享数据。有内存管理可能很复杂,我们在Highvisor的功能里使用Linux中现有的内存管理子系统来管理highvisor和lowvisor之间的内存。不过,管理lowvisor的内存需要面临额外的挑战,因为它需要管理HYP模式的独立地址空间。一种简单的方法就是重用主机内核的页表,同事在HYP模式下使用它们,这样就能保持地址空间的一致性。不幸的是,这种想法无法实现,因为HYP模式使用了与内核模式不同的页表格式。因此,highvisor显式地管理HYP模式的页表,将在HYP模式下执行的任何代码以及在highvisor和lowvisor之间共享的任何数据结构映射到HYP模式和内核模式相同的虚拟地址空间中。
为了实现CPU的虚拟化,KVM/ARM必须向虚拟机提供一个接口,该接口本质上与底层的实际硬件CPU相同,同时确保hypervisor具有硬件的控制权。这需要确保在虚拟机中运行的软件必须具有访问实际物理CPU上寄存器状态的能力,以及确保hypervisor及其主机内核相关联的物理硬件状态在运行的虚拟机中是持久的。不影响虚拟机的寄存器状态可通过保存虚拟机状态并在从虚拟机切换到主机时从内存中恢复主机状态来完成上下文的切换,反之亦然。KVM/ARM将对所有其他敏感状态的访问配置为trap到HYP模式。
表一展示了以内核和用户模式运行的软件可见的CPU寄存器状态以及每个寄存器组的KVM/ARM虚拟化方法。lovvisor有自己专用的配置寄存器,仅能被HYP模式使用,其在表一种病没有显示。在硬件支持的情况下,world切换时,KVM/ARM上下文也会被切换,因为它需要让虚拟机直接访问硬件。例如,虚拟机在未陷入到hypversor的情况下可直接对一级页表基寄存器进行编程,这是多数guest操作系统中相当常见的操作。KVM/ARM对敏感指令执行trap和仿真,当访问硬件状态是,其将影响hypervisor或泄漏硬件的相关信息,这将违反虚拟化抽象层的理念。例如,如果虚拟机执行WFI指令,KVM/ARM就会trap,这回导致CPU断点,因为这样的操作应该只由hypervisor来执行和维护。在KVM/ARM中将某些待定寄存器的状态延迟到必须时间进行处理,这样在某些工作负载下略微提高了性能。
在内核或用户模式下运行虚拟机与在内核或用户模式下运行hypervisor之间的区别取决于在world切换期间HYP模式如何配置虚拟化扩展。从主机到虚拟机的world切换执行以下操作:
1. 将所有主机GP寄存器存储在HYP的堆栈上
2. 为虚拟机配置VGIC
3. 配置虚拟机的时钟
4. 将所有主机待定的配置寄存器保存到HYP堆栈中
5. 将虚拟机的配置寄存器加载到硬件上,这可以在不影响当前执行的情况下完成,因为HYP模式使用独立于主机状态的自己的配置寄存器
6. 配置HYP模式来捕获浮点操作,用于延迟上下文的切换、捕获中断、捕获CPU停止指令(WFI/WFE)、捕获SMC指令、陷入特定的配置寄存器访问、陷入调试寄存器访问
7. 将特定的虚拟机ID写入影子ID寄存器
8. 设置二级页表的基寄存器(VTTBR),并启动二级地址转换
9. 恢复所有guest GP寄存器,并陷入用户或内核模式
CPU将停留在虚拟机的world,直到事件发生,触发trap进入HYP模式。这种事件可以由上面提到的任何trap、二级页错误或硬件中断引起。由于事件需要来自highvisor的服务,为了模拟虚拟机的预期硬件行为或服务设备中断,KVM/ARM必须执行另一个切换回highvisor及其主机的world。从虚拟机切换回主机的world执行以下操作:
1. 存储所有虚拟机GP寄存器
2. 禁止二级地址转换
3. 配置HYP模式使其能不捕获任何寄存器访问或指令
4. 保存所有虚拟机特定的配置寄存器
5. 将主机的配置寄存器加载到硬件上
6. 配置主机的时钟
7. 保存虚拟机特定的VGIC的状态
8. 恢复所有主机GP寄存器
9. 陷入到内核模式
KVM/ARM通过在虚拟机中运行时为所有内存访问使能二级地址转换来提供内存虚拟化。二级地址转换只能在HYP模式下进行配置,它的使用对虚拟机时透明的。highvisor管理二级转换页表,只允许访问专门为一个虚拟机配置的内存;其他的访问操作将导致二级页错误,该错误将被hypervisor捕获。这种机制确保一个虚拟机不能访问属于hypervisor或其他虚拟机的内存和任何敏感数据。在highvisor和lowviser中运行时需要禁用二级地址转换,因为highvisor完全控制整个系统,并直接管理主机的物理地址。当hypervisor执行world切换,使系统进入到虚拟机时,它将启用二级地址转换,并相应地配置二级页表基寄存器。虽然highvisor和所有虚拟机共享相同的CPU模式,但是二级地址转换确保highvisor不接受虚拟机的任何访问。
KVM/ARM使用分裂模式虚拟化,利用现有的内核内存分配、页引用计数和页表操作的代码。KVM/ARM通过判定IPA的错误来处理二级页错误,如果出错的地址属于虚拟机内存映射中的正常内存,KVM/ARM只需调用现有的内核函数,如get_user_pages来为虚拟机分配一页,同时将该分配的页映射到虚拟机的二级页表中。而对于单纯的hypervisor方案,其将被迫向虚拟机实现静态分配内存,或者编写一个全兴的内存分配子系统。
KVM/ARM利用现有的QEMU和Virtio用户空间设备模拟来提供I/O虚拟化,在硬件层面,ARM架构上的所有I/O机制都是通过加载/存储操作MMIO设备区域来实现的。除了直接分配给某个虚拟机的设备外,所有其他虚拟机都无法访问MMIO区域。KVM/ARM使用二级地址转换来确保虚拟机不能直接访问物理设备。某个虚拟机访问任何不属于其的内存区域的行为都会被捕获得到hypervisor,hypervisor会根据故障地址将刚问转发到QEMU中的特定模拟设备。这与x86有些不同,x86使用特定的x86硬件指令,例如inl和outl来操作I/O端口而不是MMIO。正如我们在第五章所介绍,KVM/ARM实现是其能使用很少的I/O开销来完成虚拟化的工作量。
KVM/ARM利用与LInux的紧密集成重用了现有设备驱动和相关功能,包括中断的处理。在虚拟机中运行时,KVM/ARM将CPU配置为将所有硬件终端捕获到HYP模式。对于每个中断,其将陷入到highvisor中,由主机来处理中断,这样hypervisor仍然完全控制硬件资源。在这两种情况下,所有硬件中断处理都是通过重用主机中的现有的LInux中断处理功能来完成。然而,虚拟机必须从模拟设备中接收虚拟中断形式的通知,多核guest操作系统必须能够将一个虚拟的IPI从一个核发送给另外一个核。KVM/ARM使用VGIC将虚拟中断注入虚拟机,以减少陷阱HYP模式的次数。如第二章所描述的,虚拟中断通过编程存在于hypervisor CPU控制接口的VGIC中的list寄存器将虚拟中断提交给虚拟CPU。KVM/ARM对二级页表进行配置,以防止虚拟机访问控制接口,并且只允许访问VGIC虚拟CPU接口,这样可以确保只有hypervisor可以对控制接口进行编程,虚拟机可直接访问VGIC虚拟CPU接口。但是,guest操作系统仍然会尝试访问GIC分发器来配置GIC,并将IPIs从一个虚拟核发送到另外一个虚拟核。这种访问将被捕获到hypervisor,并且hypervisor必须模拟分发服务器。
KVM/ARM引入了虚拟分发器,这是作为highvisor的一部分的GIC分发器的软件模型。虚拟分发服务器会向用户空间暴露一个接口,因此用户空间中的模拟设备可以向虚拟分发服务器发出虚拟中断,并向虚拟机暴露一个与物理GIC分发服务器相同的MMIO接口。虚拟分发服务器将保持关于每个中断的状态的内部软件状态,并在调度虚拟机时使用该状态,以便对list寄存器进行编程来注入虚拟中断。例如,如果虚拟CPU0向虚拟CPU1发送一个IPI,分发服务器将为虚拟CPU1的list寄存器编写程序,以便在下一次虚拟CPU1运行时引发一个虚拟IPI中断。
理想情况下,虚拟分发服务器只在必要时访问硬件list寄存器,因为设备MMIO操作通常比缓存的内存访问慢得多。当将一个虚拟机调度到一个物理核上运行时,必须切换list寄存器的上下文,但是在虚拟机和hypervisor之间进行切换时,List寄存器的上下文则不需切换。例如,如果没有一个中断没有挂起,那么就没有逼样访问任何list寄存器。注意,一旦系统hypervisor在切换到虚拟机时向list寄存器写入了虚拟中断,那么当切换回hypervisor时,它也必须重新读取list寄存器。因为list寄存器描述了虚拟中断的状态,例如,是否虚拟机ACK了虚拟中断。KVM/ARM的初始未优化版本使用了一种简化的方法,它完全切换所有VGIC状态,包括每个world交换时的List寄存器。
读取计数器和编程计时器是许多操作系统中用于进程调度和定期轮询设备状态的常见操作。例如,Linux读取计数器以确定进程的时间片是否已过期,并编写计时器以确保进程不超过其允许的时间片。出于各种原因,应用程序工作负载也经常使用计时器。如果将每一个这样的操作都捕获到hypervisor中可能会导致明显的性能开销,并且运行虚拟机直接访问计时器硬件通常意味着放弃对硬件资源的计时控制,因为虚拟机可禁用计时器并在较长时间内控制CPU。
KVM/ARM利用ARM的对通用计时器的硬件虚拟化特性,允许虚拟机在不需陷入HYP模式的情况下直接访问计数器和编程计时器,同时确保hypervisor仍然控制硬件。由于对物理计时器的访问是使用HYP模式控制的,所以任何控制HYP模式的软件都可以访问物理计时器。KVM/ARM通过在hypervisor中使用物理计时器并禁止从虚拟机访问物理计时器来维护对硬件的控制。作为guest操作系统运行的Linux内核只访问虚拟计时器,因此其可在不需陷入hypervisor的情况下直接访问计时器硬件。
不幸的是,由于体系架构的限制,虚拟计时器不能直接引发虚拟中断,而总是死会引发硬件中断,这就会使CPU陷入hypervisor。KVM/ARM检测虚拟机的虚拟计时器何时到期,并向虚拟机注入相应的虚拟中断,并在highvisor中执行所有的硬件ACK和EIO操作。硬件仅为每个物理CPU提供一个虚拟计时器,多个虚拟CPU可以跨这个硬件实例进行多路复用。为了在此场景中支持虚拟计时器,如果虚拟机一直运行的话,KVM/ARM在虚拟机捕获到hypervisor时检测未过期的计时器,并利用现有的操作系统功能,在虚拟机计时器启动时编写一个软件计时器。当这样一个软件计时器触发时,将执行一个回调函数,该函数使用上面描述的虚拟分发服务器想虚拟机发出一个虚拟计时器中断。