基本概念
虚拟化泛指将物理资源抽象成虚拟资源,并在功能和性能等方面接近物理资源的技术。例如:
- 物理内存抽象成虚拟内存
- 操作系统中设备抽象成文件
- 物理显示器抽象成窗口
- Java程序运行在JVM(Java virtual machine)
一般而言,计算机系统自下而上被划分为多个层次
- 硬件向上对软件提供的指令集抽象ISA(instruction set architecture)通常被分为系统ISA和用户ISA。系统ISA提供特权操作,一般在操作系统内核态运行,例如切换进程页表。用户ISA提供给普通应用程序使用,一般在用户态运行。两者结合起来构成完整的硬件编程接口:ISA = system ISA + user ISA
- 操作系统运行于硬件之上,向下管理硬件资源,向上提供系统调用接口。上层应用可以通过系统调用请求操作系统执行特权操作。系统调用与用户ISA共同组成了应用程序的ABI(application binary interface): ABI = system call + user ISA
- 库程序调用(library calls)提供系统运行时、系统公共服务和功能丰富的第三方程序。其运行与用户态,以函数库的形式供应用程序调用。应用程序一般通过库函数调用操作系统的系统调用接口。确定了用户程序所使用的用户ISA和所调用的库函数,也就确定了应用程序的用户态编程接口API(application program interface):API = library calls + user ISA
硬件和软件资源在不同层次的抽象对应不同的虚拟化方法
- 物理硬件层的虚拟化:虚拟机监控器hypervisor通过直接对硬件资源进行抽象和模拟,提供了虚拟硬件ISA接口,包括了虚拟系统ISA和虚拟用户ISA。虚拟硬件ISA提供了完整的硬件资源抽象,从而使多个操作系统能够运行其上并共享硬件资源
- 操作系统层的虚拟化:操作系统本身就是虚拟化技术的一种体现。它将CPU、内存和IO等资源进行了抽象,最终以进程为单位使用这些资源,并进一步由命名空间(namespace)和控制组(Cgroups)等容器(container)技术实现对资源的隔离和限制。这一层次的虚拟化称之为轻量级虚拟化、进程虚拟化。有些地方将基于二进制翻译的方法运行相同或不同ISA的二进制程序称为进程虚拟化。而容器虚拟化主要是基于同构指令集的轻量级虚拟化方法,其提供了虚拟ABI。最典型的就是docker技术。
- 应用层程序运行环境的虚拟化:应用程序本身作为虚拟机(也称为沙箱sandbox),提供洪湖态虚拟运行时支撑用户应用程序的运行,即虚拟API接口。比较常见的就是高级程序语言的虚拟运行环境,例如JVM和python虚拟机
主要功能和分类
基本功能
经典的“一虚多”系统虚拟化架构如图所示。hypervisor运行在硬件资源层上,并为虚拟机提供虚拟的硬件资源。guest os运行在虚拟的硬件资源之上,这与传统的操作系统功能一致,管理硬件资源并为上层应用提供统一的软件接口。总览整个架构,hypervisor需要具备以下两种功能:
- 虚拟环境的管理。hypervisor需要对物理硬件进行虚拟化,所以至少需要CPU虚拟化模块、内存虚拟化模块、I/O虚拟化模块。除此之外,为了满足多个虚拟机同时运行,那么也需要具备一套完整的调度机制来调度各虚拟机执行,调度的基本单位是vCPU,而非VM。
- 物理资源的管理。某种程度上来说,它还担负着操作系统的职责,管理底层物理资源、提供安全隔离机制,以保证虚拟机中的恶意代码不会破坏整个系统的稳定性。
其中较为重要的就是虚拟化必不可少的三个模块:
- CPU虚拟化:虚拟环境下,hypervisor运行与最高特权级别,而客户机操作系统处于非最高特权级,无法直接访问物理资源。因此虚拟机对物理资源的访问应当触发异常,陷入hypervisor中进行监管和模拟。这些访问物理资源的指令称为敏感指令。这种处理方式称之为trap and emulate。但是系统中通常存在一些敏感非特权指令,这就被称为虚拟化漏洞,CPU虚拟化的关键就是消除虚拟化漏洞。
- 内存虚拟化:内存虚拟化引入了GPA(guest physical address)供虚拟机使用。但是当虚拟机需要访问内存时,是无法通过GPA找到对应的数据的,需要hypervisor将GPA转为HPA(host physical address)
- I/O虚拟化:在物理环境下,操作系统通过I/O端口访问特定的I/O设备,称为PIO。或者将I/O设备上的寄存器映射到预留的内存地址空间进行读写,称为MMIO。hypervisor需要截获所有的PIO和MMIO操作并对其进行模拟,再将结果告知虚拟机。
虚拟化分类
虚拟化必须满足以下三个条件:
- 资源控制:虚拟机对物理资源的访问都应在hypervisor的监控下进行,虚拟机不能超过hypervisor直接访问物理机资源。
- 等价:物理机与虚拟机的运行环境在本质上应该是相同的。
- 高效:绝大多数的虚拟机指令应该由主机硬件直接执行,而无须控制程序的参与。
根据hypervisor与物理资源和操作系统交互方式的不同,可以将其分为两类
- type1类型的hypervisor直接运行在物理硬件资源上,需要承担系统初始化、物理资源管理等操作系统职能。可以将其视为一个为虚拟化而优化裁剪过的内核,可以直接在hypervisor上加载客户机操作系统。
- type2类型的hypervisor运行在宿主机操作系统中,只负责实现虚拟化相关功能,物理资源的管理等则是复用宿主机操作系统中的相关代码。这种类型更像是对操作系统的一种拓展。
系统虚拟化实现方式
基于软件的全虚拟化
基于软件的全虚拟化技术采用解释执行、扫描与修补、二进制翻译等模拟技术弥补虚拟化漏洞。解释执行采用软件模拟虚拟机中每条指令的执行效果,相当于每条指令都需要trap,这种方法并不高效。扫描与修补为每条敏感指令在hypervisor中生成对应的补丁代码,然后扫描虚拟机中的代码段,将所有的敏感指令替换为跳转指令,跳转到hypervisor中执行对应的补丁代码。二进制翻译则以基本块为单位进行翻译,翻译是指将基本块中的特权指令与敏感指令转换为一系列非敏感指令,它们具有相同的执行效果。对于某些复杂的指令,无法用普通指令模拟出其执行效果,二进制翻译就会采用类似于扫描和修补的方案,将其替换为函数调用。
对于内存的虚拟化,前文提到需要引入GPA。一般来说需要完成GVA→GPA→HVA→HPA的复杂转换。GVA到GPA由GPT(客户机页表完成)。HVA到HPA由(宿主机页表完成)。而中间转换GPA到HVA通常就是由hypervisor进行维护。但是如此复杂的转换开销较大。因此基于软件的全虚拟化技术引入了SPT(影子页表)。SPT记录了GVA到HPA的直接映射。每一个进程都需要一张SPT。为了维护SPT和GPT的一致性,hypervisor会截获虚拟机对GPT的修改,并在处理函数中对SPT进行相应的修改。
上文提到访问I/O设备一般是通过PIO或MMIO的方式。所以对IO设备的虚拟化只需要hypervisor截获这些操作。PIO操作一般都会使用到敏感指令,利用CPU虚拟化进行截获。对于MMIO而言,在建立SPT时,hypervisor不会为虚拟机MMIO所属的物理地址区域建立页表项,这样在执行MMIO时会触发缺页异常从而陷入到hypervisor。
硬件辅助虚拟化
为了解决软件在虚拟化引入的性能开销,在硬件上可以加入对虚拟化的支持,称为硬件辅助虚拟化。典型的有Intel VT技术(例如引入了EPT拓展页表用于将GPA转为HPA)。AMD等CPU厂商也有自己的硬件虚拟化技术,在这就不展开了。
半虚拟化
半虚拟化打破了虚拟机与hypervisor之间的界限,虚拟机会与hypervisor相互配合以期获得更好的性能。在半虚拟化环境中,虚拟机将所有敏感指令替换为主动发起的超调用hypercall。它类似于系统调用。通过hypercall,客户机操作系统主动配合敏感指令的执行,大大减少了虚拟化的开销。
KVM/QEMU
KVM是Linux内核提供的开源hypervisor,也是目前主流的虚拟化技术。KVM在Linux启动时被动态加载,其利用硬件辅助虚拟化的特性,能够高效的实现CPU和内存的虚拟化。
KVM无法单独使用,因为其不提供IO设备的模拟,也不支持对整体虚拟机的状态进行管理。它向用户态程序暴露特殊的设备文件/dev/kvm作为接口,运行用户态程序利用它来实现最关键的CPU和内存虚拟化,而QEMU则可以弥补缺失的IO虚拟化所需要的设备模型。
QEMU是开源的软件仿真器,它能够通过动态二进制翻译技术来实现CPU虚拟化,同时提供多种IO设备的模拟,因此可以作为低速的type2 hypervisor进行工作。然而,二进制翻译这种模拟方法带来了巨大的性能开销,导致虚拟机运行缓慢。为此QEMU利用KVM暴露的/dev/kvm接口,以KVM作为“加速器”,从而提升虚拟机的性能。
QEMU通过打开设备文件/dev/kvm实现和KVM内核模块(kvm.ko)的交互。在创建虚拟机时,QEMU会根据用户配置完成创建vCPU线程,分配虚拟机内存、创建虚拟设备等工作。在QEMU中,虚拟机的每个vCPU对应QEMU的一个线程,当QEMU完成所有初始化工作后,会通过ioctl指令进入内核态的KVM模块中,由KVM模块通过虚拟机启动或恢复指令切换到虚拟机运行,执行虚拟机代码。当vCPU执行了特权指令或者发生特定行为时,会触发虚拟机的trap操作退出到KVM,由KVM判断能否进行处理。