系统虚拟化技术按照实现方式可以分为基于软件的全虚拟化技术、硬件辅助虚拟化技术和半虚拟化技术。一般来说,虚拟机上的客户机操作系统“认为”自己运行在真实的物理硬件资源上,但为了提升虚拟化性能,会修改客户机操作系统使之与Hypervisor相互协作共同完成某些操作。这种虚拟化方案称为PV(Para-Virtualization,半虚拟化),与之对应的是全虚拟化(Full Virtualization),无须修改客户机操作系统就可以正常运行虚拟机。下面分别从基于软件的全虚拟化、硬件辅助虚拟化和半虚拟化三个方面对上述虚拟化实现技术进行分析。
基于软件的全虚拟化技术采用解释执行、扫描与修补、二进制翻译(Binary Translation,BT)等模拟技术弥补虚拟化漏洞。解释执行采用软件模拟虚拟机中每条指令的执行效果,相当于每条指令都需要“陷入”,这无疑违背了虚拟化的高效原则。扫描与修补为每条敏感指令在Hypervisor中生成对应的补丁代码,然后扫描虚拟机中的代码段,将所有的敏感指令替换为跳转指令,跳转到Hypervisor中执行对应的补丁代码。二进制翻译则以基本块为单位进行翻译,翻译是指将基本块中的特权指令与敏感指令转换为一系列非敏感指令,它们具有相同的执行效果。对于某些复杂的指令,无法用普通指令模拟出其执行效果,二进制翻译采用和类似扫描与修补的方案,将其替换为函数调用,跳转到Hypervisor进行深度模拟。以如下x86指令集中cpuid指令的执行代码为例。cpuid指令是x86架构中用于获取CPU信息的敏感指令。经过二进制翻译后,将其替换为对helper_cpuid的函数调用,即获取虚拟CPU的配置信息并返回,模拟出真实cpuid指令的执行效果。翻译前后代码如下。
对于内存虚拟化,前面提到虚拟化需要引入一层新的地址空间,即GPA。客户虚拟机中的应用使用的是GVA(Guest Virtual Address,客户机虚拟地址),而要访问内存中的数据,必须通过HVA(Host Virtual Address,宿主机虚拟地址)访问HPA。一种可能的解决方案是,将地址转换分为两部分,分别加载GPT(Guest Page Table,客户机页表)和HPT(Host Page Table,宿主机页表),完成GVA到GPA再到HPA的转换,其中转换GVA→GPA由GPT完成,而转换HVA→HPA由HPT完成,而中间转换GPA→HVA通常由Hypervisor维护,这样通过复杂的GVA→GPA→HVA→HPA多级转换,完成了客户虚拟机的内存访问,开销比较大。因此基于软件的虚拟化技术引入了SPT(Shadow Page Table,影子页表),记录了从GVA到HPA的直接映射,只需要将SPT基地址加载到页表基地址寄存器(例如CR3)中即可完成从GVA到HPA的转换。由于每个进程有自己的虚拟地址空间,因此SPT的数目与虚拟机中进程数目相同。为了维护SPT与GPT的一致性,Hypervisor需要截获虚拟机对GPT的修改,并在处理函数中对SPT进行相应的修改。
物理机访问I/O设备一般是通过PIO或MMIO的方式,因此I/O虚拟化需要Hypervisor截获这些操作。在x86场景下,PIO截获十分简单,因为设备发起PIO一般需要执行IN、OUT、INS和OUTS指令,这四条指令都是敏感指令,可以利用CPU虚拟化中提到的方式进行截获。而对于MMIO而言,CPU是通过访问内存的方式发起的。因此Hypervisor采用一种巧妙的方式解决这一问题:在建立SPT时,Hypervisor不会为虚拟机MMIO所属的物理地址区域建立页表项,这样虚拟机MMIO操作就会触发缺页异常从而陷入Hypervisor中进行处理。
因此,全虚拟化区分普通用户态指令和系统特权指令。前者直接执行即可,而对后者使用陷入和模拟技术返回到虚拟机监视器系统来模拟执行。问题的关键在于对敏感指令的处理:全虚拟化实现了一个二进制系统翻译模块,该模块负责在二进制代码层面对代码进行转换,从而将敏感代码转换为可以安全执行的代码,避免了敏感指令的副作用。二进制翻译系统往往还带有缓存功能,显著提高了代码转换的性能。该翻译技术的主要使用者有VMware公司早期版本的系统虚拟化产品。虽然全虚拟化无须对客户机操作系统进行任何修改,但却带来了二进制翻译的开销,导致了性能瓶颈。
为了解决软件在虚拟化引入的性能开销,Intel和AMD等CPU制造厂商都在硬件上加入了对虚拟化的支持,称为硬件辅助虚拟化(Hardware Assisted Virtualization)。基于硬件辅助虚拟化,也可以实现全虚拟化,不用修改客户机代码。这里以典型的Intel VT技术为例进行简要说明。
前面提到,原本的x86架构存在虚拟化漏洞,并非所有敏感指令都是特权指令。最直接的解决方案就是让所有敏感指令都能触发异常,但是这将改变指令的语义,导致现有的软件无法正常运行。于是Intel VT-x引入了VMX(Virtual-Machine Extensions,虚拟机扩展)操作模式,包括根模式(root mode)和非根模式(non-root mode),其中Hypervisor运行在根模式而虚拟机运行在非根模式。在非根模式下,所有敏感指令都会触发VM-Exit陷入Hypervisor中,而其他指令则可以在CPU上正常运行。VMX的引入使得Hypervisor无须大费周章地去识别所有的敏感指令,极大地提升了虚拟化的性能。
而对于内存虚拟化,前面提到可能需要两次地址转换,这就需要不断地切换页表寄存器CR3的值。因此软件全虚拟化技术引入了SPT,直接将GVA转换为HPA,而Intel VT-x引入了EPT(Extended Page Table,扩展页表)。原本的CR3装载客户页表将GVA转换为GPA,而EPT负责将GPA转换为HPA,直接在硬件上完成了两次地址转换。两次地址转换即GVA→GPA→HPA,其中转换GVA→GPA仍由客户机操作系统的GPT转换,不用修改;而第二次GPA→HPA由硬件EPT自动转换,对客户虚拟机透明。虽然SPT是直接将GVA转换为HPA(GVA→HPA),只有一次硬件转换。在没有页表修改的条件下,SPT更高效;然而客户机页表的修改需要通过VM-Exit陷出到Hypervisor进行模拟以保证页表同步,导致了SPT的性能问题。采用EPT后,客户机页表的修改不会导致EPT的同步(没有VM-Exit),因此EPT更为高效。此外,前面提到SPT的数量与虚拟机中进程数目相对应,而由于EPT是将GPA转换为HPA,所以理论上只需要为每个虚拟机维护一个页表即可,减少了内存占用。
在I/O虚拟化方面,Intel引入了Intel VT-d(Virtualization Technology for Direct I/O,直接I/O虚拟化技术)等硬件优化技术。相较于软件全虚拟化技术需要对设备进行模拟,Intel VT-d支持直接将某个物理设备直通给某个虚拟机使用,这样虚拟机可以直接通过I/O地址空间操作物理设备。但也引入了新问题,原本物理设备发起直接内存访问需要的是宿主机物理地址,但将它分配给某个虚拟机后,该虚拟机只能为其提供客户机物理地址。因此Intel VT-d硬件上必须有一个单元(DMA重映射硬件)负责将GPA转换为HPA。这种设备直通分配有一个明显缺陷,即一个物理设备只能供一个虚拟机独占使用,这就需要更多的物理硬件资源。
SR-IOV(Single Root I/O Virtualization,单根I/O虚拟化)设备可以缓解这一问题。每个SR-IOV设备拥有一个PF(Physical Function,物理功能)和多个VF(Virtual Function,虚拟功能),每个VF都可以指定给某个虚拟机使用,这样从单个物理设备上提供了多个虚拟设备分配给不同的虚拟机使用。但SR-IOV的VF数量受硬件限制,限定了虚拟设备的可扩展性。
半虚拟化打破了虚拟机与Hypervisor之间的界限,在某种程度上,虚拟机不再对自己的物理运行环境一无所知,它会与Hypervisor相互配合以期获得更好的性能。在半虚拟化环境中,虚拟机将所有的敏感指令替换为主动发起的超调用(Hypercall)。Hypercall类似于系统调用,是由客户机操作系统在需要Hypervisor服务时主动发起的。通过Hypercall,客户机操作系统主动配合敏感指令的执行(这些指令受Hypervisor监控),大大减少了虚拟化开销。
相较于全虚拟化的暴力替换,半虚拟化则另辟蹊径。它对客户机操作系统中的敏感指令都进行了替换,取而代之的是Hypervisor新增的Hypercall。这些Hypercall实现了敏感指令本身的功能,同时在每次执行时都确保能够退回到Hypervisor。此外,不仅仅是敏感指令的处理,如果客户机操作系统能够意识到自身运行于虚拟机环境下,并与Hypervisor进行配合,修改代码进行针对性优化,就能提升性能。Xen早期采用半虚拟化作为性能提升的主要手段,并大获成功。因此相较于CPU虚拟化,由于I/O半虚拟化性能提升明显,更受开发者的关注,如virtio、Vhost、Vhost-user和vDPA等一系列优化技术被提出来,并且不断演进。因此,相较于全虚拟化,半虚拟化能够提供更好的性能,但代价却是对客户机操作系统代码的修改。为了支持各种操作系统的各种版本,半虚拟化虚拟机监视器的实现者必须付出大量代价做适配工作(virtio提供了标准化的半虚拟化硬件接口,减少了适配难度),并且由于半虚拟化需要修改源代码,对不开源的系统(如Windows)适配比较困难(目前Windows设备驱动程序也广泛支持virtio接口)。
表1-1总结了三种虚拟化实现方式对CPU、内存和I/O虚拟化等方面的影响。目前主流硬件架构(包括x86、ARM等)都对硬件辅助虚拟化提供了支持,RISC-V的虚拟化硬件扩展作为重要的架构规范也正在完善中 。同时,半虚拟化驱动规范virtio接口标准正在普及,目前主流的操作系统都提供了virtio的支持。因此,目前主流的虚拟化实现方式是硬件辅助虚拟化和半虚拟化,大幅降低了虚拟化性能的开销。
表1-1 虚拟化实现技术比较
在CPU、内存和I/O三类虚拟资源中,前两类都已经得到较好的解决。由于I/O设备抽象困难、切换频繁、非常态事件高发等原因,开销往往高达25%~66%(万兆网卡中断达70万次/秒,虚拟化场景下占用高达5个CPU核),因此I/O成为需要攻克的主要效能瓶颈。此外,虚拟化可以嵌套(Nested Virtualization),即在虚拟机(L1,第一层)中创建和运行虚拟机(L2,第二层),以此类推(物理机看作第零层,L0)。如果CPU支持嵌套的硬件辅助虚拟化(如Intel x86),则可以降低嵌套的虚拟机性能损失。限于篇幅,本书不深入讨论嵌套虚拟化。