虚拟化概览

Fuchsia 的虚拟化堆栈能够运行客户机操作系统。Zircon 实现了一个 Type-2 Hypervisor,它可以公开系统调用,使用户空间组件能够创建和配置 CPU 和内存虚拟化。虚拟机管理器 (VMM) 组件基于 Hypervisor 构建,通过定义内存映射、设置 trap 以及模拟各种设备和外围设备来组装虚拟机。然后,客户机管理器组件位于 VMM 顶部,用于提供客户机专用二进制文件和配置。Fuchsia 目前支持 3 种访客软件包;一种未经修改的 Debian 客户机、Zircon 客户机,以及基于 Termina 的 Linux 客户机。

启用了 VMX 的基于 Intel 的 x64 设备以及可启动到 EL2 的大多数 arm64(ARMv8.0 及更高版本)设备均支持 Fuchsia 虚拟化。值得注意的是,目前不支持 AMD SVM。

显示虚拟化组件的示意图

Hypervisor

Hypervisor 公开系统调用,以允许创建内核对象来支持虚拟化。创建新的 Hypervisor 对象的系统调用要求调用方有权访问 Hypervisor 资源,以便组件创建虚拟机的能力由产品控制。换句话说,必须为 Fuchsia 组件授予创建客户机操作系统的权限,以便产品能够限制哪些组件能够利用这些功能。

CPU 虚拟化

zx_vcpu_create 系统调用会创建一个新的虚拟 CPU (VCPU) 对象,并将该 VCPU 绑定到调用线程。然后,VMM 可以使用 zx_vcpu_{read|write}_state 系统调用来读取和写入该 VCPU 的架构寄存器。zx_vcpu_enter 系统调用是一个阻塞系统调用,用于将上下文切换到客户机,从 zx_vcpu_enter 的返回表示上下文切换回主机。换言之,如果 zx_vcpu_enter 中当前没有线程,则访客上下文中不会执行任何内容。zx_vcpu_read_statezx_vcpu_write_statezx_vcpu_enter 全部必须从调用 zx_vcpu_create 的同一线程进行调用。

zx_vcpu_kick 系统调用的存在以允许主机明确请求 VCPU 退出并导致对 zx_vcpu_enter 的任何调用返回。

内存和 IO 虚拟化

zx_guest_create 系统调用会创建新的客户机内核对象。重要的是,此系统调用会返回一个表示客户机的物理地址空间的虚拟内存地址区域 (vmar) 句柄。然后,VMM 可以通过将虚拟内存对象 (vmo) 映射到此 vmar 来提供客户机“物理内存”。由于此 vmar 表示客户机-物理地址空间,因此到此 vmar 的偏移量将对应于客户机-物理地址。例如,如果 VMM 希望公开 Guest-Physical 地址范围 [0x00000000 - 0x40000000) 的 1 GiB 内存,则 VMM 将创建一个 1GiB vmo 并将其映射到偏移量为 0 的 Guest-Physical vmar 中。

此访客物理地址 vmar 使用第二级地址转换 (SLAT) 实现,这允许 Hypervisor 定义主机-物理地址 (HPA) 到访客-物理地址 (GPA) 的转换。然后,客机操作系统能够安装自己的页表,用于处理从客户机虚拟地址 (GVA) 到客户机物理地址的转换。

显示 2 级地址转换的图表

借助 zx_guest_set_trap 系统调用,VMM 可以安装用于设备模拟的 trap。客户机可以使用内存映射 I/O (MMIO) 与硬件连接,这涉及客户机使用与访问内存相同的指令来读取和写入设备。对于 MMIO,SLAT 中不存在设备 GPA 的映射,这会导致客户机陷入 Hypervisor。

x86 提供了一种为 IO 设备寻址的替代方法,称为端口映射 I/O (PIO)。使用 PIO 时,客户机将使用备用指令来访问设备,但这些指令仍然会导致客户机陷入 Hypervisor 进行处理。

如何处理 trap 因创建的 trap 类型而异:

ZX_GUEST_TRAP_MEM - 为 MMIO 设置 trap。对与此 trap 关联的客户机-物理地址空间中的地址范围执行读取或写入操作会导致 zx_vcpu_enter 系统调用返回到 VMM,然后该系统调用负责模拟访问、更新 VCPU 寄存器状态,然后再次调用 zx_vcpu_resume 以返回客户机。

ZX_GUEST_TRAP_IO - 与 ZX_GUEST_TRAP_MEM 类似,不同之处在于,该 trap 不是在客户机物理地址空间中设置 trap,而是将安装到处理器的 IO 空间中。如果架构不支持 PIO,则会失败。

ZX_GUEST_TRAP_BELL - 为 MMIO 设置异步 trap。当客户机向与此 trap 关联的客户机物理地址范围写入数据时,Hypervisor 会在与此 trap 关联的端口上将消息加入队列,并立即恢复 VCPU 执行,而不返回用户空间,而不会使 zx_vcpu_enter 返回 VMM。这可用于模拟旨在使用此模式的设备。例如,Virtio 设备允许客户机驱动程序通过写入客户机物理内存中的特殊页面来通知虚拟设备有工作要完成。

不支持在“IO”聊天室中设置异步 Trap。不支持从设置了 ZX_GUEST_TRAP_BELL 的区域读取数据。

陷阱处理

通常,VCPU 线程的大部分时间都处于阻塞状态,zx_vcpu_enter,这意味着它在客户机上下文中执行。如果从该系统调用返回 VMM,则表示发生了错误,或者更常见的情况是,VMM 需要干预才能模拟某些行为。

为了演示这一点,我们考虑几个具体示例来说明 VMM 如何处理 trap。

MMIO 同步陷阱示例

以 ARM PL011 串行端口模拟为例。请注意,虽然这实际上是 ARM 专用设备,但 ARM 和 x86 上会以类似方式处理 trap。

首先,VMM[0x808300000 - 0x808301000) 的访客物理地址范围上注册同步 MMIO 陷阱,以告知 Hypervisor 对此区域的任何访问都必须导致 zx_vcpu_enter 将控制流返回到 VMM

接下来,VMM 将在一个或多个 vCPU 上调用 zx_vcpu_enter,以将上下文切换到客户机。在某些时候,PL011 驱动程序会尝试从 PL011 设备中的串行端口控制寄存器 UARTCR 寄存器读取数据。该寄存器位于偏移量 0x30 处,因此对应于本例中的访客物理地址 0x808300030

由于针对访客实际地址 0x808300030 注册了陷阱,因此该读取操作会导致访客陷入 Hypervisor 进行处理。Hypervisor 可以观察到此访问权限具有关联的 ZX_GUEST_TRAP_MEM,并通过从 zx_vcpu_enter 返回并提供 zx_port_packet_t 中包含的陷阱的详细信息,将控制流传递给 VMM。然后,VMM 可以使用访问的访客-实体地址,将其与相应的虚拟设备逻辑相关联。在这种情况下,设备在成员变量中维护该寄存器值。

// `relative_addr` is relative to the base address of the trapped region.
zx_status_t Pl011::Read(uint64_t relative_addr, IoValue* value) {
  switch (static_cast<Pl011Register>(relative_addr)) {
    case Pl011Register::CR: {
      std::lock_guard<std::mutex> lock(mutex_);
      value->u16 = control_;
      return ZX_OK;
    }
    // Handle other registers...
  }
}

这将返回一个 16 位值,但我们仍需将此结果公开给客户机。由于客户机已执行 MMIO,因此客户希望结果位于加载指令中指定的任意寄存器中。此操作通过使用 zx_vcpu_read_statezx_vcpu_write_state 系统调用将目标寄存器的值更新为模拟的 MMIO 结果来实现。

显示同步 MMIO trap 的示意图

陷阱示例

接下来,我们将演示铃铛陷阱的操作方法。在这种情况下,我们在主 VMM 之外的组件中实现 Virtio Device。在初始化期间,VMM 会请求 Virtio Device 寄存器 Bell 本身进行陷阱,以便将 trap 传送到 Virtio Device 组件而不是 VMMVirtio Device 完成任何 Trap 设置后,VMM 会开始使用 zx_vcpu_enter 执行 VCPU,并且控制流会转移到客户机。

在某些情况下,客户机驱动程序会对 Virtio Device 已捕获的访客实体地址发出 MMIO 写入操作。此时,客户机会从客户机上下文陷入到 Hypervisor,这会导致使用 zx_port_packet_tVirtio Device 传送通知。值得注意的是,在这种情况下,zx_vcpu_enter 永远不会在处理此 trap 期间返回,并且 Hypervisor 可以快速将上下文切换回客户机,从而最大限度地缩短 VCPU 被阻塞的时间。

Virtio Device 收到 zx_port_packet_t 后,便会采取设备专属步骤来处理该 trap。这通常涉及直接对客户机物理内存执行读写操作,但也不会阻止 VCPU 执行。设备完成请求后,可以使用 zx_vcpu_interrupt 发送中断来通知客户机中的驱动程序。

由于绝大多数通信是使用共享内存(而不是使用同步 trap)完成的,因此 Virtio 设备比严重依赖同步 trap 的设备效率高得多。

显示异步 MMIO 陷阱的示意图

Trap 处理的架构差异

虽然在大部分捕获 (trap) 处理方面相同,但在响应 trap 所需的操作方面,存在一些重要区别,具体取决于底层硬件支持。最值得注意的是,在 ARM 上,由硬件生成的底层数据中止会提供一些有关访问的解码信息,我们可以转发到用户空间(例如:访问大小、读取/写入、目标寄存器等)。在 Intel 上,不会发生这种情况,因此 VMM 需要进行一些指令解码来推断出相同的信息。

中断虚拟化

Fuchsia 通过在内核中进行 LAPIC/GICC 模拟和在用户空间中进行的 I/OAPIC/GICD 模拟来实现某些平台所谓的“拆分 irqchip”。用户空间 I/OAPIC 和 GICD 使用 zx_vcpu_increment 系统调用将中断转发到目标 CPU。

虚拟机管理器 (VMM)

虚拟机实例 (VMM) 是使用 Hypervisor 系统调用构建和管理虚拟机以及执行设备模拟的用户空间组件。VMM 使用提供给它的 GuestConfig FIDL 结构构建虚拟机,该结构包含有关应将哪些设备提供给虚拟机,以及客户机内核、ramdisk 和块设备资源的配置。

概括来讲,VMM 使用 Hypervisor 系统调用来组装虚拟机,以创建客户机和 VCPU 内核对象。它通过创建 VMO 来分配客户机 RAM,并将其映射到客户机物理内存 vmar。它使用 zx_guest_set_trap 注册用于虚拟硬件模拟的 MMIO 和 port-io 处理程序。VMM 可模拟 PCI 总线,并可将设备连接到该总线。它会将客户机内核加载到内存中,并使用客户机内核所需的各种资源(例如设备树 blob 或 ACPI 表)设置启动数据。

内存

VMM 会分配一个 vmo 用作客户机-物理内存,并将该 vmo 映射到客户机-物理内存 vmar(由 zx_guest_create 创建)。在客户机-物理内存 vmar 中对内存进行寻址时,我们将这些地址称为“客户机-物理地址”(GPA)。VMM 还会将同一 vmo 映射到其进程地址空间,以便可以直接访问此内存。在 VMM 的 vCPU 中对内存进行寻址时,我们将这些地址称为“主机虚拟地址”(HVA)。VMM 能够将 GPA 转换为 HVA,因为它既知道客户机内存映射,也知道自己的客户机内存映射的地址。

Virtio 设备和组件

许多设备使用虚拟 I/O (Virtio) 通过 PCI 向客户机公开。Virtio 规范定义了一组设备,它们通过高度依赖 DMA 对客户机物理内存的访问并最大限度地减少同步 IO 陷阱的数量,在虚拟化上下文中高效运行。为了提高设备之间的安全性和隔离性,我们在每个 Virtio 设备各自的 zircon 进程中运行,并且仅路由该组件所需的功能。例如,仅为 Virtio 块设备提供支持虚拟磁盘的特定文件或设备的句柄,而 Virtio 控制台只能访问串行流的 zx::socket。

VMM 与设备之间的通信是使用 fuchsia.virtualization.hardware FIDL 库完成的。对于每台设备,都有一小段代码关联到 VMM(称为控制器),它充当这些 FIDL 服务的客户端,并在启动期间连接到用于实现设备的组件。每个设备实例都有一个进程,因此,如果虚拟机有 3 个 Virtio 块设备,那么 3 个 zircon 进程中将有 3 个控制器实例和 3 个 Virtio 块组件。

Virtio 设备按照驻留在客户机物理内存中的共享数据结构的概念运行。客户机驱动程序将在启动时分配和初始化这些结构,并为 VMM 提供指向客户机物理内存中这些结构的指针。当驱动程序想要通知设备它已向这些结构发布了新工作时,它会写入到客户机物理内存中特定于设备的特殊“通知”页面,设备可以根据写入此“通知”页面的偏移量推断特定事件。每个设备组件都将为此区域注册一个 ZX_GUEST_TRAP_BELL,以便 Hypervisor 将这些事件直接转发到目标组件,而无需通过 VMM 弹跳。然后,设备组件可以通过其 HVA 读取这些结构,从而直接读取和写入这些结构。

正在启动

VMM 不提供任何客户机 BIOS 或固件,而是直接将客户机资源加载到内存中,并将启动 VCPU 配置为直接跳转到内核入口点。而该详情则取决于正在加载的内核。

Linux 访客

对于 x64 Linux 客户机,VMM 会根据 Linux 启动协议将可启动的内核映像(例如:bzImage)加载到客户机物理内存中,并使用其他内核资源(ramdisk、内核命令行)更新 Real-Mode 内核标头和零页VMM 还会生成并加载一组 ACPI 表,用于描述提供给客户机的模拟硬件。

Arm64 Linux 客户机的行为方式类似,只是我们遵循 arm64 启动协议并提供设备树 blob (DTB),而不是 ACPI 表。

锆石访客

VMM 还支持根据 Zircon 的启动要求启动 Zircon 客户机。如需详细了解 zircon 靴子如何操作,请参阅此处。

邀请对象管理员

Guest Manager 组件的作用是将客户机二进制文件(内核、ramdisk、磁盘映像)与配置(要启用的设备、客户机内核配置选项)打包在一起,并在启动时将它们提供给 VMM

树内有 3 位访客管理员,其中两个相当简单,另一个比较高级。简单的访客管理器没有任何客户机专用代码,只有传递给 VMM 的配置和二进制文件。然后,这些客户机通过虚拟控制台或虚拟帧缓冲区使用这些客户机。

简单访客管理器:ZirconGuestManager DebianGuestManager

更高级的 Guest Manager 是 TerminaGuestManager,它使用在 Virtio Vsock 上运行的 gRPC 服务提供其他功能。EndinaGuestManager 具有额外的功能,可连接到这些服务并提供更多功能(在客户机中运行命令、装载文件系统、启动应用)。

如需详细了解如何在 Fuchsia 上启动和使用虚拟化,请参阅 Fuchsia 虚拟化使用入门