操作系统 | 系统虚拟化

概述

系统虚拟化的历史

  • 1968,IBM 在大型机 System/360-67 中实现了第一个商用虚拟机监视器(Virtual Machine Monitor,VMM,也称为 Hypervisor)CP/CMS。CP 是虚拟监视器,负责创建和运行虚拟机(Virtual Machine,VM),并在其内运行 CMS。
  • 20 世纪 80 年代,个人计算机流行起来了,但是个人计算机的运算能力难以高效地支撑虚拟机的运行,因此系统虚拟化未有较大的发展。
  • 20 世纪 90 年代,服务器开始运行 Web 服务,甚至出现了服务器算力过剩的情况。

1997 年,斯坦福大学开发了 Disco 系统(可以在多核主机中运行的虚拟监视器)。

1998 年,Disco 开发者将其技术商业化,并创立了 VMware 公司。

  • 2000 年,互联网经济破灭,原本运行 Web 服务的服务器被大量闲置。如何提高服务器的资源利用率成为互联网企业和服务器托管企业重点关注的问题。
  • 云计算的出现和流行,IaaS 模式使得用户只需要租用云服务商的虚拟机,并在虚拟机中部署和运行自己的程序即可,这不仅降低了运维成本,还提高了物理服务器的资源利用率,提升了经济效益。因此,此模式在近二十年内得到了大规模推广,计算能力也变得像水、电一样成为按需购买、价格低廉的商品。

系统虚拟化的优势

  • 服务器整合

    • 传统数据中心服务器的 CPU 平均利用率仅为 20%,通过在一台物理服务器上整合多个虚拟机,可有效提高资源利用率,降低成本。
    • 基于用户“错峰使用”的特性,甚至可以使虚拟机的资源总数超过物理主机的资源总数,即云计算中的“超售”。
  • 虚拟机管理

    • 相比于传统服务器,虚拟机的管理简化了许多。通过软件接口,可以在短时间内创建数千台虚拟机并安装操作系统环境,也可以对虚拟机进行复制、备份、快照、销毁等操作。
  • 虚拟机热迁移

    • 虚拟机监视器可以将正在运行的虚拟机从一台物理服务器迁移到另一台物理服务器上,整个过程无须停机或重启。热迁移不但解决物理服务器维修导致的服务暂停问题,还能更好地实现全局的负载均衡。
  • 虚拟机安全自省

    • 允许虚拟机监视器从外部检查虚拟机内部状态是否正确,以判断虚拟机是否遭到入侵。

组成部分:虚拟机监视器和虚拟机组成

  • CPU 虚拟化

    • 为虚拟机提供虚拟处理器(virtual CPU,vCPU)的抽象并执行其指令的过程。
    • 虚拟机监视器直接运行在物理主机上,使用物理 ISA,并向上层虚拟机提供虚拟 ISA。虚拟 ISA 可以与物理主机上的 ISA 相同,也可以完全不同。

      • 相同的话,虚拟机中的大多数指令可以在物理机上直接执行,只有少数敏感指令需要特殊对待,性能较好。
      • 不同的话,虚拟机中的每一条指令都需要通过虚拟机监控器进行软件模拟,翻译成对应物理机上的指令。
  • 内存虚拟化

    • 为虚拟机提供虚拟的物理地址空间。
    • 虚拟机监控器负责管理所有物理内存,但又要让客户机操作系统“以为”自己依然能管理所有物理内存。因此,虚拟机监控器引入了一层新的地址空间-客户物理地址,并提供一种翻译机制,可以将客户物理地址翻译成真实的物理地址。
  • I/O 虚拟化

    • 为虚拟机提供虚拟的 I/O 设备支持。
    • 虚拟机监控器负责管理所有的 I/O 设备,客户操作系统管理的是虚拟机监控器提供的虚拟设备,虚拟机监控器将对虚拟设备的访问映射成对物理设备的访问。

VS 操作系统的抽象

  • 操作系统会为硬件定义新的“抽象“,其体现为新的软件接口。例如,操作系统对磁盘的操作抽象为对文件的操作,向上提供不同于磁盘接口的文件系统接口。
  • 虚拟机监控器只是提供对已有硬件接口的”虚拟化“。例如,虚拟机监控器可以把一个或多个文件虚拟成一个磁盘,对上层的客户操作系统提供磁盘的接口。

虚拟监视器类型

  • Type-1

    • 虚拟机监视器直接运行在最高特权级,可直接控制物理资源,并负责实现调度和资源管理等功能。可以将 Type-1 虚拟机监视器理解为一种特殊的操作系统,它所管理的“进程”就是虚拟机。
    • 典型代表就是 Xen。
  • Type-2

    • 虚拟机监视器需要依托一个宿主机操作系统,比如 Linux 和 Windows。Type-2 型的虚拟机监视器可以复用宿主操作系统中的调度和资源管理等功能,从而可以专注于虚拟化相关的功能。
    • 典型代表是 QEMU,其中操作系统内核(Linux)负责资源管理,QEMU 则负责提供核心的虚拟化功能。

CPU 虚拟化

基本概念

  • 下陷和模拟

    • 下陷是指 CPU 特权级从低特权级(例如 EL0 )切换到高特权级 (例如 EL1)
    • 模拟是指模拟下陷的指令的效果
  • 敏感指令/特权指令

    • 特权指令:在用户态执行时会触发下陷的指令

      • 包括主动触发下陷的指令(SVC)
      • 不允许在用户态执行的指令(例如写入只读内存)
    • 敏感指令:管理系统物理资源或者更改 CPU 状态的指令

      • 读写特殊寄存器或执行特殊指令以更改 CPU 状态。x86 架构中修改 CR0 或者 CR4 寄存器,ARM 架构中修改 SCTRL_EL1 寄存器;x86 架构中执行 hlt 指令,AArch64 中执行 wfi 指令等。
      • 读写敏感的内存。如,读写未映射的内存、写入只读页。
      • 执行 I/O 指令。如 x86 架构中的 in 和 out 指令。
  • 可/不可虚拟化架构

    • 可虚拟化架构

      • 所有敏感指令都是特权指令,也就是所有的敏感指令在非特权级执行时都会触发下陷。
    • 不可虚拟化架构

      • 不满足可虚拟机架构的定义。如,AArch32 中,操作系统可使用 cps 指令修改 CPU 状态 PSTATE 中的 AIF 位,来打开或者关闭外部中断。该指令在 EL1 时才会生效,如果在 EL0 执行则不会产生任何作用,也不会触发任何异常,而是被硬件执行忽略掉。

        • AArch32 是不可虚拟化架构
        • x86 架构在最初设计和实现时,有 17 条敏感指令在用户态执行时不会触发任何下陷,那么就不会被虚拟机监控器捕获到。
  • 全虚拟化/半虚拟化

    • 全虚拟化

      • 无须修改客户虚拟机源码的方式
    • 半虚拟化

      • 允许修改客户虚拟机源码的方式

弥补不可虚拟化架构缺陷的方法

(个人理解:最核心的就是敏感指令的处理方式不同,假如对敏感指令的处理方式采用了硬件那么其实就是硬件虚拟化)

  • 解释执行

依次取出虚拟机内的每一条指令,用软件模拟这条指令的执行效果。该方法不依赖于下陷,所有 的指令都将被虚拟机监控器模拟执行。该方法只是模拟指令的效果,而不需要精确地复现指令在硬件中的执行细节(只需要把这条指令要达到的效果实现出来)。

- 执行步骤

    - 虚拟机内部的代码区域将会被设置成可读权限(不可执行),然后由虚拟机监控器依次模拟每一条指令。
    - 1.虚拟机监控器读取当前虚拟机的虚拟程序计数器 PC,PC 是虚拟机监控器模拟的虚拟寄存器。
    - 2.根据 PC 的值找到待模拟的指令,并将指令解码,判断指令是何种类型的指令。
    - 3.指令模拟器根据解码的结果,找到相应的指令模拟函数。
    - 4.指令模拟函数读取虚拟机相关寄存器的内容并运行,然后更新相关内存或虚拟寄存器中的值。
    - 5.指令模拟器更新 PC,使其指向下一条指令,然后回到第一步。

- 优点

    - 不仅可以模拟与当前物理主机的 ISA 相同的虚拟机,还可以模拟不同 ISA 的虚拟机。

- 缺陷

    - 由于不加区分地模拟每条指令,会给虚拟机的执行带来巨大的性能开销。
  • 动态二进制翻译

由于解释执行是模拟每条指令,这带来了很大的开销,因此可以考虑将多条指令直接翻译成对应的模拟指令,然后直接执行翻译后的代码,从而提高了性能。
动态二进制翻译就是以基本块(basic block)为粒度,将一个基本块内的所有指令都翻译成最终的目标代码,敏感指令则在翻译过程中被替换。

- 执行步骤

    - 1.虚拟机监控器读取虚拟机的 PC,得到 PC 所指向的基本块。
    - 2.虚拟机监控器唤醒控制器。
    - 3.控制器根据 PC 中的指令地址,首先查找代码补丁缓存中是否存在已经翻译过的代码块,如果有则直接跳到第七步。
    - 4.如果缓存中不存在已经翻译过的代码块,则控制器会唤醒扫描翻译模块。
    - 5.扫描翻译模块读取内存中虚拟机的基本块,将其中的敏感指令替换为其他指令。同时将基本块中的最后一条指令替换为一条跳转指令,当执行时可以通知虚拟机监控器该基本块已经完成,这样虚拟机监控器可取出下一个基本块并进行翻译和执行。
    - 6.翻译后的基本块被称为代码补丁,它被放置于缓存中,以加速下次的过程。
    - 7.执行相应的代码块,并根据指令语义更新虚拟机(包括虚拟寄存器、内存、模拟设备)的状态。

- 优点

    - 采用批量翻译以及缓存的思想,提高了 CPU 虚拟化的性能。
  • 扫描和翻译

宿主机的指令集与虚拟机相同时,非敏感指令无须模拟,可直接在 CPU 中执行。因此,可以只让敏感指令下陷,其他指令直接执行。

- 执行步骤

    - 在虚拟机执行前,其所有内存被设置为不可执行。
    - 1.当物理 CPU 中的 PC 第一次执行某个代码页中的指令时,由于该代码页被设置为不可执行,因此会触发一次缺页异常。
    - 2.此异常将 CPU 特权级改为 EL1,从而会调用虚拟机监控器的缺页异常处理函数。
    - 3.异常处理函数将控制流交给控制器,控制器根据下陷地址找到触发此次异常的指令地址。根据指令地址,控制器首先查找缓存中是否存在已经翻译过的代码页,如果存在则直接返回用户态。
    - 4.如果缓存中不存在代码页,则模拟器唤醒扫描翻译模块。
    - 5.扫描翻译模块读取内存中待翻译的代码页内容,将其中的敏感指令(如果存在)替换为一定会触发异常下陷的指令,其他指令保持不变。

由于敏感指令只可能存在于内核代码,用户进程的代码不会包含这样的指令,因此可以只扫描内核代码,而忽略所有用户进程的代码,从而进一步提升性能。

    - 6.翻译后的代码页被放置于缓存中。
    - 7.虚拟机监控器将翻译后的代码页的权限设置为可执行,回到用户态。
    - 8.虚拟机执行翻译后的代码。此时,大部分代码可直接执行,敏感指令由于被替换为会触发异常的特权指令。因此,当执行被替换后的指令时,会触发异常,而这个异常可以被虚拟机监控器捕获,进而虚拟机监控器完成相应的指令模拟操作。
  • 半虚拟化技术

解释执行、动态二进制翻译、扫描和翻译都假设不能修改虚拟机的源代码,因此都是全虚拟化方式。

- 实现

    - 半虚拟化技术需要对客户操作系统与虚拟机监控器进行协同设计。此时,客户虚拟机不仅知道自己运行在虚拟化环境内,还需要对此进行相应的修改。比如,将 17 条敏感指令替换为可以下陷的指令。
    - 1.虚拟机监控器为虚拟机提供超级调用(hypercall),这些超级调用和系统调用类似,涵盖了调度、内存、IO 等多方面的功能。
    - 2.需要修改客户操作系统的代码,替换不可虚拟化的指令,将其改为超级调用。

- 优点

    - 1.带来更高的性能。首先是无须模拟运行敏感指令;其次,半虚拟化技术可以减少冗余的代码逻辑、数据拷贝、特权级切换等操作。
    - 2.缓解语义鸿沟(semantic gap)问题。不使用半虚拟化技术的时候,虚拟机监控器只能看到内存中的二进制数据,难以将这些二进制数据转化为有意义的语义。而半虚拟化技术允许虚拟机监控器获得虚拟机内部的状态,因此可以进一步提高资源的分配效率。

- 缺点

    - 半虚拟化技术需要修改客户操作系统的源码,尤其需要修改不同操作系统的不同版本时会带来较大的开发和调试成本。
    - 非开源的操作系统,难在其中添加半虚拟化的支持。
  • 硬件虚拟化技术

虚拟化的硬件扩展,就是从硬件上提供支持。比如新增一个 CPU 模式或者新增一层特权级等。

- Intel VT-x

    - 在已有的 CPU 特权级下新增了两个模式:根模式(root mode)和非根模式(non-root mode)。

虚拟机监控器运行在根模式下,而虚拟机运行在非根模式下。

    - 虚拟机监控器作为最高特权的管理软件,管理所有物理资源,并使用硬件虚拟化的功能为上层虚拟机提供服务。

为了管理虚拟机的硬件行为,Intel VT-x 为每一个虚拟机提供了虚拟机控制结构(Virtual Machine Control Structure,VMCS)。虚拟机监控器通过配置 VMCS 来管理虚拟机的内存映射和其他行为。

这种模式下,过去不会引起下陷的敏感指令也是可以被运行在根模式的虚拟机监控器捕获,继而实现相应的虚拟化操作。

- ARM v8.0

    - 引入了一个全新的特权级,EL2。EL2 是除 EL3 之外的最高特权级。虚拟机监控器运行在 EL2 特权级中,它控制所有的物理资源,并管理运行在 EL1 和 EL0 中的软件。
    - ARM v8.0 中实现对敏感指令的支持,并体现在以下两个方面

1.一部分敏感指令不再需要下陷,而是被直接重定向到虚拟硬件部件。这个是因为客户操作系统运行在 EL1 特权级中。比如 AArch32 中,客户操作系统直接运行在 EL1 内,因而 cps 指令可直接修改 PSTATE 寄存器,而不是被硬件直接忽略。

2.过去不可虚拟化架构中的敏感指令也都可以通过配置的方式实现下陷,从而可以被虚拟机监控器所捕获并处理。

比如,ARM 硬件虚拟化中为虚拟机监控器提供了一个名为 HCR_EL2 的系统寄存器。此寄存器的不同比特位决定了虚拟机的不同行为。比如,第 13 位就决定了虚拟机执行 wfi 指令是否会引起虚拟机下陷。如果虚拟机监控器能够捕获到这条指令(会引发下陷),那么则将 CPU 的控制权收回,继而调度其他虚拟机。

    - 在 ARM 硬件虚拟化中,硬件为 EL1 和 EL2 提供了两套系统寄存器。虚拟机可直接使用 EL1 这套寄存器而无须使用软件模拟的寄存器。虚拟机监控器则使用 EL2 这一套系统寄存器。如果虚拟机发生下陷,虚拟机监控器无须保存虚拟机在 EL1 中使用的系统寄存器。而此时,运行在 EL2 中的虚拟机监控器可以读写 EL1 和 EL0 中的任何寄存器。因此,在处理虚拟机下陷时,虚拟机监控器可以随时查看和改变虚拟机的寄存器状态。

需要注意的是,在发生 vCPU 上下文切换时,虚拟机监控器仍然需要将该 vCPU(或者说是虚拟机)在 EL0 和 EL1 中的所有系统寄存器以及通用寄存器都保存在内存之中,并将即将运行的 vCPU (或者说是虚拟机)的相关寄存器加载到物理寄存器中。(在 x86 中,当虚拟机执行敏感指令时,会触发虚拟机下陷,CPU 模式随之由非根模式切换为根模式。但是与 ARM 不同的是,x86 CPU 不同特权共享同一份系统寄存器。因此,当触发了下陷之后,硬件会讲 vCPU 中的所有系统寄存器都保存到 VMCS 中)

- ARM v8.1

    - Motivation:

ARM v8.0 新引入的 EL2 特权级只能运行 Type-1 的虚拟机监控器,不支持 Type-2 的虚拟机监控器,比如 KVM。KVM 和 Linux 耦合在一起(Type-2 的虚拟机高度依赖于宿主操作系统的功能),两者运行在同一特权级和内存空间之中。而 Linux 在开发时被假设运行在 EL1 中,因而只会使用 EL1 中的系统寄存器(比如 TTBR0_EL1、TTBR1_EL1),而这些寄存器在 EL2 中并不存在。因此,在 Linux 上运行虚拟机,那么必须将 KVM 的一部分功能从 Linux 中剥离出来,以 Lowvisor 的形式运行在 EL2 中。而宿主操作系统和其他 KVM 功能则依赖运行在 EL1 中(如图所示)。

此时,当虚拟机发生下陷时,会首先下陷到 EL2 之中。而 EL2 中的 KVM Lowvisor 在处理此次下陷的过程中可能需要使用 Linux 的功能,所以它将控制流又转回到 EL1 的 KVM 中,并调用 Linux 的相关功能。完成之后,控制流又从 Linux 回到 Lowvisor,并最终回到虚拟机之内。

而这样的架构设计造成了 KMV 和 Lowvisor 之间的多次特权级切换,因而对虚拟机的运行性能带来一定的开销。

    - ARM v8.1 进一步扩展了硬件虚拟化的功能,并且增加了 Virtualization Host Extension(VHE)。VHE 意味着可在 EL2 中运行完整的宿主操作系统。此时 KVM 的架构也发生了相应的转变(如图所示),KVM 和宿主操作系统 Linux 之间的交互不会带来任何特权级切换,从而优化了虚拟机的性能。

- 缺点

    - 物理主机的 ISA 和虚拟机 ISA 不同时,不能使用硬件虚拟化技术,只能选择软件技术。

内存虚拟化

基本概念

  • 为什么需要内存虚拟化

    • 系统虚拟化之后,一台物理主机上可同时运行多台虚拟机。此时,每个虚拟机中的客户端操作系统“看到的”物理地址空间也应该是从零开始连续增长,否则虚拟机无法正常运行(系统虚拟化之前,操作系统内核直接管理整个物理地址空间,看到的物理地址空间就是从零开始连续增长的)。

此外,虚拟机监控器不允许客户操作系统访问不属于它的物理内存区域。如果允许访问,那么将破坏了虚拟机的内存隔离,威胁整个系统安全。

  • 内存虚拟化需要满足的条件

    • 1.内存虚拟化需要给每台虚拟机提供从零地址开始连续增长的物理地址空间
    • 2.实现虚拟机之间的内存隔离,每台虚拟机只能访问分配给它的物理内存区域
  • 三种内存地址

    • 客户虚拟机地址(Guest Virtual Address,GVA):

进程和客户操作系统使用的地址,也就是内存管理中的虚拟地址

- 客户物理地址(Guest Physical Address,GPA):

虚拟机内使用的物理地址,不是真真实的、在总线中访存的地址

- 主机物理地址(Host Physical Address,HPA):

真正访存的物理地址,是 CPU 发送至总线进行访存的地址

  • 两种翻译

    • 第一阶段:客户虚拟地址转换为客户物理地址
    • 第二阶段:客户物理地址转换为主机物理地址
  • 优点

    • 1.可以为虚拟机提供一个从零地址开始连续增长的客户物理地址空间,客户操作系统“以为”自己仍然在管理物理地址空间。
    • 2.第二阶段地址翻译可提高主机物理内存的使用率。虚拟机监控器将不连续的主机物理地址映射成连续的客户物理地址,从而减少内存碎片。
    • 3.即使多个虚拟机请求的客户物理内存大小超过主机物理内存的大小,也可以通过内存虚拟化机制同时支持多台虚拟机的执行。
    • 4.实现虚拟机之间客户物理地址空间的安全隔离性,可通过第二阶段地址翻译过程限制每台虚拟机的地址范围,任何虚拟机不能读写其他虚拟机中的物理内存区域(除共享内存之外)。

实现机制

  • 影子页表机制

    • 问题

      • 硬件虚拟机技术出现之前,MMU 只存在一个页表。在非虚拟化环境时,操作系统内核中页表的使用分为三个阶段:
        1.静态配置,在进程被正式调度之前,内核首先为进程配置好一个页表,这个页表维护的是虚拟地址到物理地址的映射;
        2.页表安装,在决定调度一个进程之后,内核将此进程的页表基地址写入页表基地址寄存器;
        3.动态翻译,硬件 MMU 根据页表基地址寄存器指向的页表,动态地将虚拟地址翻译成物理地址,并完成内存访问。该过程完全由 MMU 完成,无须内核参与。

但是在虚拟环境内,这个页表已经被运行在 EL1 的虚拟机监控器所使用了。然而,客户操作系统也是需要使用页表实现第一阶段翻译的,此时在只有一个页表的情况下如何同时实现两种地址翻译呢?

- 基本思想

    - 

虚拟机监控器在静态配置阶段,将页表中客户虚拟地址到客户物理地址的映射改成客户虚拟地址直接到主机物理地址的映射即可。这样,即使 MMU 中只有一个页表,也可完成两个阶段的地址翻译。

也就是这样一个过程:操作系统写入页表基地址寄存器的时候会发生虚拟机下陷,这个下陷会被虚拟机监控器捕获,虚拟机监控器根据页表内容创建一个新的页表,新的页表和虚拟机的页表很相似只是将其中的客户物理地址改为主机物理地址。之后,虚拟机监控器将新页表基地址写入页表基地址寄存器。这样,动态翻译时,MMU 则根据页表中记录的映射,直接将客户虚拟地址翻译成主机物理地址。(需要结合软件模拟的方法来捕捉“安装页表”或“修改页表”的指令)

- 影子页表的含义 

    - 1.影子页表是虚拟机监控器为虚拟机“秘密”配置的一个页表,此页表不为虚拟机所见,对其完全透明,更不能被虚拟机所修改。
    - 2.影子页表的内容与客户虚拟机使用的页表高度相关,它的映射内容随着页表内容的改变而改变,就像“影子”一样。

- 具体步骤

    - 影子机制的使用需要配合我们在 CPU 虚拟化中学到的技术,比如通过软件模拟的方式使得敏感指令(写入页表基地址寄存器 TTBR0_EL1)引起下陷。
    - 1.操作系统在页表中配置客户虚拟地址到客户物理地址的映射。由于虚拟机内只能使用客户物理地址,因此它会将此页表的客户物理地址写入 TTBR0_EL1。
    - 2.虚拟机监控器通过软件模拟的方式使得系统寄存器写入操作引发虚拟机下陷。
    - 3.虚拟机监控器需要为每个虚拟机维护一个地址转换表,其中记录了客户物理地址到主机物理地址的映射关系。虚拟机监控器遍历客户操作系统需要安装的页表,并根据该页表和转换表的内容创建一个新的页表,其内容与客户操作系统的页表一一对应,只是将客户物理地址改写成主机物理地址,也就是影子页表包含的是客户虚拟地址到主机物理地址的映射关系。
    - 4.虚拟机监控器通过配置影子页表,将原页表所在的内存设置为只读权限。
    - 5.影子页表的主机物理地址被虚拟机监控器写入页表基地址寄存器中。
    - 6.虚拟机监控器恢复虚拟机的执行。

这样虚拟机使用的任何客户虚拟地址,将通过影子页表被直接翻译成主机物理地址。
由于在第四步的时候,原页表所在的内存设置为只读权限。所以,假如客户操作系统修改页表,将触发一次缺页异常,并下陷至虚拟机监控器。虚拟机监控器根据触发异常的客户虚拟地址,发现虚拟机试图修改页表。于是,虚拟机监控器将模拟此次修改操作,并将修改之后的结果同步到影子页表之中。

同理,如果客户操作系统试图更换页表,将再次触发上述建立影子页表的过程。

- 缺页异常处理流程

    - 1.发现缺页异常后,由于虚拟机运行在用户态,所以首先下陷到内核态并唤醒虚拟机监控器注册的缺页异常处理函数(这是因为虚拟机运行在用户态)。
    - 2.之后,虚拟机监控器查看引起此次下陷的客户虚拟地址并查询客户页表。如果客户页表中不存在与下陷的客户虚拟地址相关的映射,或者存在映射但是权限不够,则虚拟机监控器将这次缺页异常插入客户操作系统,直接调用客户操作系统注册的缺页异常处理函数(这个个人觉得又会触发下陷,因为涉及到了对页表的修改)。如果客户页表中存在相关页表项且权限足够,那说明这次异常是由于影子页表未与客户页表同步造成的。因此,虚拟机监控器只需要将客户页表的权限同步至影子页表中,再恢复虚拟机的执行即可。

- 优点

    - 地址翻译速度快。由于影子页表中记录的是客户虚拟地址到主机物理地址的直接映射,因此 MMU 只需要遍历一个页表即可完成两个阶段的地址翻译过程。即使发生了 TLB 未命中的情况,MMU 只需要遍历一遍影子页表即可。

- 缺点

    - 影子页表的建立和后续的每次更新都需要虚拟机监控器的介入,这不仅增加虚拟机监控器的实现复杂度,而且还会带来较大的性能开销。
    - 影子页表与页表一一对应,因此虚拟机监控器需要给每一个进程维护相对应的影子页表,这会带来一定的内存开销。
  • 直接页表映射机制(类似半虚拟化)

    • 基本思想

      • 影子页表技术的复杂性在于虚拟机监控器希望提供透明的虚拟机抽象,为了保证这个透明性,虚拟机监控器需要不断捕获客户操作系统对页表的修改,并同步影子页表中。

而直接页表映射机制旨在不再提供透明的抽象,而是让虚拟机知道自己运行在虚拟机监控器之上。这样,可以简化内存虚拟化的设计与实现,但是需要修改客户操作系统的代码。其实,这是半虚拟化思想在内存虚拟化中的应用,需要与半虚拟化技术提供的超级调用接口同时使用。

- VS 影子页表

    - 直接页表映射机制中不存在影子页表的概念,虚拟机内维护的页表将被直接安装在硬件 MMU 中,但是这里的页表不再使用客户物理地址,客户操作系统在这个页表中记录的是客户虚拟地址到主机物理地址的映射关系。
    - 假如虚拟机可以直接维护页表映射的话,虚拟机可能会恶意或无意地在页表中添加非法映射,从而可以访问其他虚拟机甚至是虚拟监控器的内存区域。因此,直接页表映射机制将虚拟机的页表设置为只读权限,从而阻止虚拟机直接修改页表页。

虚拟机必须使用虚拟机监控器提供的超级调用接口对页表进行修改。在接收到超级调用请求后,虚拟机监控器将检查此次需要添加或修改的映射是否合法,如是否使用了其他虚拟机的主机物理地址。(这里在修改页表的时候,需要发起超级调用,所以相当于原来的内核代码需要进行修改,而且客户操作系统发起超级调用也就意味着它是知道有虚拟机监控器的)

- 优点

    - 不需要再维护影子页表,虚拟机监控器的内存虚拟化模块的实现复杂度会降低。
    - 影子页表机制通过大量缺页异常将虚拟机对客户页表的修改“透明”地同步到影子页表中,这会带来较大的性能开销。而在直接页表映射机制中,虚拟机可将对页表的多次修改整合成一次系统调用,从而实现批量处理的效果,进而带来一定的 性能提升。

- 缺点

    - 需要修改客户操作系统代码,这个跟半虚拟化技术的缺点是一样的。
  • AArch64 中的两阶段地址翻译机制(硬件虚拟化机制)

    • 基本思想

      • 影子页表和直接页表映射可以在硬件中只有一个页表时实现内存虚拟化。而硬件虚拟化机制则是指从硬件上增加对页表的支持,比如支持两个页表。
      • 在 ARM 的硬件虚拟化扩展中,硬件在 EL2 特权级添加了第二阶段页表(stage-2 page table)。这个页表记录着客户物理地址到主机物理地址的映射关系。原先虚拟机内使用的页表被称为第一阶段页表,它记录着客户虚拟地址到客户物理地址的映射关系。第一阶段和第二阶段页表都可以被物理 MMU 识别,并都参与 MMU 的地址翻译过程。
        上图展示的是第二阶段页表的组织形式,该页表最多由 4 级页表构成,最高级被称为 L0,最低级(指向物理页)被称为 L3。每一级页表页的大小为 4KB,包含 512 个页表项,每一项是 8 个字节。每一个有效的页表项都存储了下一个页表页或内存页的主机物理地址,还存储了相关权限。
        在 ARM 硬件虚拟化中,虚拟化监控器在运行虚拟机之前,首先虚拟机第二阶段页表基地址(主机物理地址)写入 VTTBR_EL2 寄存器中,并将 HCR_EL2 系统寄存器的第 0 位(VM位)设置为 1,这表示将打开虚拟机中的第二阶段页表翻译机制。这样,虚拟机执行过程中的任何客户物理地址都会被硬件 MMU 通过 VVTBR_EL2 指向的第二阶段页表翻译成对应的主机物理地址,而这整个翻译过程无须虚拟机监控器的介入。
    • 两阶段的翻译过程

      • 硬件 MMU 先读取 TTBR0_EL1(或者TTBR1_EL1) 寄存器,也就是第一阶段的页表基地址寄存器,找到第一阶段页表的基地址。而这个基地址是一个客户物理地址,必须经过第二阶段页表的翻译才行(4次)。硬件 MMU 通过第二阶段翻译之后,就将这个客户物理地址转换为了主机物理地址,而这个主机物理地址其实是第一阶段 L0 页表页的主机物理地址。之后,再对此主机物理地址进行访存,取得第一阶段页表 L0 的页表页的内容(1次)。
      • 之后,MMU 根据客户虚拟地址中相应的偏移量,定位到第一阶段 L0 页表页中的相关项,并得到第一阶段 L1 页表页的客户物理地址。而这个客户物理地址又需要经过第二阶段的翻译,将其翻译成对应的主机物理地址(4次),并访问取得第一阶段 L1 页表页的内容(1次)。
      • 依次类推,第一阶段中的 4 次(第一阶段页表基地址、L0 得到的地址、L1 得到的地址、L2 得到的地址)都会经过上述的翻译过程,最终得到第一阶段 L3 页表页的内容。之后,根据客户虚拟地址中的偏移量找到相应的 L3 页表项,最终得到的是内存页的客户物理地址。而这个地址依旧需要经过第二阶段的翻译才能得到最终的主机物理地址(4次),并最终实现访存。
      • 假如发生了 TLB 未命中,通过上述的分析,可以看到完整地翻译一个客户虚拟地址需要经过 24 次访存操作。
      • 在使用两阶段页表翻译时,虚拟内的缺页异常处理分为两种情况

        • 1.客户虚拟机内发生的任何缺页异常,都将不再引起虚拟机下陷,硬件将会直接调用客户虚拟机注册的缺页异常处理函数,待处理完之后将恢复虚拟机的执行。
        • 2.当发生第二阶段页表相关的缺页异常之后,会产生虚拟机下陷,硬件调用虚拟机监控器注册的缺页异常处理函数,虚拟机监控器会检查引起异常的客户物理地址以及相关权限,并确定是否是因为未添加权限或权限不够导致,然后根据具体的情况进行处理。虚拟机监控器处理完成之后,恢复虚拟机的执行。
    • 优点

      • 1.第一阶段页表和第二阶段页表分开维护,客户操作系统在更新页表的时候不会引起虚拟机下陷,因此相对于影子页表来说,更新性能会更好。
      • 2.虚拟机监控器为虚拟机配置的第二阶段页表在虚拟机运行时也起作用,从而不需要为每一个进程单独配置一个页表,只需要给一个虚拟机配置一个页表即可。从而使得内存开销更小。
      • 3.影子页表机制中,每次缺页异常都会导致下陷到虚拟机监控器,它会检查该异常是由虚拟机导致的还是由虚拟机监控器导致的,从而选择相应的进行处理。而二阶段地址翻译中,第一阶段页表引起的异常将自动由虚拟机处理,第二阶段页表引起的异常将自动由虚拟机监控器处理,从而提升异常处理的性能。
    • 缺点

      • 在发生 TLB 未命中时,一个客户虚拟地址将会经过 24 次的内存访问,这带来了最大的性能开销。

但是,ARM 体系结果针对于此,对 TLB 技术做了相关的优化,使得 TLB 可以直接缓存客户虚拟地址到主机物理地址的映射。

换页

  • 简单的换页机制

    • 1.当虚拟机监控器决定将某一个内存页的数据拷贝到持久性的存储设备之前,它首先将内存页所属的虚拟机和客户物理地址信息保存起来。
    • 2.虚拟机监控器将该页的内存数据拷贝到存储设备。
    • 3.虚拟机监控器将此页所属的虚拟机的第二阶段页表的相关页表项设置为 INVALID。
    • 4.虚拟机监控器将此页设置为“空闲”或直接分配给其他虚拟机。
    • 5.如果虚拟机再次访问该页,将触发一次第二阶段缺页异常,并唤醒虚拟机监控器。此时,虚拟机监控器会查询第二阶段页表,发现此客户物理地址对应的页表项为 INVALID。之后,虚拟机监控器会查询该页对应的换页信息,最终将数据从存储设备中拷回内存,并更新第二阶段页表。然后恢复虚拟机的执行。
  • 内存气球机制

    • Motivation

      • 上述的简单的换页机制中存在语义鸿沟的问题。虚拟机监控器只能“看到”寄存器或内存中的二进制存储数据,它无法将这些二进制数据转换为有意义的虚拟机信息。具体来说,就是虚拟机监控器很难知道虚拟机内的哪些页在未来一段时间内不会被使用,也很难知道哪些页在下一刻会被使用。因此,虚拟机监控器难以高效地提供透明的换页机制来将真正不用的内存数据保存到存储设备之中。最极端的情况是:虚拟机监控器刚将某虚拟机的内存页换页到存储设备之中,客户操作系统就决定将这个页的数据保存到存储设备之中(这两个保存是不一样的,一个是虚拟机监控器在那控制,它的换页虚拟机是不知道的,它是从全部虚拟机的整体出发的;客户操作系统的保存是操作系统自己觉得需要换页保存了,才发出的申请)。此时,虚拟机监控器需要先将该页的数据从存储设备中加载回内存,并恢复第二阶段页表的映射;之后虚拟机读取该内存页的数据,并将该数据保存到存储设备中。

上述极端情况的出现主要是因为虚拟机内部和虚拟机监控器缺乏沟通机制。一种解决方法就是让客户操作系统通知虚拟机监控器哪些内存在未来一段时间内不会被使用。具体来说,可使用半虚拟化的方式为客户操作系统提供超级调用的接口,并修改客户操作系统内核中的代码,让它不断地或者在某一特定时刻通知虚拟机监控器:哪些内存在一段时间内不会被使用。通过这种方式,虚拟机监控器就可以有针对性地将这些内存页换页到存储设备之中。但是这种方式存在的一个弊端就是需要对内核做大量的修改。

- 基本思想

    - 在客户操作系统内核中插入一个伪装的驱动。该驱动不为内核其他模块提供任何有意义的功能,它只用来和虚拟机监控器交互:根据虚拟机监控器的要求,不断地使用客户操作系统内核的接口来申请或释放内存。之后,驱动将分配的内存的客户物理地址通知虚拟机监控器,这些内存页的数据会被虚拟机监控器保存到存储设备(这些内存页可能也是空闲页,即数据为0,那么可以省略保存到存储设备的步骤),而这些内存页将被交给其他虚拟机使用。

- 具体步骤

    - 假设虚拟机 2 需要更多的内存,此时虚拟机监控器需要给虚拟机 2 分配更多的内存,从而可能会将其他虚拟机所使用的页换出到存储设备中(但是其他虚拟机所使用的页在被换出之后又可能被虚拟机上的操作系统换入再换出,也就是上述提到的极端情况)。
    - 1.虚拟机监控器首先通知虚拟机1中的气球驱动,让该驱动调用客户操作系统提供的内存分配接口,分配大量的内存(气球膨胀),这些内存不会被气球驱动直接使用。
    - 2.得到这些内存之后,驱动会将这些内存的客户物理地址发送给虚拟机监控器,虚拟机监控器将这些翻译成主机物理地址,并把对应的内存中的数据保存到存储设备之中。

上述分配的内存都是操作系统内核维护的,未被任何模块使用的内存。这些内存在被气球驱动得到之后,在未来一段时间内都不会被其他模块所使用,因此可以安全地交给虚拟机监控器。

    - 3.之后虚拟机监控器,通知虚拟机2中的气球驱动(该气球驱动已经执行过气球膨胀的过程),气球驱动会将之前分配的内存通过接口释放给内核(气球收缩),而这些内存对于虚拟机 1 来说在未来一段时间内不会再被使用,即虚拟机 1 不会立即把这些页换入换出等。
    - 整体的理解:

假设虚拟机 2 需要更多的内存,此时虚拟机监控器需要给虚拟机 2 分配更多的内存,从而可能会将其他虚拟机所使用的页换出到存储设备中。(但是其他虚拟机所使用的页在被换出之后又可能被换入再换出,也就是上述提到的极端情况)。
此时,虚拟机监控器通知虚拟机 1 的气球驱动分配大量的内存,并将这些页存储到存储设备之后,这些页就是空闲状态的了,就可以被虚拟机 2 所使用了。
由于这些页是由虚拟机 1 的气球驱动所分配的,所以未来一段时间之内不会被虚拟机 1 的其他模块所使用。也就是说虚拟机 1 的操作系统不会再考虑这些页,也就不会再将这些页换入又换出啥的,即避免了上述的极端情况。

- 优点

    - 1.直接利用了操作系统内核提供的内存管理接口。
    - 2.气球驱动器的写法非常简单,仅仅需要调用操作系统提供的接口即可,无需对操作系统内核做较大的修改。

I/O 虚拟化

基本概念

  • Motivation

    • 操作系统的重要职责之一就是管理设备,即使运行在虚拟机内,它也想要管理设备。然而,出于安全性和资源利用率考虑,虚拟机监控器通常不允许虚拟机直接管理真实的设备,这个时候就需要 IO 虚拟化技术为虚拟机提供“假”设备,这样虚拟机就可以像操作真的设备那样操作虚拟设备。
  • IO 虚拟化的重要功能

    • 1.限制虚拟机对真实物理设备的直接访问。假如一个物理设备被多个虚拟机共享,那么一个虚拟机可以通过这个物理设备严重威胁其他虚拟机甚至是宿主机安全。
    • 2.IO 虚拟化会为每个虚拟机提供虚拟设备接口。比如 IO 虚拟化将虚拟机对虚拟的 1 号扇区的访问变成对物理设备对应扇区的访问。
    • 3.IO 虚拟化可以提高物理设备的资源利用率。一个虚拟机也许不能占满物理网卡的网络带宽,但是多个虚拟机同时使用网卡可以有效提高网卡的带宽利用率。

实现机制

  • 软件模拟(全虚拟化)方法

    • 基本思想

      • 借助模拟或硬件虚拟化的方法捕获原生驱动(即设备配备的未经修改的驱动程序)的硬件指令,之后在虚拟机监控器内模拟虚拟设备的行为,并为虚拟机提供 IO 服务。比如,对于磁盘设备而言,虚拟机监控器利用文件作为虚拟机的虚拟磁盘,它可以将虚拟磁盘的扇区位置映射为文件内的偏移量。

由于操作系统与设备交互有三种方法:内存映射 IO(Memory Mapped IO,MMIO)、直接内存访问(Direct Memory Access,DMA)、中断(interrupt)。因此,需要采用软件的方法模拟上述三种交互方法。

- QEMU/KVM 软件模拟实现网卡虚拟化的简要步骤

    - 1.客户操作系统的网络协议栈在内存中生成网络包数据,并调用网卡原生驱动。
    - 2.网卡驱动写入网卡 MMIO 地址,触发发送网络包的操作并生成一次 DMA 写请求(个人理解:写 MMIO 地址主要就是为了告诉网卡要发送网络包了。非虚拟化时这步之后就将由网卡负责了)。

由于 MMIO 的客户物理地址在第二阶段页表中被映射成了缺页,所以每次 MMIO 都会触发异常并下陷进入 KVM 中。

    - 3.KVM 读取引下陷的客户物理地址,发现虚拟机想要执行 DMA 写操作,于是将这次引发下陷的客户物理地址等信息写入与 QEMU 进程共享的内存区域,之后将控制流转至用户态的 QEMU 中。
    - 4.QEMU 根据引发 MIMIO 的客户物理地址发现访问的 MMIO 地址属于某种虚拟网卡。因为 QEMU 进程和虚拟机共享内存,所以 QEMU 可以读取到该 MMIO 内存中存储的 DMA 命令与地址。注意此时的 DMA 不是物理 DMA 而是通过 QEMU 模拟的 DMA 操作:QEMU 直接从虚拟机内存中读取第一步中生成的网络包数据。

这一步相当于 QEMU 提供虚拟网卡的模拟,上述的 QEMU 通过模拟 DMA 的方式从虚拟机内存中读取网络数据包,并之后借助物理网卡发送出去。由于不同网卡中 MMIO 寄存器的布局以及功能各不相同,因此 QEMU 需要针对不同的网卡(RTL8139、IntelE1000)提供相应的模拟。

    - 5.用户态 QEMU 发起系统调用,将数据传入宿主机内核。
    - 6.内核将数据发送至物理网卡的驱动程序,并借助物理网卡发送出去。
    - 7.控制流回到用户态 QEMU
    - 8.QEMU 发现数据包都已经发送出去了,就通知 KVM 恢复虚拟机的执行,并且 KVM 向虚拟机插入虚拟中断。
    - 

- 优点

    - 软件模拟的 IO 虚拟化通常作用于机器指令层,它不要求对驱动程序做任何修改,客户操作系统可使用原生驱动程序与设备进行交互。(全虚拟化的范畴)

- 缺点

    - 每次 MMIO 读写都会造成虚拟机下陷,所以该方法会带来较大的性能开销。
  • 半虚拟化方法

    • 基本思想

      • 虚拟机不再使用与特定设备对应的原生驱动程序,而是安装更为高效的前端驱动程序。前端驱动程序不再使用会引起大量虚拟机下陷的 MMIO 接口,而是使用 PV(Para-Virtualization)接口与虚拟机监控器中的后端驱动进行交互。前后端驱动不仅可以通过共享内存的方式进行数据传输,还可以利用批处理的方式将多次 IO 请求整合为一次。
    • 基本步骤

      • QEMU/KVM 架构下的 Virtio

        • 前端驱动和后端驱动之间的共享内存机制为 Virtqueue,即以队列的形式组织前后端驱动之间的共享内存,前端驱动和后端驱动分别从队列两端读取和写入数据。
        • 1.网络包数据产生之后,前端驱动多次将网络包数据按序写入 Virtqueue。
        • 2.前端驱动发起超级调用,造成虚拟机下陷,控制流进入 KVM。
        • 3.KVM 读取超级调用参数,并将控制流转发至用户态的 QEMU 进程
        • 4.QEMU 根据 KVM 传入的信息,直接从共享的 Virtqueue 中读取描述符信息,得到这些包所在的客户物理地址。QEMU 将这些地址转换成对应的主机物理地址之后,并最终得到网络包的数据。
        • 5.用户态的 QEMU 发起系统调用,并将数据传入宿主机内核。
        • 6.内核将数据发送至网卡驱动,并借助物理网卡发送出去。
        • 7.控制流回到用户态 QEMU
        • 8、9.KVM 恢复虚拟机的执行,并插入虚拟中断。
      • Linux vhost 架构

        • 由于驱动较少且简单,所以这种方法是直接将后端驱动运行在 EL2 中。

这种方式由于减少了 KVM 和 QEMU 之间的特权级切换所以性能得到了进一步的提升。

        - 

- 优点

    - 软件模拟的方法需要使用设备提供的 MMIO 接口。客户操作系统每次读写 MMIO 都会造成虚拟机下陷,而且多次 MMIO 访问只能传输少量的数据,这带来了极大的性能损失。而在半虚拟化方法中,多次的网络包数据可被同时放置于共享内存中,而这些数据的传输可以只使用一次超级调用。从而减少了虚拟机下陷的次数,提升了 IO 虚拟化的性能。
    - 半虚拟化方法中同一类设备通常共享一组前后端驱动,不再需要为具体的设备维护对应的驱动,减少了驱动的数量。
    - PV 接口使得前后端驱动的实现复杂度降低,因而更不易包含安全漏洞。

- 缺点

    - 为了添加前端驱动,需要修改客户操作系统的源码。对于闭源操作系统来说,是有很大的困难的,但是 Virtio 已成为标准,闭源的操作系统提供对 Virtio 的支持即可。
  • 设备直通

    • 基本思想

      • 虽然半虚拟化的方法已经具备较好的性能了,但是在数据传输的时候还是会发生虚拟机下陷(超级调用),它的性能还是无法与直接使用物理设备相比。

设备直通(device passthrough,也可以被翻译成设备透传)的方式就是让虚拟机来直接管理物理设备。相比全/半虚拟化,直接管理的方式,由于不会有虚拟机下陷等,所以大大提升了 IO 虚拟化的性能。

- IOMMU

    - Motivation

        - 当虚拟机和设备直接交互之后,驱动程序发起 DMA 操作时使用的地址只能是物理设备能够理解的主机物理地址(不能是客户物理地址,因为没了虚拟机监控器的这一层翻译了),那么这样会带来一定的安全问题。比如虚拟机 A 的驱动程序请求一次物理设别的 DMA 操作,这个操作要求将数据拷贝到某个主机物理地址处。假如这个主机物理地址属于其他虚拟机,那么就会存在一定的风险。所以不能允许虚拟机直接使用主机物理地址和物理设备交互,这会威胁整个系统的内存安全。

    - 基本思想

        - 使用 IOMMU 来解决这个问题,IOMMU 可以将客户物理地址转换为主机物理地址(提供了第第二阶段地址翻译机制),在这里也就是相当于虚拟机可以给设备一个客户物理地址,设备只需要使用这个客户物理地址去访存即可,而这个客户物理地址会被 IOMMU 转换为主机物理地址。

由于 IOMMU 提供了第二阶段翻译机制,因此可以在翻译的过程进行权限检查,从而组织了直通物理设备对任意主机物理地址的非法访问。

但是,虚拟机监控器首先需要为此设备配置 IOMMU 第二阶段页表,而在该页表中维护客户物理地址到主机物理地址的映射关系。这样,当设备进行 DMA 操作的时候,DMA 的所有客户物理地址都会被 IOMMU 转换成主机物理地址。并且在虚拟机试图通过 DMA 访问不属于它的内存区域时,由于权限不足或缺页等问题会导致错误,进而通知虚拟机监控器,交由它进行处理。

    - 具体实现

        - Intel VT-d
        - AMD AMD-Vi
        - ARM SMMU(System MMU)

            - 与 MMU 中存储着两阶段页表基地址类似,SMMU 内存储着一个名为 Stream Table 的数据结构。这个数据结构为每一个设备都维护了两阶段页表基地址,也就是以表的方式进行组织的,表中每项对应一个设备,名为 STE(Stream Table Entry)。STE 中的 S2TTB 指向设备使用的第二阶段页表基地址,STE 中的 S1ContextPtr 是一组 Context Descriptor 列表的指针,每个 Context Descriptor 内包含两个第一阶段页表地址(与AArch64 中的 TTBR0_EL1 和 TTBR1_EL1 类似)。
            - 虚拟机监控器在将物理设备直通给虚拟机管理前,需要根据情况配置第二阶段页表,在此页表中配置该设备所能访问的主机物理地址范围和相关权限。
            - 1.驱动发起一次 DMA 操作,并指定此次 DMA 所需访问的内存地址、大小、属性(读或者写)
            - 2.设备进行相应的操作,SMMU 根据设备标识符定位 Stream Table 中对应的 STE
            - 3.根据 STE 的信息获得第一阶段和第二阶段页表的基地址
            - 4.进入地址翻译阶段,首先根据 DMA 地址等信息查询 TLB,若 TLB 中已存在翻译过的地址映射,那么直接得到最终的主机物理地址,然后直接跳到第 6 步,若 TLB 未命中,则进入第 5 步。
            - 5.根据页表和 DMA 地址,进行相应的地址翻译过程,并得到主机物理地址。
            - 6.根据主机物理地址,读/写内存。

- SR-IOV

    - Motivation

        - 一台物理主机中能够运行的虚拟机数目通常较多,假设虚拟机都独占一个完整的物理设备,那么这会大大增加经济成本。假如虚拟机不都独占一个完整的物理设备,那么有些虚拟机就不能享受设备直通的优越性能。另外,一个虚拟机独占物理设备不利于提高物理资源的利用率,虚拟机一般无法使用完物理设备的所有资源。

    - 基本思想

        - 使用 SR-IOV(Single Root IO Virtualization)技术,这是一个标准化的规范。满足该规范的设别能够在硬件层面实现 IO 虚拟化,也就是物理设备可在内部创建出一个真身和多个分身,真身被称为 PF(Physical Function),分身被称为VF(Virtual Function)。

虚拟机监控器使用 PF 创建和管理 VF,并将每个 VF 直通给一个虚拟机管理。虚拟机通过 DMA、MMIO 等方式与 VF 直接交互,并且在数据传输的过程中不会下陷到虚拟机监控器。设备在硬件层面实现了 VF 之间的隔离,并且也需要使用 IOMMU 来限制每个 VF 可以访问的主机物理地址。

    - 优点

        - 使用 SR_IOV 和 IOMMU 之后,每个虚拟机可独占式地使用直通设备,在数据传输过程中避免了虚拟机下陷,从而提升了 IO 虚拟化。

    - 缺点

        - 由于 IO 过程完全绕开了虚拟机监控器,虚拟机监控器无法在任意时刻检查虚拟机的状态,因而难以支持虚拟机热迁移以及虚拟机安全自省等功能。
  • 中断虚拟化

中断基本概念

  • 中断是设备与操作系统交互必不可少的机制。在完成某任务后,设备可通过中断异步通知 CPU 以便其进行后续操作。如当网卡设备收到网络包,它可以发送中断异步通知 CPU,CPU 调用相应的中断函数进行处理。

虚拟化中的中断

  • 物理中断

    • 由物理设备直接产生,交给虚拟机监控器处理。一般来说,虚拟机监控器不允许设备直接将物理中断发送给虚拟机,因为这个中断不一定就跟这个虚拟机相关。
  • 虚拟中断

    • 虚拟机监控器(或者 ITS)产生并传递给虚拟机

实现机制

  • 软件模拟的方式

    • 基本过程

      • 软件模拟方法中的所有中断寄存器本质上都是内存中的状态,这些虚拟寄存器的状态改变需要由虚拟机监控器维护。对于虚拟机而言,寄存器所在的内存必须都设置为不可访问。

因此,当虚拟机需要访问相关寄存器时(如读取 ICC_IAR1_EL1 寄存器来获得当前中断号),将触发异常并进入虚拟机监控器。之后,虚拟机监控器模拟寄存器的访问过程(将读取结果写入目标寄存器)。并且在插入虚拟中断时,虚拟机监控器直接调用虚拟机内的中断处理函数。之后,虚拟机会读取 ICC_IAR1_EL1 寄存器获取中断号,并处理中断。

    - 个人理解,上述提到的寄存器都是物理寄存器。虚拟机在访问这些物理寄存器的时候会触发异常。然后这些物理寄存器的状态将由虚拟机监控器改变的,而不是由设备直接改变的。之后,虚拟机基于这些寄存器状态(由虚拟机监控器指定)来进行中断处理。

其实就是一套物理寄存器,对于虚拟机来说,寄存器的内容将由虚拟机监控器来指定。

- 缺点

    - 较多的虚拟机下陷,性能较差。
  • 硬件虚拟化方式—-以 ARM GIC(Generic Interrupt Controller)第 2 版为例

    • 基本思想

      • 虚拟机监控器可以为虚拟机配置硬件实现的虚拟寄存器(相当于每个虚拟机有一套硬件提供的虚拟机寄存器),虚拟机可使用这些寄存器控制中断的行为(例如中断的开关)。虚拟机在访问这些寄存器时不会产生任何虚拟机下陷。此外,虚拟机监控器可使用 GIC 提供的接口插入中断。
    • 虚拟中断插入过程

      • 1.当物理设备产生并发送物理中断到某一个物理核时,如果此时虚拟机正在运行,那么该虚拟机将会下陷。
      • 2.之后将由虚拟机监控器处理该物理中断,在处理完该物理中断后,虚拟机监控器判断出该物理中断相关 IO 数据需要传输给某虚拟机。因此,它先将数据拷贝到虚拟机内存区域,并在相应寄存器中(GICH_LR_EL2)写入该物理中断对应虚拟中断的信息,例如中断号和优先级。
      • 3.之后,虚拟机监控器恢复 vCPU 的执行,这个时候硬件自动根据相应寄存器中(GICH_LR_EL2)的信息向 vCPU 插入一个中断,并调用对应的中断处理函数。

而在虚拟机处理该中断的过程中,写入寄存器等操作不会触发任何虚拟机下陷。

- ITS

    - Motivation

        - 上述的方式虽然大大提升了虚拟机处理中断时的性能,但每次触发物理中断时依然会引起虚拟机下陷(虚拟机监控器需要处理这个物理中断)。尤其当设备已经直通给虚拟机时,设备产生的物理中断还是会产生的下陷,而这个下陷会对虚拟机的 IO 性能产生极大的影响。

    - 基本思想

        - 为了解决上述的问题,一种方式就是:物理设备可以直接向虚拟机发送物理中断(不会让虚拟机下陷)。在 AArch64 架构中,这种中断由 GIC 的 ITS(Interrupt Translation Service)实现。

    - 基本过程

        - 1.虚拟机监控器会配置 ITS 中的中断翻译表,将直通设备的物理中断映射成某个虚拟中断。
        - 2.当直通设备发送物理中断后。GIC 查询中断翻译表等信息,并将物理中断的信息翻译成对应的虚拟中断。
        - 3.GIC 直接将这个虚拟中断插入对应的虚拟机,并调用客户操作系统注册的中断处理函数。

    - 优点

        - 引入 ITS 之后的中断处理过程和非虚拟化场景下的过程完全一致,不会产生虚拟机下陷。在设备直通情况下,该技术可以让物理中断的产生、翻译、插入和处理都不需要虚拟机监控器参与,完全由设备和 GIC ITS 配合完成,从而进一步提升中断虚拟化的性能。

[[Linux-QEMU/KVM]]

KVM

  • 运行在 Linux 内核中的硬件虚拟化管理模块,可以将其理解为硬件虚拟化机制的驱动程序。用户态的程序通过 KVM 暴露的用户态接口来使用硬件虚拟化的功能。通过这些 API ,用户态程序可以完成创建虚拟机和虚拟内的 vCPU、设置 vCPU 的寄存器状态等。使用这些 API,可以仅仅使用短短几十行代码,即可创建一个非常简单的虚拟机监控器。

如上图所示,在用户态程序运行之前,KVM 通过 Linux 在 /dev 目录下注册并初始化了 kvm 设备文件。之后,用户态程序可通过 ioctl 等函数与 /dev/kvm 设备文件交互,以调用内核 KVM 的 API。KVM 收到 API 调用后,在 Linux 内核中创建相应的虚拟机、vCPU、第二阶段页表等。

  • KVM 的实现包含两部分

    • 1.向用户态提供驱动 API 的支持,允许用户态程序(如 QEMU)管理虚拟机相关的状态。
    • 2.运行虚拟机并处理虚拟机下陷。

QEMU

  • QEMU 是一个运行在用户态的虚拟机监控器,在硬件虚拟化出现之前 QEMU 主要通过软件模拟的方式运行虚拟机,在硬件虚拟化出现之后,QEMU 就通过 Linux 中的 KVM 来使用硬件虚拟化机制,从而提升了虚拟机的性能。目前,从整体框架上来看,QEMU 其实就是一个使用了 KVM 提供的 API 来运行和管理虚拟机的虚拟机监控器。当然,除此之外,QEMU 还支持不同设备的 IO 虚拟化,并且支持多 vCPU 的支持以及虚拟机迁移等。
  • QEMU 使用 KVM 运行虚拟机的过程

    • QEMU 其实是一个进程,该进程内的每一个用户态线程负责运行虚拟机内的一个 vCPU。具体而言,一个线程会管理一个对应的 vCPU 的所有寄存器状态,当线程调用 KVM_RUN 接口时,KVM 将对应的 vCPU 的所有寄存器状态都加载到硬件中,并开始执行。
    • 对于 Linux 内核来说,调度用户态线程可以由 Linux 的调度机制来管理,而调度用户态线程相当于调度其对应的 vCPU。因此,这种方式巧妙地复用了 Linux 的调度机制,从而不需要为虚拟化重新实现一套新的调度算法(Linux 中线程和进程使用的都是 task_struct)。
    • 作为一个用户态进程,QEMU 拥有自己的虚拟地址空间;另外,作为虚拟机监控器它还需要为虚拟机分配客户物理内存。但是,这些虚拟机内存本质上都是 QEMU 进程内部的内存,因此 QEMU 可以直接读写虚拟机的内存数据。这不仅加速了 IO 虚拟化的数据传输,而且还方便 QEMU 实现虚拟机热迁移等功能。
    • QEMU 既可以提供设备的模拟,也可以提供半虚拟化的设备模型,还可以使用设备直通的方式。

KVM 和 QEMU 分离的好处

  • 可针对不同场景设计和实现专用的用户态虚拟机监控器,而无需要修改提供机制的 KVM。如亚马逊为无服务计算设计的 Firecracker,它可以完全替代 QEMU,但是它仍然使用了 KVM 提供的硬件虚拟化机制。
  • KVM 是机制,QEMU 是策略。将机制和策略分离,可降低运行在最高特权级的 KVM 代码复杂度,而将复杂的策略逻辑运行在较低特权级,从而一定程度上提升了系统的可靠性。

巨人的肩膀

  1. 《现代操作系统原理与实现》 .陈海报,夏虞斌
程序锅 wechat
欢迎关注微信公众号【一口程序锅】,不定期的技术分享、资源分享。
让我多买本书学习学习
0%