ARM架构中KVM的支持

3.9版本的Linux内核中增加了KVM对ARM架构的支持,即KVM支持ARM架构的虚拟化。虽然KVM已经支持了i386, x86/64, PowerPc和s390架构,但是KVM对ARM架构的支持不仅仅需要重新实现其他体系架构的需求和样式。因为ARM虚拟化的扩展与其他架构非常不同。

从ARM的发展历史来看,ARM架构是不支持虚拟化的,因为ARM架构中存在许多敏感的指令,其在特权模式下执行时将不会出现陷入。但是在最近的32位ARM处理器中,像cortex-A15,其将对ARM虚拟化的硬件支持作为ARMv7架构的扩展内容。许多研究项目尝试在没有硬件虚拟化支持的情况下在ARM处理器上实现虚拟化,但是需要不同级别的半虚拟化且都不稳定。KVM针对ARM处理器虚拟化的设计能够实现其在虚拟环境中运行未经任何修改的客户操作系统(guest os)。

ARM的硬件扩展与X86的硬件扩展存在很大的不同。内存运行与ARM架构的CPU中的SVC模式,而用户空间则运行于ARM架构的CPU中的USR模式。为了能在ARM架构的CPU中运行hypervisor,ARM引入了一种新的CPU模式---HYP模式。HYP模式是KVM实现对ARM虚拟化设计的核心,它的一个重要特征是HYP模式不是SVC模式的扩展,而是一个独立的运行模式,其具有独立的特性集和独立的虚拟地址转换机制。例如:如果在HYP模式下发生了页错误(page fault),错误的虚拟地址将会被保存在HYP模式下的寄存器中,而不是SVC模式中。另外一个例子是,对于SVC和USR模式,硬件有两个独立的页表基地址寄存器,用于实现用户空间和内核空间的隔离。HYP模式只使用一个页表基地址寄存器,因此也就不存在所谓的用户空间和内核空间之分。

HYP模式的设计非常适合经典的hypervisor设计,因为这样的hypervisor不会重用任何运行在SVC模式下的内核代码。但是,KVM被专门设计成要重用现有内核组件机制,并将它们与hypervisor进行集成。相比之下,X86硬件对虚拟化的支持并没有提供新的CPU模式,而是提供了一个称为"root"和"non-root"的概念。当系统在x86上以non-root用户身份运行时,该特性集完全等同于不支持虚拟化的CPU。当系统在x86上以root身份运行时,该特性集将会被进行扩展,添加了额外用于控制虚拟机(VM)的特性,但是所有现有的内核代码都可以以root用户和non-root用户的身份进行运行。在X86中,当VM捕获到hypervisor操作时,CPU会从non-root转换成root。在ARM架构中,当VM捕获到hypervisor时,CPU将会陷入到HYP模式。

当CPU运行与SVC和USR模式时,通过配置敏感操作让CPU陷入HYP模式来控制ARM虚拟化的特性。在该模式下,其可以配置一些影子寄存器的值,这些值用于想虚拟机隐藏物理硬件的相关信息。HYP模式还控制了地址转换的第二阶段,这一特性类似于Intel用于控制虚拟机内存访问的“扩展页表”。通常,当ARM处理器出触发了装载、存储指令时,指令中使用的内存地址将会由MMU进行地址转换,将虚拟地址转换成物理地址,也即是VA到IPA。虚拟化扩展增加了一个额外的地址转换阶段,称为二级转换,且只能在HYP模式下才能使能或者禁用该阶段的转换。当二级转换被使能时,MMU将会按照如下过程进行地址的转换:

阶段1: VA--->IPA

阶段2: IPA---->PA

guest操作系统独立完成虚拟化中第一阶段的地址转换,且可以更改页表和映射关系,而不用捕获hypervisor。第二阶段的地址转换被hypervisor控制,只有在HYP模式下才能够访问地址二级转换是使用的页表基地址寄存器。第二阶段的地址转换允许运行在HYP模式下的软件以一种完全透明的方式来控制对物理内存的访问,因为虚拟机只能访问被hypervisor从IPA映射到PA的二级页表。

 

KVM/ARM设计

KVM/ARM与Linux内核紧密的集成在一起,有效的将内核当作是ARM管理程序。要让KVM/ARM使用该硬件特性,内核必须能够以某种方式在HYP模式下运行代码,因为HYP模式被用于配置硬件来运行虚拟机,并且从虚拟机到主机之间的切换都是由HYP模式来完成。

重写整个内核,让其仅仅在HYP模式下运行时不可选的,因为这样会破坏不支持虚拟化扩展的硬件的兼容性,而且运行于HYP模式的内核在虚拟机中也是无法正常工作的,因为HYP模式将不可用。同时支持在HYP模式和SVC模式下运行内核页将会对原地啊嘛造成太大的影响,而且会严重影响关键执行路径的效率。此外,HYP模式下硬件要求的页表布局与SVC模式也有所不同,因为他们要求使用LPAC(ARM的大型物理地址扩展),并且需要在页表条目中设定特定的位,而这些位在SVC模式下的内核页表将会被清除。所以KVM/ARM必须管理一组单独的HYP模式页表,并在HYP模式访问的代码和数据中被显式的进行映射。

因此,KVM/ARM必须管理一组单独的HYP模式页表,并对HYP模式访问的代码和数据进行显式映射。因此我们提出了在多个CPU模式之间分割执行,在HYP模式下尽可能少的运行代码。在HYP模式下运行的代码被限制在几百条指令之内,并且被保存在arch/arm/kvm/interrupts.s文件和arch/arm/kvm/interrupts_head.s文件中。

对于对KVM体系结构不熟悉的读者,所有体系架构的KVM都是通过向用户空间暴露一个简单的接口来提供CPU和内存等核心组件的虚拟化。设备的模拟,虚拟机的配置和设置都是由用户空间的一个进程来完成,调性的就是QEMU。当该进程决定运行虚拟机时,其将会调用KVM_VCPU_RUN ioctol()。其将在当前的CPU上执行虚拟机的代码。在ARM架构中,arch/arm/kvm/arm.s文件中的ioctl函数通过发送HVC指令将CPU切换到HYP模式,同时保存主机和客户端之间切换的所有硬件状态的上下文,最后跳转到虚拟机的SVC或者USR模式去执行guest代码。CPU将一直执行guest代码,直到硬件由于中断、二级转换页表错误或者敏感操作而重新陷入到HYP模式。当该情况发生时,KVM/ARM架构切换回主机硬件状态,并返回正常的KVM/ARM主机的包含完整内核映射的SVC代码。

当从虚拟机中返回时,KVM/ARM将会检查trap的原因,并执行必要的模拟或者资源的分配,以便虚拟机被重新恢复。例如:如果guest端对模拟设备执行内存映射I/O(MMIO)操作,则会触发二级页表错误,因为只有针对guest端的物理RAM才会被映射到二级转换页表中。KVM/ARM将读取仅在HYP模式下可用的系统寄存器,其包含了导致错误的地址,并通过QEMU和内核之间的共享内存映射结构将该地址报告给QEMU。QEMU知道仿真系统的内存映射,并将该操作转发给对应的模拟设备代码。另外一个例子就是:当在虚拟机中产生了一个硬件中断时,系统将会陷入到HYP模式,KVM/ARM将会切换回主机状态,并重新使能中断,然后在主机状态下去处理该中断,但是此次不会陷入到HYP模式。虽然每个硬件中断都会中断CPU两次,但是从虚拟机到主机的状态奇幻,ARM硬件上的实际捕获终端的成本就微不足道了。

 

HYP模式

与标准的ARM内核模式相比,HP模式是一种更有特权的模式,而且没有体系架构上定义的额ABI可以从特短更少的额模式进入到HYP模式,故从KVM/ARM框架中提供对HYP模式的访问是一个不小的挑战。一个可选择的方案就是需要bootloader安装安全监控处理程序(secure monitor handler)或者hypercall处理函数,这将允许内核陷入到HYP模式,但是该种做法容易出错,这一点在搭建TrustZone APIs的经验表明很难去建立一个在不同ARM体系架构中的API标准体系。

相反,will Deacon, Catalin Marinas和Lan Jackson建议,如果内核要支持KVM/ARM,那么就得依赖于在HYP模式下引导内核。在3.6版本的内核中,Dave Martin和Marc Zyngier开发的一系列小补丁,用于检测内核是否是在HYP模式下被引导,如果是这样,则安装一个小型stub处理程序,运行KVM/ARM等其他子系统稍后可以控制HYP模式。从实际效果来看,从HYP模式下来引导内核是合理的,因为即使是以前的内核也总是在引导的时候需要显式地切换到SVC模式。因此,将引导程序修改为从HYP模式引导所有内核可以做到内核的兼容。

在HYP模式下引导安装内核是个非常有趣的挑战。第一,ARM内核通常是以压缩镜像的方式进行加载,带有一个称为“解压器”的小型未压缩引导环境,他将内核镜像解压到内存中。如果解压器探测到当前是在HYP模式下启动的,那么在这个阶段必须安装一个临时的stub,以便允许CPU回到SVC模式来运行解压的代码。因为解压器必须打开MMU来使能cache,但是在HYP模式下则需要HYP支持LPAE页表格式,这是解码器中不需要的复杂部分。因此,解压器安装临时的HYP stub,是CPU回到SVC模式,解压内核镜像,在调用未压缩的初始化代码之前,立即再次切换回HYP模式。然后,未压缩的初始化代码将再次检测CPU是否处于HYP模式,并安装主HYP stub,以便在引导过程或者是内核启动之后而被使用。HYP stub的内容存放在arch/arm/kernel/hyp-stub.s文件中。注意,未压缩的初始化代码并不关心未压缩的代码是直接从HYP模式进入的还是从解压器进入的。

由于HYP模式具有比SVC模式更大的特权,所有从SVC模式到HYP模式的转换只能通过硬件trap。这样的trap可以通过执行HVC指令来产生,该指令将使CPU陷入HYP模式,并最终导致CPU去执行HYP异常向量表中对应的代码。这就运行子系统通过hypervisor的stub来接管HYP模式的控制权限,因为hypervisor stub允许子系统更改异常向量的位置。HYP stub通过__hyp_set_vectors函数被调用,该函数将HYP的异常向量的物理地址作为唯一的参数,并将HYP向量基地址寄存器(HVBAR替换为该地址)。当KVM/ARM在正常内核引导期间进行初始化时(在所有主要内核初始化函数运行之后),它将创建HYP模式初始化代码的映射表示(一一映射),其将包含一个异常向量,并调用__hyp_set_vectors函数来设置该向量的地址。此外,KVM/ARM初始化代码调用HVC指令来运行标识的初始化代码,这可以安全第使能MMU,因为代码是被标识映射的。

最后,KVM/ARM初始化时会设置HVBAR,时期指向KVM/ARM的HYP异常处理代码。由于HYP模式具有其自有的地址空间,KVM/ARM必须为映射到HYP模式中的任何代码或者数据选择合适的虚拟地址。为了方便和清晰,内核虚拟地址被重用映射到HYP模式的页表中,这就使得只要将所有相关的数据结构映射到HYP模式,这样就可直接取消对结构成员的引用。

在虚拟机中的敏感操作和主机端内核的hypercalls都是通过抛出一个异常进入到CPU的HYP模式。这样就不需要在host os和guest os之间的每个交换机上更改HYP异常向量,而是使用一个HYP异常向量来处理来自host和guest内核的HVC调用。HYP模式的向量处理代码会检查二级页表基寄存器中的VMID字段,VMID0是预留给host的,此字段只能在HYP模式下被访问,因此会阻止来自guest的特权访问。我们介绍kvm_call_hyp函数,该函数可在HYP模式执行代码。例如:在SVC模式下运行的KVM/ARM代码可调用如下命令是TLB条目无效,更改操作必须在HYP模式下完成:

kvm_call_hyp(__kvm_tlb_flush_vmid_ipa, kvm, ipa);

 

虚拟GIC和timers

支持硬件虚拟化的ARMv7架构也包括对时钟和中断的虚拟化支持。Marc Zyngier实现了对这些特性的支持,这些特性称为“通用计时器”和虚拟通用中断控制器(VGIC)。

传统意义上,ARM系统上的定时器操作一直是针对专用定时器设备的MMIO操作。例如虚拟机执行的这种MMIO操作将t陷入到QEMU中,这也即是说每次读取时钟计数器或者每次需要变成计数器时都将触发虚拟机与主机内核之间的切换,以及从主机内核空间到用户空间的切换。当然计数器功能可以在内核中进行模拟,但是这就需要从虚拟机陷入到主机内核态,因此与单纯的在本地硬件上运行系统相比,将大大增加虚拟机的开销,而在Linux系统中,读取时钟计数器却是个非常频繁的操作。例如:每当任务在调度器中进入队列活退出队列时,都会鞥新运行队列的时钟,特别是像Apache基准这样的多进程工作负载测试将会清楚地显示捕获每个计数器读取的开销。

ARM允许对体系结构(通用计时器)进行可选扩展,这使得计数器和计时器操作成为核心架构中的一部分。现在,读取计数器或者编程计时器是通过访问和使用协处理器寄存器来完成,而且通用计数器提供两种时钟和计数:物理和虚拟。虚拟计数器和时钟总是可用的,但是对物理时钟和计数器的访问则可通过控制寄存器进行限制,这些寄存器只能在HYP模式下才能被访问。如果内核以HYP模式进行启动,则将其配置为使用物理时钟,否则内核则使用虚拟计时器。这样在虚拟机中运行的未经修改的内核编程计时器时则不需要切换到主机态来完成该操作。这也就提供了主机态与虚拟机态之间的必要隔离。

如果一个虚拟机在编程一个虚拟时钟时,而恰巧虚拟时钟在更改之前就被抢占了,则KVM/ARM读取计时器设置来计算计时器上剩余的时间,并在内核中修改相关的软计时器。当软计时器过期时,计时器处理程序会将时钟中断注入到虚拟机中。在虚拟机运行时,如果虚拟机在软计时器到期之前被调度,则虚拟计时器硬件将被重新编程。

中断控制器的作用是接收来自于设备的中断请求并将其转发到一个或者多个CPU中。ARM的通用中断控制器(GIC)提供了一个“分发器”,其是GIC和几个CPU接口的核心逻辑。GIC运行CPUs屏蔽某些中断、设定优先级或者将特定中断绑定到特定的CPU上。最终一个CPU可用使用GIC将处理器间中断(IPIs)从一个CPU核发送给另外的CPU核,这是ARM上SMP交叉调用的底层机制。

通常,当GIC想CPU发出中断时,CPU将会向GIC确认中断,与中断设备进行交互,向GIC发出中断结束信号(EOI),恢复正常操作。确认和EOI信号中断都是特权操作,当在虚拟机中产生该操作时将会产生主机态和虚拟机态的切换,从而增加而外的性能开销。VGIC中对虚拟化的硬件支持是以虚拟CPU接口的形式被提供,CPU可以查询该虚拟CPU接口来确定和EOI虚拟终端,从而无需产生主机态和虚拟机态的切换。硬件的支持进一步为VGIC提供了一个虚拟控制接口,该接口仅能由KVM/ARM访问,用于变成从虚拟设备(通常是由QEMU模拟的)产生的虚拟中断。

由于对分发器的访问并不是一个常见的操作,所以硬件并不提供虚拟分发服务器,所以KVM/ARM提供了GIC分发仿真代码嵌入到内核中,作为支持VGIC的一部分。因此,虚拟主机可直接确认中断和EOI虚拟中断而无需切换到主机态来完成该操作。在虚拟机接收的实际硬件中断往往会让CPU陷入到HYP模式,KVM/ARM让内核的标准ISRs像往常一样处理中断,因此主机仍然完全控制物理硬件。

VGIC或者通用计时器并没有机制允许硬件直接将物理中断作为虚拟中断从虚拟计时器穿透到虚拟机中。因此虚拟机的定时器中断会像任何其他硬件中断一样陷入,KVM/ARM为虚拟时钟中断注册了一个处理入口,当该处理功能被ISR调用时,其会将使用软件向相关的虚拟时钟中断注入该中断。

 

结论

在KVM/ARM的开发过程中,我们不断第测量虚拟化的开销,并运行长时间的工作负载来测试其稳定性和性能。我们使用多内核配置和多用户空间环境(包括ARM和Thumb-2),使用SMP和上位机对host和guest来验证其工作负载。一些负载一次运行多个星期而没有崩溃,而且其暴露在极端内存压力和CPU过载的情况下业余预期的行为一直。因此我们认为该实现是稳定的,并鼓励用户尝试和使用该系统。

我们使用宏观和围观基准来显示结果,在多平台的负载均衡上,KVM/ARM的开销最多占用其10%的性能开销。而CPU绑定的工作负载几乎与未使用KVM/ARM的主机运行一直。KVM/ARM的性能开销与X86平台相当。对于一些宏工作负载,比如Apache和MySQL,KVM/ARM甚至比使用相同配置的X86上的开销还小。这种性能改进的一个重要原因可以归结于IPIs路径的优化和对进程重新调度造成的VGIC和定时器硬件支持。