QEMU-KVM虚拟化技术
虚拟化技术概述
虚拟化是一个广义的术语,在计算机方面通常是指计算元件在虚拟的基础上而不是真实的基础上运行(对计算机物理资源的抽象,实现资源的模拟、隔离和共享)。虚拟化技术可以使得一台计算机上原本只能够一台计算机使用的硬件设备能够同时运行多个逻辑计算机,每个逻辑计算机可运行不同的操作系统,并且应用程序都可以在相互独立的空间内运行而互不影响,从而显著提高计算机的工作效率。
在实际的生产环境中,虚拟化技术主要用来解决高性能的物理硬件产能过剩和老的、旧的硬件产能过低的重组、重用,透明化底层物理硬件,从而最大化的利用物理硬件,对资源充分利用。
我们将此虚拟的环境称之为VM(Virtual Machine)。安装在这个环境之上的系统我们称为Guest OS(客户系统);运行VMM的操作系统则称Host OS(本地操作系统)。
虚拟化的本质
分区:在整个物理服务器上运行多个虚拟机;隔离:在同一服务器上的虚拟机相互隔离;封装:整个虚拟机都保存在文件中,可以通过移动文件的方式迁移;相对硬件独立:无需修改任何服务上运行的虚拟机;
虚拟化分类
从虚拟化的实现方式来看,虚拟化架构主要有三种形式:寄居虚拟化架构、裸金属虚拟化架构和操作系统虚拟化架构。
以虚拟化技术维度,主要有:全虚拟化、半虚拟化、硬盘辅助虚拟化。
虚拟化架构
裸金属虚拟化(Type1)
裸机虚拟化指的是,Hypervisor 直接运行在硬件上,即以 Hypervisor 作为 Host OS 直接管控硬件资源。例如 VMware ESXI
便是采用此种架构的 Hypervisor。虚拟机有指令要执行时,Hypervisor会接管该指令,模拟相应的操作。
宿主机虚拟化(Type2)
宿主机虚拟化,也叫寄居虚拟化。在一个完整的计算机系统中,最底层是物理硬件,物理硬件之上是操作系统,操作系统之上是各类软件和进程。寄居虚拟化便是在主机操作系统之上安装Hypervisor,也叫VMM(Virtual Machine Monitor,虚拟机管理层),再往上便是虚拟化软件所创建的虚拟户了。
宿主机虚拟化实现方式是直接安装和运行应用程序即可,便于实现。但是因其主要依托主机操作系统对设备的支持,性能损耗较大。典型的产品为VMware Workstations。
操作系统虚拟化(容器)
隔离性差,最后一种不常用,虚拟机运行在传统操作系统上,创建一个独立的虚拟化实例(容器Container),指向底层托管操作系统,缺点是操作系统唯一,如果底层操作系统跑的是Windows,那么VPS/VE就都得跑Windows。
在宿主架构中的虚拟机作为主机操作系统的一个进程来调度和管理,裸金属架构下则不存在主机操作系统,它是以Hypervisor直接运行在物理硬件之上,即使是有类似主机操作系统的父分区或Domain 0,也是作为裸金属架构下的虚拟机存在的。宿主架构通常用于个人PC上的虚拟化,如WindowsVirtual PC,VMware Workstation,Virtual Box,Qemu等,而裸金属架构通常用于服务器的虚拟化。
虚拟化技术
Ring环
这部分一定要知道
早期OS在设计时,设计了一个Ring0-Ring3的权限环机制。其中只用到了Ring0和Ring3。
-
Ring0被称为内核态,即对硬件的敏感操作均在此;如果这里出现问题,系统很可能会崩溃。因为ring 0可以直接访问CPU和系统内存。
-
Ring1-2是驱动层。ring 1 和ring 2 提供了ring 3 所缺乏的独特优势。操作系统使用ring 1 与计算机硬件进行交互。这个戒指需要运行命令,例如通过我们监视器上的摄像头流式传输视频。必须与系统存储、加载或保存文件交互的指令存储在ring 2 中。这些权限称为输入和输出权限,因为它们涉及将数据传入和传出工作内存RAM。
-
Ring3被称为用户态,一般应用软件的正常操作均在此。由于ring 3无法访问CPU或内存,因此涉及这些的任何指令都必须传递给ring 0。
当程序运行在用户模式(Ring 3)时,如果需要执行一个需要更高权限的指令,比如直接访问硬件设备,它会触发一个陷阱(trap),操作系统会捕获这个陷阱并决定是否允许该操作。如果允许,操作系统会将CPU切换到更高的特权级别(如Ring 0或1)来执行该指令。
但在虚拟化环境中,这种特权级别的切换会变得更加复杂。虚拟机监控器(Hypervisor/VMM)需要拦截客户操作系统(Guest OS)发出的敏感指令,即使该指令在客户机中看似运行在特权级别(如Ring 0)。由于虚拟化层对硬件资源的绝对控制,客户操作系统的"Ring 0"实际上是经过虚拟化的非特权模式。此时,Hypervisor会通过陷入再模拟(Trap-and-Emulate)机制或借助硬件虚拟化扩展(如Intel VT-x/AMD-V),在真正的物理特权级别(如VMX root模式)下模拟该指令的执行,同时确保虚拟化环境的隔离性和安全性。
特权指令与敏感指令
- 特权指令(Privileged Instruction):对于系统中一些敏感资源的管理和读写的指令被定位特权指令,只有处于 Ring 0 才能进行正确执行,否则会丢出异常;
- 敏感指令(Sensitive Instruction):由于虚拟化的引入,由于 OS 现在处于 Ring1 所以不能执行特权指令,所以交由 Ring 0 的 VMM 来处理执行,这部分指令称为敏感指令;可以理解为客户机中必须交由 VMM 处理的指令;
对于有虚拟化的环境,客户机处于 Ring 1 而不是 Ring 0,如果所有的敏感指令都是特权指令,那么执行任意的敏感指令都会产生 trap,这样保证了客户机中如果进行这些“敏感”操作的指令,都会交给处于 Ring 0 的 VMM 处理;
敏感指令包括:
- 所有 I/O 指令;
- 企图访问或者修改 VM mode 或者机器状态的指令;
- 企图访问或者修改敏感寄存器 / 存储单元的指令;
- 企图访问存储保护系统或内存 / 地址分配系统的指令;
但是 x86 中有些指令,必须由处于 Ring 0 状态的 VMM 处理,但是工作在 Ring 1 不会产生 Trap,这样的话如果处于 Ring 1 的客户机执行这些指令,不会产生 Trap,也不能被定义为特权指令,这与上一句中的目的相冲突,所以必须也要 Trap 这些 “非特权指令”,x86 中称之为 临界指令(Critical Instructions);
所以 x86 中,敏感指令 = 特权指令 + 非特权指令 / 临界指令,如果一个系统上 敏感指令 = 特权指令,那么为了让 VMM 完全控制硬件资源,我们让虚拟机上的 OS 处于 Ring 1,不能直接执行 敏感/特权指令,而 VMM 处于 RIng 0 ,所以 OS 上执行 敏感/特权指令 的时候,就会 引起陷入 / cause a trap 到 VMM,再由 VMM 来模拟执行引起异常的指令;
临界指令 包括 敏感指令 中的 敏感寄存器指令 和 保护系统指令;
全虚拟化
也称为原始虚拟化技术,运行在虚拟机上的操作系统通过Hypervisor来最终分享硬件,所以虚拟机发出的指令需经过Hypervisor捕获并处理。
这是VMware提出的解决方案: BT, Binary Translation 二进制翻译技术, 即将所有特权指令通过VMM翻译之后传出, 最后再把结果传回给虚拟机。通过这样的方式, 就成功的欺骗了虚拟机, 让它以为自己在 0 环,即虚拟机不知道自己是虚拟机。如图所示。
全虚拟化技术不需要修改客户操作系统,因此具有很好的兼容性和可移植性。但这个方式有个问题,所有的指令都需要VMM进行翻译,性能折扣太高了。
半虚拟化
思想:将整个Guest OS
运行在非特权模式下,遇到敏感指令中的特权指令,可以通过陷入到vmm进行模拟执行。但有些敏感指令不是特权指令,半虚拟化的核心就是将Guest OS
中的此类指令全部修改了,通过hypercall直接和vmm通信,从而达到陷入再模拟的效果,修改操作系统内核来将不可虚拟化的指令替换为hypercalls,在这种情况下虚拟机知道自己是虚拟机。
半虚拟化不支持未经修改的操作系统(如Windows 2000/XP),因此它的兼容性和可移植性较差。
半虚拟实际运行时,要实现定制化的功能,首先要了解 API 的特性,并围绕 API 定制对应的功能,才能在已改变内核的操作系统中实现。
开源的Xen项目是半虚拟化的一个例子,它使用一个经过修改的Linux内核来虚拟化处理器,而用另外一个定制的虚拟机系统的设备驱动来虚拟化I/O。它通过Domain0进行敏感指令的操作。
硬件辅助虚拟化
虚拟化的核心就是如何保证Guest OS
可以正确的执行敏感指令又不破坏vmm,x86 cpu当前的运行级别无法满足要求,所以只能从软件上弥补,但性能上终归差点意思。硬件开发厂商提供了新的方法,给cpu薪资一种模式客户机模式(non-root模式)原先的模式称为root模式,每种模式拥有ring0~ring3。引入两种模式之后,前面讨论的非特权指令的敏感指令在non-root模式下可以重新定义。root模式下表现和原先一样 ,这样就完美的解决了虚拟化漏洞的问题。
虚拟化对象
CPU虚拟化
目前主要的 CPU 虚拟化技术是 Intel 的 VT-x/VT-i 和 AMD 的 AMD-V 这两种技术。
CPU 虚拟化使用的经典模型是「Trap & Emulate」,使用特权级压缩(Ring Compression)的方式来实现虚拟环境:
- Hypervisor 运行在最高特权级上,Guest VM 运行在低特权级上,Guest VM 在硬件上直接执行非敏感指令,当 Guest VM 执行到敏感指令时,其便会陷入位于最高特权级的 Hypervisor ,此时便能由 Hypervisor 模拟敏感指令的行为。
- 当发生 virtual CPU 调度时,我们将 vCPU 的状态保存,恢复 Hypervisor 状态,Hypervisor 完成其行为后进行下一 virtual CPU 的调度,恢复下一 vCPU 的状态并恢复执行。
纯软件实现CPU虚拟化
前文我们已经指出 x86 架构存在非特权敏感指令,直接导致 VMM 无法截获 x86 VM 的敏感行为,这违反了 Popek and Goldberg virtualization requirements
,因此在硬件对虚拟化的支持出现之前,虚拟化厂商只好先从软件层面下手。
模拟& 解释执行
「模拟」(Emulate)技术的出现其实早于虚拟化,纯软件的模拟本质上就是通过编写能够呈现出与被模拟对象相同行为的应用程式从而达到运行非同构平台应用程序的效果。
模拟技术不仅能够应用于程序级别的模拟,还能应用于系统级别的模拟:CPU 运行的本质行为其实就是从 PC 寄存器所指内存区域中不断取出指令解码执行,我们不难想到的是,实现一个虚拟机最简单粗暴的方法便是通过模拟每一条指令对应的行为,从而使得 VM 的行为对 VMM 而言是完全可控的。
实现模拟技术的原理也是最简单的——我们可以通过解释执行的方式来实现模拟技术,即模拟器程序不断地从内存中读取指令,并模拟出每一条指令的效果。从某种程度而言,每一条指令在执行时都完成了 “陷入”,因此我们可以使用模拟技术解决虚拟化的漏洞,同时还能模拟与物理机不同架构的虚拟机
Qemu——Quick Emulator
本质上便是一个模拟器,其完整地模拟了一套包括各种外设在内的计算机系统。
不过基于解释执行的模拟技术有着一个非常致命的缺点——性能极差,因为每一条指令都需要经过 VMM 的解析后再由 VMM 模拟执行,哪怕最简单的一条指令也可能需要分解成多个步骤与多次内存访问,效率极低。
让我们重新审视我们为什么需要在 x86 架构上使用模拟技术来实现虚拟机:非特权敏感指令的存在打破了 Popek and Goldberg virtualization requirements
,但非特权敏感指令仅是少数,大部分指令我们仍能直接在物理硬件上运行,因此基于模拟技术进行改进的虚拟化技术出现了:扫描 & 修补
与 二进制翻译
。
扫描 & 修补
虚拟化场景下的虚拟机大都是与物理机有着相同的 ISA,因此我们并没有必要采用纯模拟的技术实现虚拟机,而是可以让非敏感指令直接在硬件上执行,通过某种方式让非特权敏感指令陷入 VMM,从而重新实现 Trap & Emulate 模型。
扫描 & 修补(Scan & Patch)便是这样的一种技术,其让非敏感指令直接在硬件上执行,同时将系统代码中的敏感指令替换为跳转指令等能陷入 VMM 中的指令,从而让 VM 在执行敏感指令时能陷入 VMM,使得 VMM 能够模拟执行敏感指令的效果。
基本执行流程如下:
- VMM 在 VM 执行每段代码之前对其进行扫描,解析每一条指令,查找特权与敏感指令
- VMM 动态生成相应指令的补丁代码,并将原敏感指令替换为一个外跳转以陷入 VMM,从而在 VMM 中执行动态生成的补丁代码
- 补丁代码执行结束后,再跳转回 VM 中继续执行下一条代码
在「扫描 & 修补」技术当中大部分的代码都可以直接在物理 CPU 上运行,其性能损失较小,但「扫描 & 修补」同样存在着一定的缺陷:
- 特权指令与敏感指令仍通过模拟执行的方式完成,仍可能造成一定的性能损失
- 代码补丁当中引入了额外的跳转,这破坏了代码的局部性
局部性原理:CPU 存取指令 / 数据的内存单元应当趋向于聚集在一个较小的区域
- VMM 需要维护一份补丁代码对应的原始代码的副本,这造成了额外的开销
二进制翻译
为了进一步地提高虚拟化的性能,「二进制代码翻译」(Binary Translation)技术应运而生,类似于「扫描 & 修补」技术,二进制代码翻译同样会在运行时动态地修改代码,不过不同的是 BT 技术以基本块(只有一个入口和一个出口的代码块)作为翻译的单位:
- Emulator 对读入的二进制代码翻译输出为对应 ISA 的一个不包含特权指令与敏感指令的子集所构成的代码,使其可以在用户态下安全运行
- Emulator 动态地为当前要运行的基本块开辟一块空间,称之为翻译缓存(translation cache),在其中存放着翻译后的代码,每一块 TC 与原代码以某种映射关系(例如哈希表)进行关联
我们可以看出二进制代码翻译技术与扫描修补技术的原理大体上是非常类似的,但是二进制代码翻译技术会对所有的代码进行翻译,而扫描与修补技术则只会 patch 掉敏感指令与特权指令;同时扫描 & 修补技术不会改变代码的整体结构,而仅是将敏感与特权指令替换为能触发陷入 VMM 的指令,但是二进制代码翻译技术会直接改变一个基本块的代码整体结构(例如翻译前基本块可能长度 40B,翻译后变成 100B,内部代码的相对位置也会发生变化)。
翻译方法大致分为以下两种:
- 简单翻译:可以直接理解为等效代码模拟,这种方法实现较为简单,但是会让指令数量大幅膨胀。
- 等值翻译:翻译的原代码与结果代码相同。理论上大多数指令都可以使用等值翻译直接在硬件上执行,但这需要更复杂的动态分析技术。
在相同 ISA 架构上大部分指令都是可以直接进行等值翻译的,除了以下几种:
- PC 相对寻址指令。这类指令的寻址与 PC 相关,但在进行二进制翻译后更改了代码基本块的结构,因此这类指令需要额外插入一些补偿代码来确保寻址的准确,这造成了一定的性能损失。
- 直接控制转换。这类指令包括函数调用与跳转指令,其目标地址需要被替换为生成代码的地址。
- 间接控制转换。这类指令包括间接调用、返回、间接跳转,其目标地址是在运行时动态得到的,因此我们无法在翻译时确定跳转目标。
- 特权指令。对于简单的特权指令可以直接翻译为类似的等值代码(例如 cli 指令可以直接翻译为置 vcpu 的 flags 寄存器的 IF 位为 0),但对于稍微复杂一点的指令,则需要进行深度模拟,利用跳转指令陷入 VMM 中,这通常会造成一定的性能开销。
由于二进制代码翻译技术使用了更为复杂的过程,由此也会引入更多的问题,对于以下情形则需要额外的处理:
- 自修改代码(Self Modifying Code)。这类程序会在运行时修改自身所执行的代码,这需要我们的 Emulator 对新生成的代码进行重翻译。
- 自参考代码(Self Referential Code)。这类程序会在运行中读取自己的代码段中内容,这需要我们额外进行处理,使其读取原代码段中内容而非翻译后的代码。
- 精确异常(Precise Exceptions)。即在翻译代码执行的过程中发生了中断或异常,这需要将运行状态恢复到原代码执行到异常点时的状态,之后再交给 Guest OS 处理。BT 技术暂很难很好地处理这种情况,因为翻译后的代码与原代码已经失去了逐条对应的关系。一个可行的解决方案就是在发生异常时进行回滚,之后重新使用解释执行的方式。
- 实时代码。这类代码对于实时性要求较高,在模拟环境下运行会损失时间精确性,目前暂时无法解决。
硬件辅助虚拟化
Intel VT 技术是 Intel 为 x86 虚拟化所提供的硬件支持,其中用于辅助 CPU 虚拟化的是 Intel VT-x
技术,其扩展了传统的 IA32 处理器架构,为 IA32 架构的 CPU 虚拟化提供了硬件支持。
VT-x 技术为 Intel CPU 额外引入了两种运行模式,统称为 VMX 操作模式(Virtual Machine eXtensions),通过 vmxon
指令开启,这两种运行模式都独立有着自己的分级保护环:
VMX Root Operation
:Hypervisor 所工作的模式,在这个模式下可以访问计算机的所有资源,并对 VM 进行调度。VMX Non-Root Operation
:VM 所工作的模式,在这个模式下仅能访问非敏感资源,对于敏感资源的访问(例如 I/O 操作)会使得 CPU 退出 Non-Root 模式并陷入 Hypervisor 中,由 Hypervisor 处理后再重新进入 Non-Root 模式恢复 VM 的运行。
由此,我们对 Root 模式与 Non-Root 模式间的切换行为进行定义:
VM-Entry
:Hypervisor 保存自身状态信息,切换到 VMX Non-Root 模式,载入 VM 状态信息,恢复 VM 执行流。VM-Exit
:VM 运行暂停并保存自身状态信息,切换到 VMX Root 模式,载入 Hypervisor 状态信息,执行相应的处理函数。
由于 Non-Root 模式与 Root 模式都各自有着自己的分级保护环,因此 Host OS 与 Guest OS 都可以不加修改地在自己对应的模式下直接在硬件上运行,仅有当 Guest OS 涉及到敏感资源的访问及 Host OS 对 VM 的调度时才会发生切换,这在确保了 VM 高性能的同时满足了 Trap & Emulate
模型实现,也解决了 x86 架构的虚拟化漏洞。