Hardware and Software Support for Virtualization 学习笔记
本文最后更新于:2025年6月25日 上午
1 定义
本章介绍了虚拟化、虚拟机和虚拟机监视器的基本概念,因为各种文章、教科书和商业产品描述有时会使用相互冲突的定义。
- 虚拟化是通过强制模块化应用分层原则,即公开的虚拟资源与被虚拟化的底层物理资源相同。
- 虚拟机是一个完整的计算环境的抽象,通过对计算机的处理器、内存和 I/O 组件进行组合虚拟化。
- hypervisor 是一个管理和运行虚拟机的专用系统软件。
- 虚拟机监视器(VMM)是指管理程序中专注于 CPU 和内存虚拟化的部分。
1.1 虚拟化(virtualization)
定义是以计算机系统的两个基本原理为基础的。
首先,分层是单个抽象的表示,通过添加一个间接层来实现,当(i)间接依赖于单个较低层和(ii)使用定义良好的名称空间来公开抽象时。其次,强制的模块化还保证了层的客户端不能绕过抽象层,例如直接访问物理资源或对底层物理名称空间的使用具有可见性。因此,虚拟化只不过是一个分层实例,其公开的抽象与底层物理资源等价。是间接性、强制模块化和兼容性的结合,是降低计算机系统复杂性和简化操作的特别强大的方法。
以 RAID 的经典示例为例。RAID是一种磁盘阵列技术,将多个廉价的物理硬盘组合成一个虚拟磁盘,提供冗余和性能增强的存储解决方案。在这个例子中,虚拟化的思想是将多个物理硬盘抽象为一个单一的虚拟磁盘,使文件系统可以像访问单个硬盘一样访问这个虚拟磁盘。
- RAID示例:RAID是一种典型的虚拟化技术示例。它将多个物理硬盘组合在一起,以提供性能和冗余。虚拟化的概念在这里体现在将多个硬盘抽象为一个虚拟磁盘,因此文件系统不需要知道它们是物理硬盘。
- 接口的兼容性:虚拟磁盘和物理硬盘都遵循相同的块设备接口,这意味着文件系统可以以相同的方式与它们进行交互。这种兼容性使得无论RAID层是否存在,文件系统的部署方式都可以保持不变。
- 物理地址的隐藏:RAID层在内部管理磁盘资源,隐藏了物理地址的细节。这意味着文件系统可以透明地将数据存储在虚拟磁盘上,无需关心底层物理硬盘的位置。这种抽象简化了操作,特别是在硬盘故障时需要替换物理硬盘时。
计算机体系结构中的虚拟化: 虚拟化显然是计算机体系结构的基本组成部分。通过内存管理单元(MMU)公开的虚拟内存就是一个典型的例子: MMU 增加了一个间接级别,通常通过分段和分页机制的组合来隐藏应用程序的物理地址。由于 MMU 控制操作仅限于内核模式,因此强化了模块化。
1. 虚拟化的基本原理
虚拟化是计算机体系结构的重要组成部分,它旨在提供一种抽象层,使硬件资源能够被多个应用程序或系统共享,同时保障安全性和隔离性。其中一个经典的虚拟化示例是通过内存管理单元(MMU)实现的虚拟内存。MMU引入了一个额外的间接级别,通过分段和分页机制,将应用程序的物理内存地址映射到虚拟内存地址。这种技术有助于增强模块化,因为操作系统可以有效地管理和隔离不同应用程序的内存空间,使它们似乎在独立的地址空间中运行。
2. 操作系统中的虚拟化
虚拟化的概念也广泛应用于操作系统。操作系统的主要任务之一是为多个并发应用程序提供安全且隔离的访问计算机资源,如CPU、内存和I/O设备。例如,操作系统通过控制MMU来确保不同的进程具有孤立的地址空间,使它们无法相互干扰。此外,操作系统可以在物理CPU核心上透明地调度多个线程,以充分利用有限的CPU资源,这也是虚拟化的一种形式。此外,操作系统还将多个不同的文件系统整合到一个虚拟名称空间中,使用户能够方便地管理和访问文件。
3. I/O子系统中的虚拟化
虚拟化还广泛应用于I/O子系统,包括磁盘和磁盘控制器。在这个领域,虚拟化的概念涉及到块级别的数据访问。例如,RAID(冗余独立磁盘阵列)控制器和存储阵列使用虚拟化来将多个虚拟磁盘的抽象呈现给操作系统,同时将它们实际上作为物理磁盘进行处理。这种虚拟化技术有助于提供数据冗余和性能优化。类似地,现代固态硬盘(SSD)中的Flash转换层在I/O子系统中提供了磨损均衡,并将SSD呈现给操作系统,就像它是一个传统的机械硬盘一样。这种虚拟化层使操作系统能够有效地管理SSD的使用,并提供更高的性能和耐久性。
1. 多路复用(Multiplexing) 多路复用是一种技术,它允许多个虚拟实体共享同一资源。这可以在空间和时间两个方面实现。在空间多路复用中,物理资源(如内存页)被划分为多个虚拟实体,以便多个应用程序可以同时访问它们。在时间多路复用中,相同的物理资源会在不同虚拟实体之间进行临时调度,使它们能够轮流使用资源。例如,操作系统调度程序将 CPU 核心和硬件线程在一组可运行进程之间进行多路复用。上下文切换操作将处理器的寄存器文件保存在与传出进程关联的内存中,然后从与传入进程关联的内存位置恢复寄存器文件的状态。例如,操作系统跨不同的地址空间多路复用不同的物理内存页。为了实现这个目标,操作系统管理虚拟到物理的映射,并依赖于 MMU 提供的架构支持。
2. 聚合(Aggregation) 聚合是指将多个物理资源组合成一个单一的抽象资源。这意味着多个物理资源被汇总在一起,以形成一个更大或更复杂的资源。例如,RAID(冗余独立磁盘阵列)控制器将多个硬盘聚合成一个卷,从而提供了冗余和性能优化。聚合后,所有对卷的读写操作都会被适当地反映在底层的物理磁盘上。
3. 模拟(Emulation) 模拟是通过软件中的间接级别来模仿物理资源或设备的技术。这意味着虚拟资源或设备可以在当前计算机系统中并不存在的情况下被模拟出来。例如,跨架构模拟器可以在一种处理器架构上模拟另一种处理器架构,以实现向下兼容性。这种技术使得在不同的硬件或软件环境中运行应用程序成为可能。
多路复用、聚合和模拟三种技术通常可以自然地结合在一起,形成一个完整的虚拟化执行堆栈。例如,在虚拟机监视器(VM)中,多路复用和模拟通常一起使用。VM可以多路复用多个虚拟机实例在同一物理服务器上运行,同时通过模拟提供对虚拟资源的访问,使虚拟机实例似乎拥有自己的独立资源。
1.2 虚拟机
虚拟机是一个完整的计算环境,具有独立的处理能力、内存和通信通道。
基于语言的虚拟机,例如 Java 虚拟机、微软通用语言运行库、嵌入浏览器的 Javascript 引擎,以及一般来说任何托管语言的运行时环境。这些运行时环境关注于运行单个应用程序,并不在本书的范围之内;
轻量级虚拟机,它依赖于硬件和软件隔离机制的组合,以确保直接在处理器上运行的应用程序(例如,作为本地 x86代码)与其他沙箱和底层操作系统安全隔离。Is 包括以服务器为中心的系统,如 Denali [182] ,以及以桌面为中心的系统,如 Google Native Client [190]和 Vx32[71]。
- Vx32:类似于Google Native Client的系统,旨在提供桌面系统中的本机代码执行的安全性和隔离。
- Google Native Client:这是一个用于桌面系统的技术,允许在Web浏览器中运行本机代码,同时提供了安全性隔离,以防止恶意代码对系统造成损害。
基于 Linux 容器的解决方案,如 Docker [129]或相应的 FreeBSD Jail [110] ,也属于同一类别。
系统级虚拟机,其中独立的计算环境类似于计算机的硬件,因此虚拟机可以运行一个标准的、商品化的操作系统及其应用程序,与其他虚拟机和环境的其他部分完全隔离。这种虚拟机将虚拟化原理应用于整个计算机系统。每个虚拟机都有自己的底层硬件副本,或者至少有自己的底层硬件副本。每个虚拟机都运行自己独立的操作系统实例,称为来宾操作系统。是这本书的重点。
图1.2还对运行系统级虚拟机的各种平台进行了分类。我们将这些平台称为系统管理程序或机器模拟器,具体取决于运行虚拟机所使用的技术:
系统管理程序依赖于 CPU 上的直接执行以获得最大效率,理想情况下可以完全消除性能开销。在直接执行时,系统管理程序设置硬件环境,然后让虚拟机指令直接在处理器上执行。由于这些指令序列必须在虚拟机的抽象中操作,它们的执行会导致陷阱,虚拟机监控程序必须模拟这些陷阱。
机器模拟器通常作为一个普通的用户级应用程序来实现,目标是提供一个精确的虚拟化架构模拟,并且经常以本机速度的一小部分运行,从5x减速到1000x,这取决于模拟细节的级别。机器模拟器在计算机体系结构中起着基础性的作用,它允许对复杂的工作负载进行详细的体系结构研究。
1.3 系统管理程序
系统管理程序是一种特殊形式的系统软件,它运行虚拟机,以最小化执行开销为目标。当多个虚拟机在同一计算机系统上同时共存时,虚拟机监控程序会在虚拟机之间适当地分配和调度物理资源。Popek 和 Goldberg 在1974年正式确定了虚拟机和 hypervisor (他们称之为 VMM)之间的关系,如下所示[143]。
虚拟机被认为是真实机器的有效的、孤立的复制品。我们通过虚拟机监视器(VMM)的概念来解释这些概念。作为软件的一部分,VMM 具有三个基本特征。首先,VMM 为程序提供了一个与原始机器基本相同的环境; 其次,在这个环境中运行的程序在最坏的情况下只显示速度的轻微下降; 最后,VMM 完全控制系统资源。
Popek 和 Goldberg 的定义与更广泛的虚拟化定义是一致的: 管理程序将分层原则应用于计算机,并具有三个特定的标准: 等价性、安全性和性能。
等价性: 复制确保公开的资源(即虚拟机)与底层计算机等价。是一个强大的需求,当体系结构需要它时,它在某种程度上被放宽了(见1.7)。
复制(Duplication):在虚拟化中,复制指的是虚拟机与底层物理计算机之间的等效性。这意味着虚拟机应该在操作和性能方面表现得像底层物理计算机一样,以便应用程序不需要关心它们是否在虚拟环境中运行。这种等效性是虚拟化技术的一个核心目标。
安全性: 隔离要求虚拟机彼此之间以及虚拟机监控程序之间都是隔离的,这强制了系统的模块性。至关重要的是,系统管理程序强制执行设计的安全性,而不对运行在虚拟机(包括客户操作系统)中的软件做任何假设。
性能: 最后,也是最关键的一点,Popek 和 Goldberg 的定义增加了一个额外的要求: 虚拟系统必须在最坏的情况下显示速度的轻微下降。是将虚拟机监控程序与机器模拟器分离的最终要求。尽管机器模拟器也满足复制和隔离标准,但它们不符合效率标准,因为即使是使用动态二进制转换的快速机器模拟器[32,124,153,184]也将目标系统的执行速度至少减慢了5x,这在很大程度上是因为在软件中模拟虚拟机的 TLB 的成本很高。
1.4 TYPE-1 AND TYPE-2 HYPERVISORS
系统管理程序体系结构可以分为所谓的 type-1和 type-2。罗伯特 · 戈德堡在他的论文[77]中介绍了这些术语,自那以后这些术语一直被使用。非正式地,type-1管理程序直接控制物理计算机的所有资源。相比之下,type-2虚拟机监控程序可以“作为”现有主机操作系统的一部分或“在”现有主机操作系统的“上面”操作。遗憾的是,文献中对这些定义的应用比较松散,导致了一些混乱。Goldberg 的定义(使用更新的术语)如下:
- 虚拟化的一个基本要求,即虚拟机中的指令应该直接在主机硬件上执行,而不需要进行额外的解释或翻译。这有助于确保虚拟机的性能接近于运行在裸机上的原生应用程序。
- Type-1和Type-2 VMM:
- Type-1 VMM:这种类型的VMM运行在裸机上,直接与硬件交互,负责执行系统的调度和资源分配。Type-1 VMM通常需要更高的特权级别,并可能包含一些虚拟化不需要的额外代码,因为它要负责管理整个物理主机。在服务器虚拟化中常见,因为它们通常提供更好的性能和隔离,适用于云计算环境。
- Type-2 VMM:运行在主机操作系统下的虚拟机监视器。虚拟机的资源分配和环境创建更清晰地与主机操作系统分离。主机操作系统负责正常的系统资源分配,而Type-2 VMM提供标准的扩展虚拟机环境。通常用于桌面虚拟化,因为它们更容易设置和管理,但性能通常较差。包括VMware Workstation、KVM、VirtualBox等
我们注意到,重点在于资源分配,而不是管理程序是以特权模式还是非特权模式运行。特别是,即使系统管理程序以内核模式运行,例如 Linux/kVM 和 VMware Workstation ,它也可以是 type-2。事实上,Goldberg 假设系统管理程序总是使用管理程序特权执行。这两种类型在当前系统中都很常见。首先,VMware ESX Server [177]、 Xen [27,146]和微软Hyper-V都是 type-1 hypervisor。即使 Xen 和 Hyper-V 依赖于一个名为 dom0的主机环境,系统管理程序本身也会做出资源分配和调度决策。VMware Workstation、 VMware Fusion、 KVM、 Microsoft Virtu-alPC、 Parallels 和 Oracle VirtualBox 都是 type-2 hypervisor。它们与主机操作系统协作,以便主机操作系统调度所有系统资源。主机操作系统像调度进程一样调度系统管理程序,即使这些系统都依赖于大量的内核模式组件来执行虚拟机。一些系统管理程序,如 VMware Workstation 和 Oracle VirtualBox,可以在不同的主机操作系统上移植,而 Fusion 和 Parallels 可以在 Mac OS X 主机操作系统上运行,Microsoft Virtual PC 可以在 Windows 主机操作系统上运行,而 KVM 可以作为 Linux 主机操作系统的一部分运行。在这些 type-2系统中,KVM 提供了与主机操作系统的最佳集成,因为系统管理程序的内核模式组件作为内核模块直接集成在 Linux 主机中。
关注点在于资源分配:在虚拟化中,关键问题之一是如何分配和管理物理主机上的资源,包括处理器时间、内存、存储等。这决定了虚拟机的性能和隔离程度。因此,虚拟化的一个主要焦点是资源分配策略,而不仅仅是管理程序运行在特权模式还是非特权模式。
一些虚拟化系统,如VMware ESX Server和Xen,即使依赖于主机环境(如dom0),也被认为是Type-1 Hypervisor,因为它们自己负责资源分配和调度
1.5 一个主机管理程序的概览: 多路复用与仿真
随着基本定义的建立,我们现在进入虚拟化计算机系统关键元素的第一个SKETCH,即虚拟机的规范和虚拟机监控程序的基本构造块。
图1.3说明了虚拟化计算机系统的关键架构组件。图中显示了三个虚拟机,每个虚拟机都有自己的虚拟硬件、自己的客户操作系统和自己的应用程序。
系统管理程序控制实际的物理资源并直接在硬件上运行。在这种简化的体系结构中,硬件(虚拟或物理)由处理元素组成,这些处理元素包括一个或多个 CPU、它们的 MMU 和缓存一致性内存。电子处理元件连接到一个 I/O 总线,带有两个附加的 I/O 设备: 一个磁盘和一个网络接口卡,如图1.3所示。是服务器部署的代表。桌面平台将包括额外的设备,如键盘、视频、鼠标、串行端口、 USB 端口等。移动平台可能还需要 GPS、加速度计和无线电。在其最基本的形式中,虚拟机监控程序使用1.1的三种关键虚拟化技术中的两种: 它(在空间中,可能在时间中)跨虚拟机多路复用物理 PE,并且它模拟其他所有东西,特别是 I/O 总线和 I/O 设备。技术的结合在实践中既是必要的,也是充分的,以达到效率标准。这是必要的,因为如果没有有效的机制来复用 CPU 和 MMU,系统管理程序将不得不模拟虚拟机的执行。事实上,机器模拟器和 hypervisor 之间的主要区别在于,前者模拟虚拟机的指令集架构,而后者复用虚拟机。CPU 的多路复用是一个调度任务,非常类似于操作系统执行的调度进程的任务。调度实体(这里是 hypervisor)设置硬件环境(注册文件等) ,然后让被调度实体(虚拟机)直接在硬件上运行,减少权限。
这种调度技术称为直接执行,因为管理程序允许虚拟 CPU 直接执行实际处理器上的指令。当然,系统管理程序还负责确保虚拟机的安全性。因此,它确保虚拟 CPU 总是以减少的权限执行,例如,这样它就不能执行有特权的结构体。因此,每当来宾操作系统试图执行一条特权指令时,虚拟机的直接执行就会导致频繁的陷阱,系统管理程序必须模拟这条特权指令。因此,围绕直接执行而设计的 hypervisor 遵循陷阱-模拟编程范型,其中大部分执行开销是由虚拟机的 hypervisor 模拟陷阱引起的。物理内存在虚拟机之间也是多路复用的,因此每个虚拟机都有一个连续的、固定大小的物理内存量的错觉。类似于操作系统在进程之间的分配。构建虚拟机监控程序的独特挑战在于 MMU 的虚拟化,以及向虚拟机公开用户级和内核级执行环境的能力。复用和仿真的结合也是足够的,因为当今计算机系统的 I/O 操作是通过合理的高级操作来实现的,例如,设备驱动程序可以发出简单的命令来发送描述符环中指定的网络数据包列表,或者发出32KB 的磁盘请求。虚拟机监控程序模拟每个类别至少一个代表性设备的硬件/软件接口,即一个磁盘设备、一个网络设备、一个屏幕设备等。作为此仿真的一部分,系统管理程序使用可用的物理设备来发出实际的 I/O。
长期以来,I/O 仿真一直是 I/O 设备虚拟化的首选方法,因为它具有可移植性优势: 即使在具有不同硬件设备的平台上运行时,虚拟机也能“看到”相同的虚拟硬件。今天,现代硬件包括对 I/O 虚拟化的高级架构支持,它能够实现某些类别的 I/O 设备的多路复用,在吞吐量和延迟方面具有显著的性能优势,但仍然以降低虚拟机的移动性和可移植性为代价。
表1.1提供了一个在2000年发布的早期面向桌面的 hypervisor VMware Workstation 2.0中多路复用和仿真结合使用的具体例子。显然,硬件已经过时了: USB 显然不见了,而且大多数读者从未见过真正的软盘。然而,这些概念仍然是相同的。对于每个组件,表1.1描述了前端设备抽象(可视为虚拟机的硬件)以及用于实现它的后端仿真机制。当资源是多路复用的,如 x86 CPU 或内存时,前端和后端是相同的,并由硬件定义。系统管理程序仅用于建立虚拟资源和物理资源之间的映射,然后硬件可以直接使用这些资源,而不需要进一步拦截
然而,当资源被模拟时,前端设备对应于该设备的一个经典选择的代表,独立于后端。系统管理程序同时实现前端和后端,通常在没有任何特定硬件支持的软件中实现。前端实际上是所选择的、有代表性的设备的软件模型。后端仿真在底层资源中选择实现功能。这些底层资源可能是物理设备或主机操作系统的某些更高级别的抽象。例如,VMware Workstation 中的磁盘前端要么是 IDE,要么是 Buslogic SCSI 设备,这是当时流行的选择,其设备驱动程序无处不在。后端资源可以是物理设备,即实际的原始磁盘,也可以是存储为现有文件系统中的大文件的虚拟磁盘。尽管不再是底层硬件的精确副本,虚拟机仍然是兼容的。假设可以在客户操作系统中加载一组不同的设备驱动程序,那么虚拟机将具有相同的功能。到目前为止,这个系统管理程序示意图假定处理元素的各个组件可以虚拟化。然而,我们还提到了一些硬件架构未能为虚拟化提供硬件支持的历史事实。是讨论将是第二章和第三章的核心。
1.6 NAMES FOR MEMORY
在早期的虚拟化论文中,有些文献使用了不同的术语,特别是”物理内存”和”机器内存”,来分别指代客户-物理内存和主机-物理内存,以区分虚拟机内存和实际物理内存。这种术语的使用可能会导致混淆,因此为了明确起见,通常使用”客户物理内存”和”主机物理内存”这样的术语。
- 管理程序:在虚拟化计算机系统中,管理程序是指虚拟机监视器(VMM)或超级管理程序,它负责管理虚拟机的资源,包括内存、处理器、存储等。
- 虚拟物理内存:这个术语指的是虚拟机可见的抽象内存,即虚拟机认为自己可以使用的内存量。然而,为了避免与虚拟内存(虚拟内存是一种操作系统技术,允许将物理内存扩展到磁盘上)混淆,通常将虚拟机可见的抽象内存称为”客户物理内存”。
- 客户物理内存:这个术语用来表示虚拟机可见的抽象内存。虚拟机认为它可以自由使用客户物理内存,而不需要考虑底层的物理硬件细节。
- 主机物理内存:这个术语用来表示底层实际的物理内存资源,也就是虚拟机实际运行在的物理硬件上的内存。主机物理内存是管理程序(VMM)负责管理的资源。
1.7 虚拟化和半虚拟化(PARAVIRTUALIZATION)的方法
- 全(软件)虚拟化(Full Software Virtualization):
- 这种方法指的是针对不具备完全虚拟化支持的体系结构,旨在最大程度地提高硬件兼容性的虚拟化监视器(hypervisors)。它特别支持在未经修改的操作系统上运行,包括VMware早期版本的虚拟化监视器。
- 有时在文献中也称为”软件虚拟化”。这种方法需要在执行前(至少有时)将虚拟机指令序列进行翻译。详细描述在§3.2中。
- 硬件虚拟化(Hardware Virtualization,HVM):
- 这种方法是为具有对虚拟化提供体系结构支持的体系结构构建的虚拟化监视器,包括所有最近的处理器。这样的虚拟化监视器还支持未经修改的客户操作系统。
- 与软件虚拟化方法不同,HVM虚拟化监视器完全依赖于直接执行虚拟机指令,而不需要翻译。在文献中,HVM有时被称为HV。详细的要求在第2章中得到正式定义。
- 架构和虚拟化监视器的示例在第4章中讨论x86体系结构的VT-x和第7章中讨论ARM体系结构的虚拟化扩展。
- 半虚拟化(Paravirtualization):
- 这种方法进行了不同的权衡,更注重简单性和整体效率,而不是与底层硬件的完全兼容性。这个术语最早由Denali引入,后来由早期的Xen虚拟化监视器广泛使用。
- 在没有虚拟化支持的平台上使用时,半虚拟化要求更改客户操作系统的二进制代码,以与底层硬件兼容。而在具备完全虚拟化支持的体系结构上,半虚拟化仍然用于通过平台特定的扩展来增强HVM,通常在设备驱动程序中实现,例如协同管理内存或实现高性能的前端设备。
- 这将在§3.3中详细描述。
1.8 使用虚拟机的好处
虚拟机最初是在大型机上发明的,当时硬件稀缺而且非常昂贵,操作系统还很原始
好处:
- 操作系统多样性:虚拟化允许不同的操作系统同时运行在一台计算机上。这是许多桌面导向的Type-2虚拟化监视器的主要原因之一。例如,它使得在Mac OS 上使用虚拟机(如Fusion和Parallels)运行Windows(有时还包括Linux)成为可能。
- 服务器整合:今天,企业IT最佳实践仍然要求每台服务器每次运行一个应用程序。随着硬件性能和效率的提高,一台物理机通常更多地成为一个虚拟机。这种服务器整合可以节省硬件成本和数据中心资源。
- 快速部署:部署物理服务器是一个复杂而耗时的任务。相比之下,可以通过门户或API完全在软件中创建虚拟机,并部署软件堆栈作为虚拟设备。这种快速部署有助于提高效率和灵活性。
- 安全性:虚拟化引入了数据中心堆栈中的新管理层,与客户操作系统不同且不可见,但能够审查操作系统的行为、执行入侵分析、或验证其来源。虚拟化监视器还可以控制虚拟机的所有I/O操作,从而轻松插入虚拟机特定的防火墙或连接到虚拟网络,增强安全性。
- 高可用性:虚拟机是一个封装的抽象,可以在任何运行兼容虚拟化监视器的服务器上运行。因此,虚拟机可以在硬件崩溃后在新服务器上重新启动,无需进行操作系统级的配置或感知,从而提供了高可用性解决方案。
- 分布式资源调度:使用实时迁移技术,一组虚拟化监视器可以变成一个单一的资源池,允许在集群中自动和透明地重新平衡虚拟机。这提高了资源的利用效率。
- 云计算:在虚拟化环境中,不同的客户(租户)可以在彼此隔离的环境中运行自己的虚拟机。当与网络虚拟化等技术结合使用时,这为云计算提供了基础,包括Amazon Web Services、Google Compute Engine和Microsoft Azure等云计算技术。
2 The Popek/Goldberg theorem
在1974年,Gerald Popek 和 Robert Goldberg 在ACM通信杂志上发表了一篇重要的论文,名为《第三代体系结构的虚拟化的形式要求》,该论文定义了确保虚拟机管理器(VMM)可以构建的必要和充分的形式要求 [143]。准确地说,他们的定理确定了一个给定的指令集架构(ISA)是否可以通过使用多路复用来虚拟化,使其可以在虚拟机内运行。对于满足定理假设的任何体系结构,任何直接运行在硬件上的操作系统也可以在虚拟机内运行,无需进行修改。当时,这项工作的动机是要解决新体系结构意外地阻止了VMM构建的证据。作者在文章中引用了DEC PDP-10,其中看似随机的架构决策“破坏”了虚拟化。尽管结果很简单,但这个定理的相关性在几十年内都被计算机架构师忽视了,一代又一代的新处理器架构都没有考虑这个定理的技术要求。直到后来,随着虚拟机再次变得重要,英特尔和AMD明确确保他们的虚拟化扩展满足Popek和Goldberg的标准,以便未经修改的客户操作系统可以直接在虚拟机中运行,而无需使用软件翻译或半虚拟化 [171]。今天,这个定理仍然是理解计算机体系结构与其支持虚拟机的能力之间的基本关系的明显起点。具体来说,这个定理确定了一个仅依赖于直接执行的VMM是否能够支持客户任意操作系统。
1.1 模型
略去数学证明
尽管存在理想化,特别是由于缺乏寄存器,但这种体系结构模型与我们今天都熟悉的模型基本没有根本区别。对于这种体系结构,Popek和Goldberg提出了以下形式化研究问题。…假设存在一个符合这一基本体系结构模型的计算机,在哪些明确条件下可以构建一个VMM,以便VMM:
- 能够执行一个或多个虚拟机;
- 在任何时候都完全控制计算机;
- 支持针对相同体系结构设计的任意、未经修改的、潜在恶意的操作系统;以及
- 具备高效性,最坏情况下只会导致速度略微下降?
这个问题的答案决定了是否可以为特定的体系结构构建VMM,以便生成的“虚拟机可以是实际机器的高效、隔离的副本”。当满足条件时,定理必须确保符合以下三个标准:
- 等效性:虚拟机与底层处理器基本相同,即计算机体系结构的副本。在虚拟机内运行的任何程序,即任何客户操作系统和应用程序组合,应该表现出与该程序直接在底层硬件上运行时相同的行为,除了可能由于时间依赖性或资源可用性(例如物理内存量)等差异。
- 安全性:VMM必须始终完全控制硬件,而不做任何关于虚拟机内运行的软件的假设。虚拟机与底层硬件隔离,运行方式就好像它在一个独立的计算机上运行一样。此外,不同的虚拟机必须相互隔离。该定理及其证明关注第一个安全性属性。第二个属性可以通过确保在两个虚拟机之间没有共享状态来实现。
- 性能:效率要求意味着在虚拟化环境中运行的程序的执行速度最多只会比直接在底层硬件上运行时的执行时间略微下降。
2.2 定理
定理1 [143]:对于任何传统的第三代计算机,如果该计算机的敏感指令集是特权指令集的子集,那么可以构建一个虚拟机监视器
2.3 递归虚拟化和混合虚拟机
该论文包括另外两个定理,分别涵盖递归虚拟化和混合虚拟机的要求。这个形式主义也适用于递归虚拟化的推理。事实上,满足定理条件的理想化体系结构也可以支持递归虚拟机。在这种情况下,虚拟机可以在客户-监管模式下运行一个VMM,而不是一个操作系统。这个VMM当然可以运行多个虚拟机,它们的客户操作系统以及它们各自的应用程序。该论文的第二个定理规范化了对VMM(而不是体系结构)提出的最少的附加假设集,以支持递归虚拟机。在递归虚拟化中,VMM本身必须在虚拟机内(未修改地)运行。近年来,递归虚拟化的研究兴趣有所增加 [33, 191]。
该论文还引入了混合虚拟机的概念,讨论了在特定情况下不满足标准的情况。再次考虑PDP-10的JRST 1指令,该指令可用于从监管模式返回到用户模式,同样也可用于在已经处于用户模式时返回子程序。显然,这是灾难性的,因为虚拟机将在没有必要进入VMM的情况下在客户操作系统和应用程序之间切换。如果发生这种情况,虚拟机将在VMM认为它仍然处于客户-监管模式时执行应用程序代码,尽管这显然是不可接受的。但对于这个特定指令来说,关键观察是,只有当虚拟机处于监管模式时,它才是控制敏感的。当虚拟机处于用户模式时,该特定指令对虚拟化不敏感。形式上来说,让我们定义一个指令在用户模式下是用户敏感的,如果它在监管模式下是行为敏感的或者控制敏感的,但在用户模式下不敏感。该定理陈述如下。
定理3[143] : 可以为任何传统的第三代机器构造混合虚拟机监视器,其中用户敏感指令集是特权指令集的子集。
定理3要比定理1弱得多,由此产生的混合虚拟机监视器与传统的VMM相当不同。具体来说,混合虚拟机监视器可以绕过这些限制:
- 当虚拟机运行应用程序时,即处于客户-用户模式时,混合VMM的行为类似于普通的VMM。
- 在虚拟机-监管模式下,混合VMM不使用直接执行,而是以100%的软件方式解释所有指令。换句话说,它解释了通过客户操作系统的所有路径的执行。尽管解释会产生很高的开销,但只要在相关工作负载中在操作系统中花费的时间部分较小,这种方法可能是可行的。第三个定理在早期识别了一个关键的细微差别,在后来半虚拟化和混合解决方案出现时,这个差别将发挥至关重要的作用,这些解决方案将直接执行与二进制翻译相结合。这些将在后面的章节中讨论。
2.4 讨论: 用分页代替分段
1974年的原始论文认识到虚拟内存是任何类型的虚拟化支持的基本前提。
为了简化起见,作者假设虚拟内存是通过单个重定位绑定的寄存器对来实现的。现在,考虑一个更现实的模型,其中虚拟内存是通过分页而不是分段来实现的。如果使用分页来实现虚拟内存,定理是否仍然适用?简短的答案是是,但深入分析会发现在第三代模型中不存在的细微差别,这些差别在构建实际系统时将发挥实际作用。特别是,访问内存的指令可能是位置敏感的,因为VMM也必须存在于内存的某个位置。此外,尽管客户操作系统以非特权方式运行,但必须确保它不受应用程序的影响。在x86体系结构中,这两个问题将在§4.1中重新讨论,分别作为地址空间压缩和特权级别压缩的挑战。
最后,VMM必须通过将客户操作系统指定的页表映射与客户物理内存和主机物理内存之间的映射组合来构建虚拟地址空间。在理想化的分段体系结构中,这只是一个简单的组合问题,但在基于页面的系统中要复杂得多。实际上,解决这个问题有两种常用的方法,分别称为影子页表(见§3.2.5)和扩展页表(见§5.1)。
2.5 WELL-KNOWN VIOLATIONS
在他们的文章中,Popek和Goldberg以DEC PDP-10为例,说明了一种未能满足虚拟化要求的架构。现在让我们列举一些现代时代已知的违规行为。这个违规行为的列表不是详尽无遗的。对于讨论的体系结构,我们侧重于那些在限制不具备完整性声称的处理器架构的部署方面产生实际影响的违规行为。我们确实识别了一些违规行为的模式:
- 直接访问物理内存:某些体系结构决策直接将物理内存暴露到虚拟内存的硬编码部分。这在MIPS体系结构中特别明显(见§2.5.1)。
- 位置敏感指令:其他决策通过非特权指令公开了某些敏感数据结构在内存中的位置(例如,x86中的全局描述符表,见§2.5.2)。
- 控制敏感违规行为:当ISA明确为相同指令定义了不同的语义,取决于特权级别时,就会发生这种情况(例如,x86上的iret、popf,见§2.5.2)。
2.5.1 MIPS
MIPS体系结构是一种经典的RISC(精简指令集计算机)ISA。它有三种执行模式:内核模式(最高特权级别)、监管模式和用户模式(最低特权级别)。只有内核模式可以执行特权指令。监管模式实际上是用户模式的一种备用形式,其明显好处在于它可以访问对常规用户模式不可用的虚拟内存部分。
首先,好消息是:这是一个非常有前途的高效虚拟机监视器设计的起点。事实上,虚拟机监视器将在内核模式下运行,并且可以简单地在监管模式下运行客户操作系统,根据其原始语义模拟所有特权指令。监管模式的可用性导致了一个重要的优化:由于监管虚拟内存受到应用程序的保护,虚拟机监视器可以简单地将客户操作系统和应用程序之间的转换视为监管模式和用户模式之间的转换。这种转换不需要更改虚拟内存的配置,也不需要刷新TLB(转译后备缓存)。
在MIPS体系结构中,虚拟内存被划分为多个固定大小的区域,每个区域都具有硬编码的属性,确定了需要访问该区域的执行模式,如何通过TLB(映射)或通过硬编码的掩码(未映射)来重新定位地址,以及是否应该通过高速缓存层次结构进行访问。表2.1显示了32位体系结构的这些区域。64位扩展创建了具有相同一般原理的额外的固定大小区域。
现在是坏消息:MIPS体系结构不具备虚拟化能力,首要原因是它使用了区域。事实上,区域的使用是位置敏感的(一种行为敏感),因为它是处理器特权级别的函数。这具有严重的后果,因为任何期望在内核模式下运行的操作系统都将被编译以使用KSEG0和KSEG1段。如果虚拟机监视器尝试在监管模式或用户模式下运行该操作系统,每个内存加载/存储指令都会导致陷阱,从而违反了虚拟化的效率标准。由于操作系统通常不会编译或链接为可执行位置无关的代码,因此虚拟化MIPS体系结构至少需要重新编译完整的客户操作系统内核。我们在§3.1中讨论了Disco,一款基于MIPS的虚拟机监视器的实现所需的体系结构和妥协。
2.5.2 X86-32
Intel x86-32体系结构是一个以复杂著称的CISC体系结构,部分原因是它包括对多十年的向后兼容性的传统支持。多年来,该体系结构引入了四种主要操作模式(实模式、保护模式、v8086模式和系统管理模式),每种模式以不同的方式启用了硬件的分段模型、分页机制、四个保护环以及安全功能(如调用门)。
x86-32体系结构不具备虚拟化能力。它包含了虚拟化敏感但非特权的指令,这违反了Popek和Goldberg对严格虚拟化的标准 [143]。这排除了传统的陷阱和模拟方法来进行虚拟化。
具体来说,Robin和Irvine识别出了17个敏感但非特权的问题指令 [151]。表2.2将这些指令分成了5类:操作中断标志的指令、操作段寄存器和段描述符的指令、可以查看系统数据结构位置的指令,以及与调用门相关的指令。其影响非常严重,而且广泛知晓。实际上,在引入VMware之前,Intel Corporation的工程师们确信他们的处理器在任何实际意义上都无法虚拟化 [74]。
x86-32体系结构确实提供了一个符合Popek/Goldberg定理的严格可虚拟化的执行模式:v8086模式专门设计用于运行16位虚拟机,运行16位的Intel 8086 ISA。这个模式对于Windows 95/98至关重要,因为它允许这些32位操作系统运行传统的MS-DOS程序。不幸的是,该模式只能执行16位的虚拟机。
2.5.3 ARM
ARM架构是一种RISC ISA。从虚拟化的角度来看,ARM可以看作具有两种主要的执行模式,一种或多种特权模式和用户模式。只有特权模式才能执行特权指令。例如,ARMv6具有7种处理器模式,用户模式和6种特权模式,而ARMv8实际上只有用户模式和单一特权模式,尽管ARMv8的术语将它们称为异常级别。每种模式都有一些分组寄存器,这意味着,例如,寄存器13在每种模式下指向不同的物理寄存器。特权模式之间的区别仅涉及分组寄存器,对于本讨论的目的可以忽略不计。
ARM架构不具备虚拟化能力。它包含了虚拟化敏感但非特权的指令,违反了Popek和Goldberg对严格虚拟化的标准。Dall和Nieh识别出了24个敏感但非特权的问题指令。这些特定指令是针对ARMv6识别的,但也存在于架构的其他版本中。例如,ARMv7在这方面与ARMv6非常相似。甚至在最新版本的ARM架构ARMv8中,也存在类似的问题指令。这些指令涉及用户模式寄存器、状态寄存器以及依赖于CPU模式的内存访问,如表中所列。
有各种加载/存储多个指令,当处于特权模式时访问用户模式寄存器。然而,这些指令在ARM架构中被定义为在用户模式下执行时是不可预测的,这意味着不能依赖指令的结果。例如,一个常见的不可预测指令的实现是,处理器会忽略它而不会引发陷阱。
状态寄存器指令涉及到特殊的ARM状态寄存器,即当前程序状态寄存器(CPSR)和保存程序状态寄存器(SPSR)。CPSR指定了CPU的当前模式和其他状态信息,其中一些是特权的。SPSR是在多种特权模式下可用的分组寄存器。基本的ARM保护机制通过在处理器通过异常进入相应特权模式时将CPSR复制到SPSR来工作,以便可以确定异常发生时的CPU状态信息。类似地,SPSR在各种时候复制到CPSR,以更新CPU的模式作为首选的异常返回机制。这些指令存在三种问题。首先,MRS可以在任何模式下用于读取CPSR并确定CPU模式,尽管在ARMv8中不再如此。其次,CPS用于写入CPSR并更改CPU模式,但在用户模式下执行时会被忽略。第三,当在用户模式下执行时,其他状态寄存器指令是不可预测的。例如,LDM(3)在特权模式下执行时将SPSR复制到CPSR作为指令的一部分,但在用户模式下执行时是不可预测的。
几乎所有的数据处理指令都有一个特殊版本,除了其ALU操作外,还将CPSR替换为SPSR。这些指令以在异常返回时更改CPU模式的方式设计,例如,可以从内核跳转到用户空间并同时更改模式。这些指令在用户模式下执行时是不可预测的。
内存访问指令使用不同于执行模式的CPU模式来访问内存。ARM处理器上的虚拟内存系统使用访问权限来限制根据CPU模式对内存的访问。尽管CPU处于特权模式,架构定义了四个名为”带翻译的加载/存储”的指令,这些指令使用用户模式的访问权限来访问内存,因此会由于内存访问违规而触发陷阱。然而,当在用户模式下执行时,这些指令的行为就像常规的内存访问指令一样。例如,当作为虚拟机的一部分以用户模式运行操作系统内核时,这些指令的语义与在内核模式下运行操作系统时不同,因此操作系统可能看不到它所期望的内存访问故障。
2.6 FURTHER READING
Popek和Goldberg的论文中的证明是针对一个理想化的单处理器、第三代架构以及单一虚拟内存段的。当然,当前的架构要复杂得多:它们支持具有缓存一致性和明确定义的细微内存一致性模型的多处理器架构。它们支持通过分页的虚拟内存,可以作为分段的替代或补充。更糟糕的是,细节中包含问题,特别是在具有众多传统执行模式和指令的CISC架构上。
尽管存在这些注意事项,但构建性的证明提供了一种推理虚拟化的框架。控制敏感和行为敏感指令的分类对评估任何现代计算机架构都是有用的指南。鼓励有兴趣的读者详细阅读Popek和Goldberg的经典论文,以便他们可以推理已知的架构[143]。
x86-32的限制已经被Robin和Irvine记录下来[151],然而,该论文错误地得出结论,因此无法构建实际且安全的虚拟化解决方案。第4章描述了Intel后来如何依赖Popek和Goldberg的定理作为框架,在x86-64处理器中添加了虚拟化支持[171]。
ARMv6和早期的ARM架构的限制已经被Dall和Nieh记录下来[59]。第7章描述了ARM后来如何在ARMv7处理器中添加了虚拟化支持[39],以及该架构如何与Popek和Goldberg的定理相关。
3 Virtualization without Architectural Support
这一章主要是关于过去的内容。那些只关心了解虚拟化在当代硬件和虚拟机监视器上运行方式的从业者可能会倾向于跳转到第四章。然而,过去对于任何需要了解我们是如何走到今天的情况的计算机科学家以及对早期开发的具体技术感兴趣的人仍然具有重要意义。
实际上,先前开发的许多软件技术在虚拟化之外也有应用。此外,阅读本章的计算机架构师将对在引入虚拟化硬件支持之前和之后所做的架构决策的意外后果有所了解。例如,VMware和Xen都依赖于32位架构上的分段来进行保护,正如本章所述。随着在x86-64上引入VT-x,分段不再需要构建虚拟机监视器,因此分段被移除。然而,分段还有其他应用,例如为轻量级虚拟机提供保护,如VX32 [71]和最初的Google原生客户端[190]。
MIPS、x86-32和ARM的Popek和Goldberg定理的违规行为在§2.5中已经描述过。由于这些违规行为的直接后果,没有任何虚拟机监视器——至少没有使用Popek和Goldberg所预期的陷阱和模拟与直接执行相结合的技术构建的虚拟机监视器——能够同时满足等效性、安全性和性能要求。
本章描述了三个系统——Disco、VMware Workstation和Xen——它们分别使用不同的技术或做出不同的权衡来解决定理的限制。表3.1提供了本章其余部分的导航。每个部分都作为一个案例研究,介绍了特定虚拟机监视器的显著特点,这些部分按照引入每个系统的时间顺序排序。
VMWARE WORKSTATION—FULL VIRTUALIZATION ON X86-32
VMware Workstation 1.0于1999年发布,是32位x86架构的第一个虚拟机监视器(VMM)[45, 46, 65, 162]。作为一款商业产品,VMware的愿景是创建一个在由x86-32 CPU构建的通用平台上有用的虚拟化层,并主要运行Microsoft Windows操作系统(即WinTel平台)。虚拟化的好处可以帮助解决WinTel平台已知的一些限制,如应用程序互操作性、操作系统迁移、可靠性和安全性。此外,虚拟化还可以轻松实现操作系统替代方案的共存,特别是Linux。
由于其专注于商业封闭源操作系统,x86计算环境与虚拟化需要采用新的方法,因为不像以前的Disco或稍后的Xen那样重新编译内核是一个选项。此外,x86行业具有分散的结构。不同的公司独立开发x86处理器、计算机、操作系统和应用程序。然后,系统集成商将这些组件组合成一个支持的“开箱即用”解决方案。对于x86平台,虚拟化需要在不改变平台的现有硬件或现有软件的情况下插入。因此,VMware Workstation的架构如下:
- 作为纯虚拟化解决方案,与现有未修改的客户操作系统兼容;
- 作为现有Linux和Windows主机操作系统的Type-2虚拟机监视器。
VMware Workstation将Popek和Goldberg的虚拟机的三个核心属性适应到基于x86的目标平台,具体如下:
- 等价性(Equivalence):正如在§2.5.2中讨论的,x86-32架构具有17个虚拟化敏感的非特权指令,这违反了Popek和Golberg关于严格虚拟化的标准[151]。这排除了传统的陷阱和模拟虚拟化方法。事实上,英特尔公司的工程师曾坚信他们的处理器无法以任何实际意义的方式进行虚拟化[74]。VMware的解决方案结合了直接执行(在可能的情况下,特别是用于运行应用程序)和动态二进制翻译(在需要时,特别是在运行客户操作系统时)。动态二进制翻译是一种高效的仿真形式;参见§3.2.2。
- 安全性(Safety):虚拟机监视器必须保证虚拟机的隔离,而不会对内部运行的软件做任何假设。VMware Workstation配置了硬件,特别是广泛使用分段截断来隔离虚拟机(参见§3.2.3)。为了简化测试并减少在未经测试的代码路径中引入安全漏洞的可能性,VMware Workstation仅支持x86-32架构的子集,以运行某些指定的支持客户操作系统。任何不受支持的请求,例如尝试在%cpl¹=1或%cpl=2处执行代码,这在任何支持的客户操作系统中都不会发生,将简单地中止执行。
- 性能(Performance):作为设计目标,VMware的目标是以接近本机速度运行相关工作负载,而在最坏的情况下,将它们运行在当时的处理器上,性能与在不具备虚拟化功能的前一代处理器上运行时相同。这样的性能水平将允许用户有效地使用虚拟机,就像使用真实机器一样。这基于这样一个观察,即大多数x86软件并不是设计仅在最新一代CPU上运行的。
为了实现这样的性能目标,尽管其为Type-2架构且存在主机操作系统,VMware Workstation在执行期间需要对硬件进行完全控制。§3.2.3描述了如何配置段表和CPU以减少动态二进制翻译的开销。§3.2.4描述了Type-2虚拟机监视器的模块化设计,分为三个不同的组件:主机操作系统的用户级进程、主机操作系统内的内核模块以及在硬件的完全控制下运行的VMware VMM。这是通过world switch实现的,它在与主机操作系统的共位情况下赋予了VMware VMM对硬件的完全控制权。最后,§3.2.5描述了影子分页,允许虚拟机(包括客户操作系统)直接使用底层硬件的内存管理单元(MMU)。
3.2.1 X86-32 FUNDAMENTALS
在深入讨论VMware如何虚拟化x86-32架构之前,让我们简要介绍该架构的关键方面:
- 执行模式:
- x86-32架构具有一种本机执行模式,称为“保护模式”,是现代操作系统和应用程序的主要运行模式。
- 此外,它包括三种非本机模式:实模式、系统管理模式和v8086模式,每种模式具有特定的用途。
- 在保护模式下,当前特权级别(cpl)用于区分内核执行(%cpl=0)和用户级执行(%cpl>0)。理论上有四个特权级别,但在实际中,用户级代码通常在%cpl=3下运行,中间特权级别不被使用。
- I/O特权级别(IOPL):
- 该架构引入了I/O特权级别(iopl),允许用户级代码启用或禁用中断,提供了控制不同特权级别中断处理的机制。
- 内存管理:
- x86-32架构采用分段和分页两种内存管理机制。
- 分段由六个段寄存器管理,如代码段(%cs)、堆栈段(%ss)、数据段(%ds)以及额外的段寄存器(%es、%fs、%gs)。每个段通过基地址和限制定义了32位线性地址的一部分。
- 分页用于将线性地址转换为物理地址。该架构规定了一个以特权级寄存器%cr3为根的三级页表结构。
- 当发生TLB(翻译高速缓冲)未命中时,处理器直接遍历页表结构并将映射插入TLB,无需软件干预。
了解x86-32架构的这些基本方面对于理解VMware如何针对该平台进行虚拟化至关重要。
3.2.2虚拟化 X86-32 CPU
为了虚拟可虚拟化的体系结构而构建的虚拟机监控程序(VMM)采用了由Popek和Goldberg描述的陷阱和仿真方法。这种技术也被称为直接执行(DE),因为虚拟机指令直接在处理器上执行。然而,由于其设计目标是在不可虚拟化的体系结构上运行未经修改的客户操作系统,单独的直接执行并不是一种策略。
另一种选择是采用全仿真方法。通过SimOS [153]机器模拟器的经验表明,使用诸如在用户级程序中运行的动态二进制翻译(DBT)等技术,可以将完全仿真的开销限制在5倍的减速。虽然对于机器模拟环境来说速度很快,但显然不足以满足我们的性能要求。
VMware解决这个问题的方法结合了两个关键见解。首先,虽然直接执行无法用于虚拟化整个x86体系结构,但它实际上可以在大多数情况下使用,特别是用于运行应用程序。第二个关键见解是通过正确配置硬件,特别是通过谨慎使用x86分段保护机制,可以使动态二进制翻译下的系统代码也能以接近本机速度运行;这将在§3.2.3下面讨论。
图3.1显示了动态确定是否可以使用直接执行还是必须使用动态二进制翻译的实际算法。该算法依赖于虚拟机的一些特定寄存器的状态,并导致以下决策:
- (行#1-#3):只有在保护模式和v8086模式下才会设置%cr0.pe。当虚拟机处于实模式或系统管理模式时,需要使用DBT。例如,BIOS在实模式下运行;
- (行#4-#6):x86 v8086模式始终可以直接使用DE来虚拟化v8086模式下的客户,例如,在Windows 95中运行MS-DOS仿真 [111];
- (行#7-#9):每当虚拟机可以控制中断标志时,都需要使用DBT。这个并集包括了当客户在内核模式执行(cpl=0)时的常见情况,但也包括了用户级进程可以通过禁用中断来建立关键区域的罕见情况(例如,在Linux上使用iopl(2)系统调用);
- (行#10-14):每当无法从内存中恢复六个段描述符寄存器中的任何一个的隐藏内容时,都需要使用DBT。这是一个有点隐晦的边缘情况,必须处理才能正确运行Windows 95;
- 在其余情况下可以使用DE,特别是在无法禁用中断时可以在用户级别使用。
如果我们排除这些边缘情况,算法使VMware VMM使用DBT来执行客户操作系统,使用DE来运行应用程序。这是一个好消息,因为内核代码的数量是有限的,并且大多数应用程序在用户级别花费了大部分执行时间。
这个算法有两个有趣的特性:(i)它不对未来可能执行的客户指令做任何假设,而是以O.1/的速度执行;(ii)通过谨慎编码虚拟处理器状态,可以用少数汇编指令来实现。
正式考虑:Popek和Goldberg在他们的混合虚拟机中讨论了类似的情况(参见§2.3和第3定理[143]),适用于所有用户敏感指令都是特权的体系结构。x86-32体系结构几乎属于这一类别:sgdt、sidt、sldt和smsw是唯一的非特权、用户敏感指令。幸运的是,即使是Intel的手册也将它们描述为可用但对应用程序没有用处 [102]。此外,用于隔离VMware VMM的段截断通过lsl指令可见于应用程序。VMware Workstation使用直接执行违反了等效性要求,但除了提供一种确定应用程序是否在虚拟化环境中运行的简便方法外,几乎没有实际后果。
3.2.3 VMWare VMM 及其二进制翻译器
VMware VMM通过巧妙地结合直接执行和动态二进制翻译技术,并利用分段和基于页的保护机制,实现了对x86架构的高效虚拟化。
虚拟机监控程序(VMM)的主要功能是虚拟化CPU和主内存。在其核心,VMware VMM将直接执行子系统与动态二进制翻译器结合在一起。简单来说,直接执行用于运行客户应用程序,而动态二进制翻译器用于运行客户操作系统。
动态二进制翻译(DBT)[84]是一种高效的仿真形式。DBT不是逐个解释指令,而是将一组指令编译成一个可执行代码片段,通常是一个基本块。代码存储在称为翻译缓存的大缓冲区中,以便可以多次重复使用。DBT具有一些众所周知的优化,例如链接,允许在编译的片段之间直接跳转[53]。VMware依赖于DBT,以便处理器不是执行或解释原始虚拟机指令,而是本地执行翻译缓存中相应的已翻译序列。
DBT的性能,特别是系统级DBT的性能,对硬件配置非常敏感。例如,Embra [184]由于MMU仿真的成本而减速了5倍。通常,DBT系统也在与它们模拟的软件相同的地址空间中运行。对于VMware来说,挑战在于确保VMware VMM可以与虚拟机安全共享一个地址空间,而且对虚拟机不可见,并且执行时的性能开销最小。鉴于x86架构支持基于分段和分页的保护机制,解决方案可以使用其中一个或两个机制。例如,使用平坦内存模型的操作系统只依赖于分页(而不是分段)来保护自己免受应用程序的影响。
VMware VMM只使用分段来进行保护。线性地址空间被静态划分为两个区域,一个用于虚拟机,一个用于VMM。虚拟机段由VMM截断,以确保它们不与VMM本身重叠。
图3.2说明了VMware使用段截断的方式,以一个采用平坦内存模型的客户操作系统为例。在%cpl=3运行的应用程序使用截断的段,而且还受到其自身操作系统的限制,以阻止它们使用页面保护来访问客户操作系统区域。
在通过二进制翻译运行客户内核代码时,硬件CPU位于%cpl=1。二进制翻译引入了一个新的特定挑战,因为已翻译的代码包含了各种指令。有些指令需要访问VMM区域(以访问支持VMM数据结构),而其他指令需要访问线性地址空间的虚拟机部分。解决方案是依赖于硬件保护,而不是运行时内存检查。具体来说,VMware VMM保留一个段寄存器%gs,始终指向VMM区域:由翻译器生成的指令使用<gs>
前缀来访问VMM区域,并且二进制翻译器在翻译时保证,不会有虚拟机指令直接使用<gs>
前缀。相反,已翻译的代码对于原始具有<fs
>或<gs>
前缀的虚拟机指令使用%fs。其余的三个段(%ss,%ds,%es)可以由虚拟机指令直接使用(在它们的截断版本中)。
当然,虚拟机指令可能有合法的,甚至是频繁的,原因要使用靠近线性地址空间顶部的地址,那里实际上是VMM所在的地方。正如预期的那样,段截断会为每个这样的引用触发通用保护故障,这可以由VMM来模拟。为了减少这种陷阱的数量,VMware Workstation依赖于自适应二进制翻译作为一种优化,以在运行时消除大多数通用保护故障。自适应二进制翻译是基于这样一个观念的,即客户内核中的相同几个位置几乎导致了所有这些通用保护故障。它会重新翻译基本块,并将原始引起故障的指令(引起陷阱的指令)替换为安全模拟内存访问而不引发陷阱的特定序列。
图3.2还说明了基于页的保护(pte.us)的作用。虽然没有用于保护VMM免受虚拟机的保护,但用于保护客户操作系统免受其应用程序的保护。解决方案很简单:实际页表中的pte.us标志与原始客户页表中的标志相同。在%cpl=3运行的客户应用程序代码受到硬件的限制,只能访问pte.us=1的页面。在%cpl=1下通过二进制翻译运行的客户内核代码不受此限制。
使用段截断来保护 VMware VMM [45]。在这个例子中,虚拟机的操作系统是为平面内存模型设计的。应用程序在用户级直接执行(cpl = 3)。 客户操作系统内核在转换缓存(TC)中的二进制转换下运行,cpl = 1
在通过二进制翻译运行客户内核代码时,硬件CPU处于%cpl=1。由于已翻译的代码包含了多种指令,其中一些需要访问VMM区域(以访问支持VMM数据结构),而其他一些需要访问线性地址空间中虚拟机的部分。
解决方案是依赖硬件保护机制,而不是运行时内存检查。具体来说,VMware VMM保留一个段寄存器%gs,始终指向VMM区域:由翻译器生成的指令使用<gs>
前缀来访问VMM区域,并且二进制翻译器在翻译时保证,不会有虚拟机指令直接使用<gs>
前缀。相反,已翻译的代码对于原始具有<fs>
或<gs>
前缀的虚拟机指令使用%fs。其余的三个段(%ss,%ds,%es)可以由虚拟机指令直接使用(在它们的截断版本中)。
然而,虚拟机指令可能会有合法的、甚至是频繁的理由,要使用接近线性地址空间顶部的地址,而实际上VMM位于那里。正如预期的那样,段截断会为每个这种引用触发通用保护故障,这可以由VMM来模拟。为了减少这种陷阱的数量,VMware Workstation依赖于自适应二进制翻译作为一种优化,以在运行时消除大多数通用保护故障。自适应二进制翻译的基本观念是,客户内核中的相同几个位置几乎导致了所有这些通用保护故障。它会重新翻译基本块,并将原始引起故障的指令(引起陷阱的指令)替换为安全模拟内存访问的特定序列,从而减少陷阱的触发。这有助于提高性能并减少运行时的开销。
图3.2还说明了基于页面的保护(pte.us)的作用。尽管不用于保护VMM免受虚拟机的影响,但它用于保护客户操作系统免受其应用程序的影响。解决方案很简单:实际页表中的pte.us标志与原始客户页表中的标志相同。在%cpl=3下运行的客户应用程序代码受到硬件的限制,只能访问pte.us=1的页面。在%cpl=1下通过二进制翻译运行的客户内核代码不受此限制。这种方式有助于确保客户操作系统受到保护免受其应用程序的恶意影响。
3.2.4主机操作系统的作用
VMware Workstation是一种类型-2的虚拟机监控程序,表面上运行在像Linux或Windows这样的主机操作系统之上,但实际上虚拟机监控程序,特别是VMware VMM,在执行虚拟机时完全控制CPU。
图3.3展示了VMware Workstation的托管架构,它由三个组件组成:(i) 一个用户级程序(VMX),负责与最终用户和主机操作系统的所有交互,特别是用于I/O仿真的目的;(ii) 安装在内核中的小型设备驱动程序(VMM驱动程序);和(iii) VMware VMM,它在与主机操作系统相同的级别上运行,但在一个不连贯的环境中运行,其中主机操作系统已被暂时挂起并从虚拟内存中删除。
//todo
3.6 FURTHER READING
原始的VMware Workstation虚拟化监控程序已在[45]中详细描述。其他论文则描述了VMware Workstation的关键方面,包括其I/O和网络性能[162]以及GPU的虚拟化[66]。自它们首次引入以来,VMware Workstation已经成熟,并成功地过渡到利用VT-x和AMD-v等新兴架构支持虚拟化的能力。
Agesen等人描述了VMware在虚拟化方面的演进过程(请参阅[4])。
类似地,Pratt等人描述了Xen的演进过程,特别是从使用硬件支持的半虚拟化到全虚拟化的过渡(在具有硬件支持的机器上)[146]。Chisnal于2007年编写的书籍《Xen Hypervisor的权威指南》提供了对Xen内部的详细描述,包括在采用硬件虚拟化之前和之后的情况[50]。
VMware ESX Server(现在称为vSphere)是一种商用的类型-1虚拟化监控程序。它与VMware Workstation共享相同的VMware VMM子系统,但作为类型-1虚拟化监控程序的一部分,直接调度I/O、CPU和内存资源[7, 82, 177],并且可以实现虚拟机的实时迁移[135]。我们特别推荐Waldspurger关于ESX内存管理的描述[177],这篇文章获得了ACM SIGOPS Hall of Fame奖项[2]。
KVM for ARM在[59]中有更详细的描述,而VMware的MVP在[28]中有描述。
曾经有一个放弃的Xen for ARM移植[91]需要对客户内核进行全面修改,但从未完全开发完成。这些半虚拟化方法都不能运行未经修改的客户操作系统。这些方法已被利用ARM硬件虚拟化支持的解决方案所取代,首次引入于ARMv7,将在第7章中讨论。
四 X86-64: 使用 VT-x 的 CPU 虚拟化
现在,我们将在三个章节中依次描述x86-64处理器中用于虚拟化的体系结构支持。这种体系结构支持是CPU(第4章)、MMU(第5章)和I/O子系统(第6章)创新的组合。本章描述了VT-x,即英特尔技术,用于虚拟化CPU本身。第4.1节首先描述了英特尔工程师在设计VT-x时提出的关键要求。第4.2节描述了CPU虚拟化的方法、根模式和非根模式的概念,以及体系结构与Popek和Goldberg定理的关系。第4.3节以KVM(Linux内核虚拟机)作为案例研究,介绍了如何构建一个专门设计为假定硬件支持虚拟化的虚拟机监控程序。第4.4节讨论了CPU虚拟化的性能影响,特别是在不同模式之间的原子转换的实现成本。最后,与所有章节一样,我们提供了进一步阅读的指引。
4.1设计规定
intel虚拟化技术,一般称为 VT-x [171] ,于2005年推出,主要目标是为虚拟化提供架构支持。
英特尔虚拟化技术的一个核心设计目标是消除对CPU半虚拟化和二进制翻译技术的需求,从而实现可以支持各种未经修改的客户操作系统的VMM的实现,同时保持高水平的性能。—R. Uhlig等人,IEEE Computer ,2005年 [171]
在提出这个目标时,英特尔的工程师观察到,使用诸如半虚拟化和动态二进制翻译等现有技术在虚拟化现有x86架构方面面临一些严峻的技术挑战。Uhlig等人[171]列出了以下对使用软件技术的方法的具体挑战。
- 环别名和压缩:设计为在%cpl=0下运行的客户内核代码必须使用其余的三个特权级来确保隔离。这会创建一个别名,因为至少有两个不同的客户特权级必须使用相同的实际特权级。然而,根据架构的规范,运行在这两个特权级中的软件必须相互保护。
- 地址空间压缩:VMM必须位于线性地址空间的某个部分,而客户软件不能访问或使用该部分。
- 对特权状态的非故障访问:一些臭名昭著的Pentium 17 [151]指令提供对特权状态的只读访问,并且是行为敏感的,例如中断描述符表(sidt),全局描述符表(sgdt)等的线性内存位置。这些结构由VMM控制,位置与客户操作系统指定的位置不同。
- 客户转换的不必要影响:为了满足效率标准,关键是敏感指令(必须触发转换的指令)在实际中很少出现。不幸的是,现代操作系统广泛依赖于特权级敏感的指令,例如在关键区域内暂停中断,或在内核和用户模式之间进行转换。理想情况下,这些指令不应对虚拟化敏感。
- 中断虚拟化:中断标志的状态(%eflags.if)对非特权指令(pushf)可见。由于当客户正在运行时无法清除该标志,因为这会违反安全性,因此客户直接使用该指令将导致不正确的行为。此外,popf指令是控制敏感的,因为其语义取决于CPU是否可以控制中断(即%cpl ≤ %eflags.iopl)。
- 访问隐藏状态:x86-32架构包含一些“隐藏”状态,最初从内存加载,但如果内存中的内容发生更改,则无法由软件访问。例如,32位段寄存器是隐藏的:它们从内存加载到处理器中,但不能从处理器重新加载到通用寄存器或内存中。只要内存内容不发生更改,就可以将段寄存器重新加载到处理器中,例如,在由虚拟化引起的陷阱后和对客户操作系统进行隐藏后。然而,一些旧操作系统,尤其是Windows 95,在某些关键区域修改段描述符表的内容,但明确依赖于这些非常特定的x86-32语义。
当然,第3章中描述的系统的证据表明,这些挑战在实践中可以得到缓解,但无法完全消除。例如,地址空间压缩挑战会在虚拟机使用线性内存顶部时影响性能:一些客户操作系统可能会因此表现非常差。更令人担忧的是,一些“臭名昭著”的“Pentium 17”指令始终会向在用户级别运行的应用程序返回不正确的结果,这是对等性的限制。
在设计VT-x时,英特尔的核心设计目标是完全满足Popek和Goldberg定理的要求,明确的目标是运行在基于VT-x的虚拟化程序之上的虚拟机满足等效性、安全性和性能这三个核心属性。
- 等效性(Equivalence):英特尔的架构师们设计了VT-x,以在虚拟化硬件和基础硬件之间提供绝对的架构兼容性,而且还要向后兼容传统的x86-32和x86-64 ISA。这比VMware Workstation更为雄心勃勃,后者着眼于一组明确定义的客户操作系统,并且半虚拟化方法如Xen,它需要内核修改,并且实际上只适用于开源操作系统。
- 安全性(Safety):基于动态二进制翻译或半虚拟化的先前虚拟化程序通过需要在软件中维护复杂的不变量的推理来提供安全性和隔离性,例如正确使用分段进行保护。通过专门为虚拟化设计的体系结构支持,一个大大简化的虚拟机监视程序可以提供相同的特性,但代码规模要小得多。这减少了虚拟机监视程序的潜在攻击面和软件漏洞的风险。
- 性能(Performance):具有讽刺意味的是,性能比现有的最先进的虚拟化技术更高并不是第一代硬件虚拟化支持的发布目标。相反,在开始阶段的目标仅仅是建立适当的体系结构和在体系结构和微体系结构级别进行持续改进的路线图。事实上,第一代具有硬件虚拟化支持的处理器在性能上与使用DBT的最先进的解决方案不竞争。
4.2 VT-X 架构
VT-x架构的核心设计决策是不改变ISA(指令集架构)中各个指令的语义。这当然包括那些最明显违反虚拟化原则的指令。作为一个推论,该架构也不试图单独解决限制虚拟化的架构的各个方面,如第4.1节所述。相反,VT-x复制了处理器的整个架构可见状态,并引入了一种新的执行模式:根模式(root mode)。Hypervisors(虚拟机监视程序)和主机操作系统在根模式下运行,而虚拟机在非根模式下执行。这个架构扩展具有以下属性:
- 处理器在任何时间点都处于根模式或非根模式中的一种。状态转换是原子的,意味着单个指令或陷阱可以从一种模式切换到另一种模式。这与操作系统执行上下文切换的传统实现方式不同,后者需要一个复杂的指令序列。
- 新模式(根模式与非根模式)仅用于虚拟化。它与处理器的所有其他执行模式(例如,实模式、v8086模式、保护模式)正交,这些模式在两种模式下都可用。它也与保护模式的保护级别(例如%cpl=0–%cpl=3)正交,每种模式分别适用于每种模式。
- 每种模式定义了自己独立的完整64位线性地址空间。每个地址空间由独立的页表树定义,具有独立的页表寄存器。只有与当前模式对应的地址空间在TLB中处于活动状态,并且TLB在状态转换的一部分中以原子方式更改。
- 每种模式都有自己的中断标志。特别是在非根模式下,软件可以自由地操作中断标志(%eflags.if)。外部中断通常在根模式下传递,并在必要时触发从非根模式到根模式的转换。即使在非根中断被禁用时,状态转换仍会发生。
图4.1说明了VT-x的核心设计如何由系统软件使用:在根模式下,软件可以访问x86-64的完整(非虚拟化)架构,包括受保护模式的所有特权环(在图中显示)以及执行的传统模式(未显示)。这提供了架构的向后兼容性,因此对于软件来说,主机操作系统通常在根-%cpl=0中运行,其应用程序将在根-%cpl=3中运行。虚拟机监视程序也在根-%cpl=0中运行,在那里它可以发出新的特权指令以进入非根模式。图4.1还显示,虚拟机使用特权环的完整副本执行:每个客户操作系统在非根-%cpl=0中运行,应用程序在非根-%cpl=3中运行。
4.2.1 VT-X 和 POPEK/GOLDBERG 定理
回想一下2.2中讨论的 Popek 和 Goldberg 的中心虚拟化定理。
..定理1[143] : 对于任何传统的第三代计算机,如果该计算机的敏感指令集是特权指令集的子集,则可以构造虚拟机监视器。
VT-x架构满足了Popek和Goldberg的定理的标准,但通过与最初用于证明该定理的原始模型相比,进行了重大的改进。Popek和Goldberg确定了虚拟化的关键标准。通过他们的定理构建证明,他们进一步证明了虚拟化可以在不需要任何额外的硬件约束的情况下实现,只需支持受保护操作系统所需的那些约束,特别是使用地址空间压缩和环别名(在用户级别同时运行客户操作系统和应用程序)来构建虚拟机监视程序。
而英特尔采取了一种不同的实用路径,通过复制处理器的整个状态并引入专用的根模式。特别是,对架构的完全复制是为了确保ISA的向后兼容性和虚拟化工作负载的完全等效性而进行的[171]。因此,必须重新定义术语,以确凿地表达VT-x遵循Popek/Goldberg标准。相应的核心VT-x设计原则可以非正式地表述如下。
在具有根模式和非根模式执行以及处理器状态的完全副本的体系结构中,如果所有敏感指令(根据非可虚拟化的传统体系结构)都具有根模式特权,则可以构建虚拟机监视程序。
在非根模式下执行时,所有具有根模式特权的指令都是(i)由处理器实现的,要求它们仅对处理器的非根副本执行操作,或者(ii)引发陷阱。
我们可以做三个观察。
- 与最初的Popek和Goldberg定理不同,这个重新表述不考虑指令是否具有特权(根据它们对在%cpl>0运行的软件的可用性定义),而只考虑它们是否具有根模式特权的问题。
- 这些陷阱足以满足等效性和安全性的标准。这与最初的定理类似。
- 但是,通过在硬件中实现某些敏感指令以减少转换是满足虚拟化的性能标准所必需的。
举几个例子可以帮助说明灵敏度的概念与(常规的)特权的概念是相互独立的。首先,以非根-%cpl=0运行的客户操作系统将发出某些特权指令,例如读取或甚至写入控制寄存器:这些指令必须在处理器状态的非根副本上运行。根据原则,在这种情况下有两种可能的实现方式:(i)要么在硬件中在非根上下文中执行指令,因为整个处理器状态都是复制的。虚拟机监视程序不应被通知,因为硬件完全处理这些指令;(ii)或者从非根模式引发陷阱,允许虚拟机监视程序模拟指令。从性能的角度来看,前者更可取,因为它减少了转换的次数。显然,这需要处理器的特定架构支持。因此,实施决策是硬件复杂性和总体性能之间的权衡。
作为第二个例子,操纵中断标志的指令(cli、sti、popf)是敏感的,特别是因为其他指令实际上可以将关于中断标志本身的信息泄漏到内存中(pushf)。然而,请记住,这四个指令在体系结构中不是特权指令,也就是说,在某些情况下,它们对用户级应用程序是可用的。鉴于它们在现代内核中的高执行率,它们的非根实现是由处理器直接处理的。
作为第三个例子,一些在用户级可用的指令,如sgdt和sidt,是已知的行为敏感指令。这些罕见的指令虽然不具备特权,但对虚拟化敏感。因此,它们必须具有(并且确实如此)根模式特权。
4.2.2根模式和非根模式之间的转换
图4.2展示了VT-x实现中根模式和非根模式之间的主要交互和转换。虚拟机的状态存储在物理内存中的一个专用结构中,称为虚拟机控制结构(Virtual Machine Control Structure,VMCS)。一旦初始化,虚拟机通过vm resume指令恢复执行。这个特权指令将状态从内存中的VMCS加载到寄存器文件中,并在主机环境和客户环境之间执行原子转换。然后,虚拟机在非根模式下执行,直到必须由虚拟机监视程序处理的第一个陷阱或下一个外部中断。从非根模式到根模式的这种转换称为#vmexit。
表4.1列出了#vmexit的各种可能原因。原因本身存储在一个专用寄存器(vmcs.exit_reason)中,以加速模拟。导致退出的原因可以分为以下几类。
• 客户尝试执行根模式特权指令,因为这是Popek和Goldberg定理的基本要求。这包括大多数经典(即非可虚拟化)x86架构的特权指令,以及导致定理违规的敏感但非特权的指令。
• 新的vm call指令,旨在允许非根模式和根模式之间的显式转换,特别是由客户操作系统发出并发送到虚拟机监视程序的超级调用。这类似于sys enter指令,用于在用户模式和内核模式之间进行转换。
• 异常是由非根模式下执行的任何无害指令引发的,偶然触发陷阱的情况。这包括特别是由于阴影分页、访问内存映射的I/O设备或由于段违规而引发的页面故障(#PF)或一般性故障(#GP)。
• 当扩展页面映射(由虚拟机监视程序控制)无效时,导致的页面故障的子集称为EPT违规。这个退出是在引入扩展分页(参见第5章)时引入的。
• 当CPU在非根模式下执行时发生的外部中断,例如由主机上的网络或磁盘I/O引发的中断。这些事件必须由虚拟机监视程序(对于类型1设计)或主机操作系统(对于类型2设计)处理,可能对虚拟机本身具有或不具有任何副作用。
中断窗口在虚拟机启用中断并且虚拟机有未处理中断时打开。在#vmexit之后,虚拟机监视程序可以将未处理的中断模拟到虚拟机上。
最后,VT-x引入的用于支持虚拟化的ISA扩展也是受控敏感的,因此会导致#vmexit,每个#vmexit都有不同的退出原因。这种退出在“正常虚拟化”期间永远不会发生,但在嵌套虚拟化中起着基础作用。
根模式和非根模式之间的转换在体系结构上是原子的:一个单一指令——vmresume——将转换回非根模式并将VMCS状态加载到当前处理器状态中。在另一个方向上,陷阱#vmexit将虚拟CPU的状态存储到VMCS状态中。虽然VMCS状态由特定的内存区域支持,但体系结构没有规定处理器必须将整个状态溢出到缓存的内存中,还是可以将其推迟到处理器本身的子集中。因此,当前VMCS的内存状态是未确定的。此外,VMCS的布局未定义。虚拟机监视程序软件必须通过vmread和vmwrite指令对客户状态的选定部分进行访问。
4.2.3一个警示性的故事ーー虚拟化中央处理器与无视 MMU
今天,所有支持虚拟化的处理器架构也都虚拟化了内存管理单元(MMU)。然而,这并不总是这样。事实上,第一代带有VT-x的英特尔CPU仅提供了有关内存虚拟化的基本支持。在这样的设计中,根模式和非根模式分别具有一个不同的%cr3寄存器,指定页表树的基址。%cr3寄存器会在vm entry和#vm exit之间的根模式和非根模式之间的过渡中原子更新。因此,虚拟机监视程序可以配置100%不重叠的地址空间。这解决了先前虚拟机监视程序的地址压缩问题,并消除了依赖分段来保护虚拟机监视程序的需要。但在这种最小化的设计中,内存虚拟化的其他方面都留给了软件。与之前没有任何架构支持的架构一样,仍然有两种方法可行:(1)用包含宿主物理值的重复页表集合来阴影虚拟机的页表(使用虚拟客户物理值)——这是VMware所做的(见§3.2.5);或者(2)依赖虚拟内存子系统的半虚拟化,并通知虚拟机监视程序验证所有新映射(见§3.3)。当阴影页表时,这个选择产生了严重而意想不到的后果。使用阴影分页,英特尔处理器的前两代未能解决性能标准:超过90%的#vmexit过渡是由于阴影分页和由此产生的虚拟机性能较慢,比简单地禁用VT-x并使用软件技术时要慢[3]。这种异常的解释实际上是微妙的:阴影操作通过保持硬件页表的映射与由客户操作系统在其自己的页表中所做的更改同步来进行。识别更改的机制是MMU本身:所有客户页表页都被降级为只读映射,以确保从虚拟机到虚拟机监视程序的过渡,即在经典架构中的陷阱和在VT-x中的#vmexit。因此,完全依赖直接执行的虚拟机监视程序必然会因虚拟机客户操作系统的虚拟内存更改而遭受陷阱。相比之下,VMware的内存跟踪机制依赖于自适应二进制翻译,以消除绝大多数页面故障。自适应动态二进制翻译是一种有效的技术,因为它依赖于操纵页表项的指令的高局部性,每个操作系统大约有一打这样的指令。一旦动态识别了这些指令位置,自适应的重新翻译过程就简单地模拟这些指令并更新阴影条目,而不会直接引用内存位置,因此不会引发昂贵的陷阱。 幸运的是,这个异常在随后的处理器中得到了解决。
4.3 KVM ーー VT-X 的hypervisor
到目前为止,我们已经描述了 VT-x 引入的硬件扩展,并讨论了一些体系结构方面的注意事项。我们现在使用基于 Linux 的内核虚拟机 KVM [113]作为实践创新的案例研究。KVM 因其成熟性和简单性而成为一个有趣的研究对象。
KVM是最重要的开源类型-2虚拟化监视器之一。它在众多项目、云托管解决方案中得到应用,并广泛部署在企业和私有云中。KVM是主要商业Linux发行版官方支持的虚拟机监视器,也是大多数OpenStack部署的基础。
- KVM依赖于独立的开源项目QEMU来模拟I/O。在没有KVM的情况下,QEMU是一个完整的机器模拟器,支持对CPU的跨架构二进制翻译,以及完整的I/O设备模型。与KVM一起,它们组成了类型-2虚拟机监视器,其中QEMU负责处理所有I/O前端设备模拟,Linux主机负责I/O后端(通过正常系统调用),而KVM内核模块负责复用处理器和内存管理单元(MMU)。
- KVM的内核组件实现了CPU和内存虚拟化的功能,相当于VMware虚拟机监视器(VMM)。这些组件已内置到Linux内核中,旨在避免与Linux产生不必要的冗余。KVM从一开始就被设计为Linux的一部分,明确的目标是将所有内核驻留组件合并到Linux主线源代码树中。这一目标于2007年的Linux版本2.6.20中实现。
- 与Xen或VMware Workstation不同,KVM从一开始就假设存在硬件支持虚拟化的情况下进行设计。这意味着它充分利用了硬件虚拟化支持,如Intel的VT-x。这使得KVM成为研究基于VT-x设计的虚拟化监视器内在复杂性的一个特别好的案例。
综上所述,KVM是一个备受推崇的开源虚拟机监视器,广泛应用于各种项目,包括云托管和企业环境。它依赖于QEMU来提供I/O模拟,并与Linux内核深度集成,充分利用硬件虚拟化支持,成为主要Linux发行版的官方支持虚拟机监视器。
4.3.1 充分利用 VT-X 的挑战
KVM的开发人员根据Popek和Goldberg的虚拟机三个核心属性进行了如下调整:
等价性:KVM虚拟机应能够运行任何x86操作系统(32位或64位)及其所有应用程序,无需任何修改。KVM必须在硬件级别提供足够的兼容性,以便用户可以选择他们的客户操作系统内核和发行版。
安全性:KVM虚拟化虚拟机可见的所有资源,包括CPU、物理内存、I/O总线和设备以及BIOS固件。即使在存在恶意或故障的客户操作系统的情况下,KVM hypervisor始终完全控制虚拟机。
性能:KVM应具有足够的速度来运行生产工作负载。然而,KVM明确设计为类型-2架构,这意味着资源管理和调度决策作为主机Linux内核的一部分(其中KVM内核模块是其中的一部分)而保留。
为实现这些目标,KVM的设计进行了仔细的权衡,以确保所有性能关键组件都在KVM内核模块内处理,同时限制了该内核模块的复杂性。具体来说,KVM内核模块仅处理与x86处理器的仿真、内存管理单元(MMU)、中断子系统(包括APIC、IOAPIC等)的仿真相关的核心平台功能;所有负责I/O仿真的功能都在用户空间处理。
为了进一步简化实现,KVM的原始版本高度利用了两个现有的开源项目:(i)QEMU用于用户空间中的所有I/O仿真,以及(ii)Xen的x86特定部分,它作为Linux内核模块的起点。此后,KVM和Xen基本上走上了不同的道路,但QEMU的演进主要受到对KVM的要求驱动。
4.3.2 THE KVM KERNEL MODULE
KVM的内核模块仅处理基本的CPU和平台仿真问题。这包括CPU仿真、内存管理和MMU虚拟化、中断虚拟化以及一些芯片组仿真(如APIC、IOAPIC等)。但它不包括所有I/O设备仿真。
考虑到KVM仅设计用于遵循Popek/Goldberg原则的处理器,理论上设计是简单明了的:(i)适当配置硬件;(ii)让虚拟机直接在硬件上执行;(iii)在第一次陷入或中断时,虚拟机监视程序然后重新获得控制权,并“只是”根据语义仿真陷入的指令。
然而,实际情况要复杂得多。我们的分析基于2016年10月的linux-4.8版本。KVM内核模块本身有超过25,000行源代码(LOC)。这种复杂性部分原因在于:(i)需要支持追溯到首款具有VT-x(Intel的Prescott)的处理器的多个不同版本;(ii)x86指令集架构的固有复杂性;(iii)硬件中仍然缺乏对一些基本操作的架构支持,这些操作必须在软件中处理。
图4.3说明了KVM陷入处理逻辑的关键步骤,从最初的#vmexit到vmresume指令返回非根模式。在#vmexit之后立即,KVM首先将所有vcpu状态保存在内存中。英特尔体系结构手册定义了54种可能的退出原因(见表4.1)。然后,KVM根据vmcs.exit_reason进行第一级调度,对每个退出原因(arch/x86/kvm/vmx.c中的handler_*
)使用不同的处理程序。这些处理程序中的大多数都很简单。特别是,某些常见代码路径仅依赖于VMCS字段来确定执行所需的必要仿真步骤。根据情况,KVM可以执行以下操作:
- 仿真指令的语义,并将指令指针增加到下一条指令的开头;
- 确定必须将故障或中断转发到客户环境。根据x86的语义,KVM更改堆栈指针,并将以前的指令和堆栈指针存储在堆栈上。然后,在客户的中断描述符表指定的指令处恢复执行;
- 更改底层环境并重新尝试执行。例如,在发生EPT违规时会发生这种情况;
- 什么都不做(至少对虚拟机状态来说)。例如,当发生外部中断时,由底层主机操作系统处理。非根执行最终会在中断之前中断的位置继续。
不幸的是,VMCS中可用的信息有时不足以处理#vmexit,而不实际解码引起它的指令。因此,KVM以及试图提供等效性的任何VT-x的虚拟机监视程序必须还包括通用目的的解码器,能够解码所有指令,以及通用目的的仿真器,可以对其进行仿真。
图4.3还说明了KVM通用目的仿真器实现中涉及的关键步骤。执行这个基本步骤的核心逻辑在arch/x86/kvm/emulate.c中实现,该文件有5000多行代码,充满了宏、复杂性和微妙之处。仿真器的关键步骤包括:
- 从客户虚拟内存(%cs:%eip)中提取指令。首先,虚拟地址必须转换为线性地址,然后转换为客户物理地址,并从内存中提取;
- 解码指令,提取其操作数和操作数。x86-64指令的CISC性质和可变长度使得这个过程不是微不足道的;
- 验证指令是否可以在虚拟CPU的当前状态下执行,例如,特权指令只能在虚拟CPU处于cpl0时执行;
- 从内存中读取任何内存读取操作数,就像在x86 CISC架构中使用与指令获取相同的虚拟到线性到客户物理重定位步骤一样;
- 仿真解码的指令,可以是x86架构中定义的任何指令。每个指令操作码通过其自己的专用仿真例程(源代码中的
em_*
)进行仿真; - 将任何内存写操作数写回客户虚拟机;以及
- 根据需要更新客户寄存器和指令指针。
显然,这些步骤复杂、昂贵,并且充满了特殊情况和可能的异常条件。图4.3进一步显示了这种复杂性,因为每个阶段都包含可能的异常情况,在这些情况下,陷阱和仿真逻辑会得出结论,客户指令无法成功执行,而应在客户虚拟CPU中生成故障,例如虚拟#GP、#PF或#UD。
此外,仿真器以某些罕见情况下对实际硬件造成故障,例如当除以零时。图中以红色显示了这一点,导致了额外的复杂性。
这个仿真器也以其脆弱性而闻名,它的实现随着时间的推移不断发展,以解决缺陷报告以及持续的ISA扩展。它仍然容易出错。Amit等人最近的研究发现KVM内核模块中有117个仿真错误,其中72个仅在仿真器中发现。
4.3.3 THE ROLE OF THE HOST OPERATING SYSTEM
KVM专门设计为Linux的一部分。与其他类型-2的虚拟机监视程序(如VMware Workstation或VirtualBox)不同,它深度集成到Linux环境中。例如,perf工具包具有专门用于分析KVM虚拟机的模式;这是由于KVM和Linux项目的深度集成所实现的。
图4.4显示了核心KVM虚拟机执行循环[113],以一个虚拟CPU为例。外部循环位于用户模式下,重复执行以下操作:
- 通过对字符设备/dev/kvm的ioctl进入KVM内核模块;
- 然后,KVM内核模块执行客户代码,直到(i)客户使用I/O指令或内存映射I/O发起I/O操作,或者(ii)主机接收到外部I/O或定时器中断为止;
- 然后,QEMU设备仿真器根据需要模拟发起的I/O;
- 在发生外部I/O或定时器中断的情况下,外部循环可以使用另一个ioctl(/dev/kvm)简单地返回到KVM内核模块,而没有进一步的副作用。
然而,这一步在用户空间是必不可少的,因为它为主机操作系统提供了进行全局调度决策的机会。
内部循环(在KVM内核模块内部)反复执行以下操作:
- 恢复虚拟CPU的当前状态;
- 使用vmresume指令进入非根模式。在那一点上,虚拟机在该模式下执行,直到下一个#vmexit;
- 根据退出原因处理#vmexit,如§4.3.2中所述;
- 如果客户发出了一个编程的IO操作(exit_reason = IO)或内存映射的IO指令(exit_reason = exception,但仅在访问内存映射的IO页时),则中断循环并返回到用户空间;
- 如果#vmexit是由外部事件引起的(例如,exit_reason = interrupt),则中断循环并返回到用户空间。
4.4 PERFORMANCE CONSIDERATIONS
VT-x的设计围绕着在根模式和非根模式之间复制体系结构状态以及在它们之间进行原子转换的能力。单个指令vmresume返回非根模式并将VMCS状态加载到当前处理器状态中。
另一方向上,陷入#vmexit将整个虚拟CPU的状态存储到VMCS状态中。 模式之间的原子转换并不意味着高执行速度,当然也不是单周期的执行时间。直观地说,这种转换预计会暂停整个执行流水线,因为整个寄存器文件、特权状态和指令指针都会发生变化。 实际上,这些转换的测得成本很高,并且表明在处理器的微码固件中存在复杂的实现。表4.2显示了硬件往返的成本,定义为#vmexit后由虚拟机监视程序中的NULL处理器执行的指令指针增加并恢复虚拟机的执行。在早期的处理器中,例如Prescott,单个硬件往返的成本以微秒为单位测得,远远超过常规陷阱的成本。从那以后,转换已经改进了5倍,但仍然很高。
Amit等人在他们的研究中[15]发现了一些关于VT-x是否符合Popek/Goldberg定理的次要硬件限制:物理地址宽度可以直接通过CPUID指令获得,而无法虚拟化。当在具有不同物理地址宽度的系统之间进行虚拟机的实时迁移时,这可能会导致问题。此外,一些FPU状态无法完全虚拟化,需要采取解决方法。
4.5 FURTHER READING
在同一项研究中,Amit等人主要确定了KVM实现中违反了预期的虚拟机监视程序的安全等效性属性的软件错误。通过系统比较KVM虚拟机与英特尔的参考模拟器[15]的行为,他们确定了KVM中的117个不同错误。这些错误的共同原因是x86 ISA的复杂性完全暴露给了软件。尽管这些错误中的大多数都是在实际中影响不大的边缘案例等效性限制,但至少有6个错误导致了安全漏洞,可能导致主机DoS、虚拟机DoS或特权升级。这些问题要求重新设计KVM内核模块,特别是考虑将指令模拟器移动到用户空间,以便更容易地隔离错误[37]。
对KVM有更深入了解兴趣的读者很快会发现,源代码是最好的文档形式,尽管KVM网站[118]中也包含了一些有用的指导。
自从硅中引入VT-x以来,x86架构的所有虚拟机监视程序都已经采用了VT-x。当前版本的VMware Workstation类似于KVM,利用了VT-x的MMU功能:世界切换不再以其原始形式存在,VMware Workstation的核心执行循环与KVM类似。Adams和Agesen[3]在硬件支持存在的情况下,详细研究了在DBT和直接执行之间的这种权衡。每当虚拟机监视程序必须在内存中影子复制在实际中频繁访问的数据结构时,这种权衡就会很大。在扩展页处理器引入之前,这种权衡是基本的,作者得出结论称VT-x(不包括扩展分页)会影响性能。
Xen也早早采用了VT-x,称为硬件虚拟化(HVM)[50, 146]。使用HVM,虚拟机操作系统不再需要进行paravirtual化才能在Xen上运行。相反,paravirtualization只是一组可选的扩展,通过与虚拟机监视程序直接通信来提高性能和功能。
就我们所知,从根模式到非根模式的过渡的微体系结构成本从未在学术上深入研究过,也没有披露在英特尔或AMD处理器中的其实施的显要方面。从Prescott到Sandy Bridge的5倍改进的原因仍然是一个引发猜测的问题。相反,焦点一直是不懈减少处理#vmexit的软件成本。
Table4.3: Hardware costs of individual VT-x instructions and
#vmexit
for different Intel processors
X86-64: 带扩展页表的 MMU 虚拟化
虚拟机监控程序必须虚拟化物理内存,以便每个虚拟机都具有管理自己连续物理内存区域的错觉。回想一下第1.6节的定义:每个虚拟机都提供了客户物理内存的抽象,而虚拟机监控程序管理主机物理内存,即实际的底层物理资源。
这导致了一个二维问题:客户操作系统定义了虚拟内存和客户物理内存之间的映射。虚拟机监控程序然后独立地定义了客户物理内存和主机物理内存之间的映射。
在内存管理单元中没有任何架构支持的情况下,虚拟机监控程序依赖于阴影分页来虚拟化内存。在阴影分页中,虚拟机监控程序管理一组综合的页表,将虚拟内存映射到主机物理内存(有关VMware实现的描述,请参见§3.2.5)。作为在软件中实现的阴影分页可能是虚拟机监控程序中最复杂的子系统。它依赖于内存跟踪来跟踪内存中的页表结构的更改。阴影分页还严重依赖于启发式方法来确定哪些页面应该被跟踪,因为页表可以位于物理内存的任何位置,并且可以由客户操作系统自行分配和重新分配。此外,VT-x的引入对阴影分页的性能产生了负面影响(请参见§4.2.3)。
扩展页表为MMU虚拟化提供了体系结构支持。第5.1节描述了x86-64处理器中扩展分页的设计。第5.2节描述了KVM如何管理和虚拟化内存,并利用扩展页表。第5.3节衡量了MMU虚拟化的成本。最后,与所有章节一样,我们提供了进一步阅读的指南。
5.1 扩展分页
- 扩展页表:扩展页表是硬件辅助的内存虚拟化方法。它利用处理器的硬件特性来管理虚拟内存到物理内存的映射。处理器提供了额外的硬件结构(通常是多级页表),用于直接管理这些映射。这种方法通常需要硬件支持,如Intel的EPT(扩展页表)或AMD的NPT(嵌套页表)。
- 影子页表:影子页表是一种软件辅助的内存虚拟化方法。在这种方法中,虚拟机监视器(例如Hypervisor)维护了两组页表结构:一组是虚拟机的页表,用于虚拟机内部的虚拟地址到虚拟机物理地址的映射,另一组是影子页表,用于虚拟机物理地址到真正的主机物理地址的映射。虚拟机监视器负责在虚拟机执行期间保持这两组页表同步。
扩展页表,也称为嵌套页表,消除了基于软件的阴影分页的需要。该设计由Bhargava等人于2008年发表,并在大约同一时间由AMD和英特尔推出芯片支持。扩展页表在硬件中将x86经典的硬件定义的页表结构(由客户操作系统维护)与由虚拟机监控程序维护的第二页表结构相结合,该结构指定了客户物理到主机物理的映射。这两个结构都以树的形式组织。
使用扩展页表,与处理器深度集成的TLB查找逻辑不会发生根本性变化:TLB仍然以集合关联高速缓存的形式将虚拟页映射到主机物理页。在支持超级页(例如x86-64上的2 MB和1 GB)的体系结构上,通常为每个页大小有一个不同的高速缓存。
从根本上改变的是TLB未命中处理逻辑。每当TLB中不存在映射时,x86-64架构规定处理器将遍历由%cr3根的页表树,并将缺失的映射插入到TLB中。在x86-64上,在没有扩展分页的情况下,对于常规页,树是一个n=4级树,对于2 MB页,树是一个n=3级树,对于1 GB页,树是一个n=2级树。因此,页面遍历逻辑必须访问n个内存位置以找到缺失的映射。只有然后CPU才能执行实际的内存读取或写入。
让我们考虑一下虚拟到客户物理树是n级,客户物理到主机物理树是m级的情况。如图5.1所示。在TLB未命中时,硬件遍历了完全由客户物理页组成的客户页表结构,要求每个客户物理引用必须分别映射到自己的主机物理地址。为了解决第一棵树中的每个引用,处理器必须首先在第二棵树中执行m个引用,然后在第一棵树中查找映射。共有n个这样的步骤,每个步骤需要m+1个引用。这nx(m+1)个引用导致所需的客户物理地址。还需要m个查找来将此客户物理地址转换为主机物理地址。因此,进行扩展页查找所需的架构定义的内存引用次数为nxm+n+m。
总之,扩展分页通过二次查找算法组合了两个独立的映射。尽管在架构上是二次的,但当前一代处理器广泛依赖于辅助数据结构来减少对内存层次结构的实际引用次数。
5.2 虚拟化 KVM 内存
KVM最初在引入扩展分页之前发布。因此,KVM内核模块可以配置为启用或禁用该功能[42]。然而,在实践中,考虑到当前(2017年)使用KVM的情况时,不使用扩展分页的情况仅限于嵌套虚拟化的情况,例如Turtles研究原型[33]。
图5.2显示了KVM中内存管理的关键方面。该图显示了三个不同的页表结构,由不同的实体管理(以红点表示)。
- 作为用户空间进程,QEMU在其自己的虚拟地址空间中的一个连续部分中分配虚拟机物理内存。这样做有几个原因:(i)它将内存管理和分配的细节交给了主机操作系统,符合类型2架构的原则;(ii)它方便用户空间访问虚拟机物理地址空间;这在特定情况下由模拟设备使用,这些设备执行对虚拟机物理内存的DMA读写操作。像机器上的任何进程一样,Linux管理该进程的页表树。当QEMU进程被调度时(包括虚拟机的VCPU正在运行的所有情况),根模式的%cr3寄存器定义了一个地址空间,其中包括QEMU和连续的虚拟机物理内存。
- 虚拟机管理其自己的页表。启用嵌套分页后,非根模式的cr3寄存器指向客户页表,这定义了当前的地址空间以虚拟机物理内存为基础。这完全受到客户操作系统的控制。此外,KVM无需跟踪客户页表的更改或上下文切换。实际上,当启用该功能时,非根模式中对cr3的分配不需要引发#vmexit。
- 在x86架构中,常规页表的格式(由cr3寄存器指定)与嵌套页表的格式不同。因此,KVM内核模块负责管理嵌套页表,根据Intel架构在eptp寄存器中指定。
我们还做了一个额外的观察,对性能有影响:对于虚拟化管理程序来说,访问虚拟机物理地址空间比访问虚拟机虚拟地址空间更容易。 具体来说,VT-x架构不提供根模式软件有效访问非根模式环境的虚拟地址空间的体系结构支持。
图5.2显示,对于虚拟化管理程序来说,访问虚拟机物理地址空间很简单:用户空间进程可以简单地将一个常量偏移量添加到内存位置的引用中。实际上,KVM模块本身也可以使用相同的方法,因为用户空间进程已经映射了地址空间。然而,对于虚拟化管理程序来说,要访问虚拟机的虚拟地址空间要困难得多和复杂得多,因为这些映射只存在于处理器的内存管理单元(MMU)中,而虚拟机在执行时存在,但在虚拟化管理程序执行时不存在。 不幸的是,客户指令指针是一个虚拟地址,KVM解码器必须从内存中读取故障指令的内容。同样,指令的所有内存操作数都涉及虚拟机虚拟地址的引用。因此,KVM解码器和模拟器在软件中重复引用客户页表,以确定指令和操作数在虚拟机物理内存中的位置。 KVM的类型2虚拟机监视器设计在管理虚拟机物理内存时引入了一个复杂性。尽管格式不同,但嵌套页表中虚拟机物理地址空间的语义映射必须与主机操作系统的虚拟机物理地址空间的映射保持一致。例如,如果虚拟机物理映射发生变化,例如,在处理虚拟机的#vmexit时分配了新页面,那么相同的映射也必须反映在主机上的QEMU进程的地址空间中。反之,如果主机操作系统决定从QEMU进程中交换出一个页面,则还必须删除相应的虚拟机物理扩展映射。
当前版本的KVM在内存管理领域包含了一些关键的优化。特别是,Linux的KSM机制允许内存的透明共享,类似于VMware ESX Server中引入的解决方案。KSM允许具有相同内容的内存页面在Linux进程之间以及KVM虚拟机之间进行透明共享。当发现两个属于潜在不同进程的不同页面具有相同的内容,并且该内容被声明为稳定,即在不久的将来不太可能更改时,将检测到匹配。然后,KSM执行以下操作:(i)从该集合中选择一个页面;(ii)在所有地址空间中为该页面建立只读映射;(iii)通知KVM模块执行相同的操作以处理嵌套页表;以及(iv)释放所有其他相同页面的副本。