概述

Linux 内核包含各种代码,用于在 Microsoft 的 Hyper-V 虚拟机监控程序上作为完全启用的来宾运行。Hyper-V 主要由裸机虚拟机监控程序以及在父分区中运行的虚拟机管理服务组成(大致相当于 KVM 和 QEMU 等)。来宾 VM 在子分区中运行。 在本文档中,对 Hyper-V 的引用通常包括虚拟机监控程序和 VMM 服务,而不区分哪些功能由哪个组件提供。

Hyper-V 在 x86/x64 和 arm64 架构上运行,并且两种架构都支持 Linux 来宾。除非另有说明,否则 Hyper-V 的功能和行为在两种架构上通常是相同的。

Linux 来宾与 Hyper-V 的通信

Linux 来宾通过四种不同的方式与 Hyper-V 通信

  • 隐式陷阱:按照 x86/x64 或 arm64 架构的定义,某些来宾操作会陷阱到 Hyper-V。Hyper-V 模拟该操作并将控制权返回给来宾。这种行为通常对 Linux 内核不可见。

  • 显式超调用:Linux 对 Hyper-V 进行显式函数调用,传递参数。Hyper-V 执行请求的操作并将控制权返回给调用者。参数在处理器寄存器中或 Linux 来宾和 Hyper-V 之间共享的内存中传递。在 x86/x64 上,超调用使用 Hyper-V 特定的调用序列。在 arm64 上,超调用使用 ARM 标准 SMCCC 调用序列。

  • 合成寄存器访问:Hyper-V 实现了各种合成寄存器。在 x86/x64 上,这些寄存器以 MSR 的形式出现在来宾中,并且 Linux 内核可以使用 x86/x64 架构定义的正常机制来读取或写入这些 MSR。在 arm64 上,必须使用显式超调用来访问这些合成寄存器。

  • VMBus:VMBus 是一个更高层次的软件构造,它建立在其他 3 个机制之上。它是 Hyper-V 主机和 Linux 来宾之间的消息传递接口。它使用 Hyper-V 和来宾之间共享的内存,以及各种信令机制。

前三种通信机制记录在 Hyper-V 顶层功能规范 (TLFS) 中。TLFS 描述了 Hyper-V 的一般功能,并提供了有关超调用和合成寄存器的详细信息。TLFS 目前仅针对 x86/x64 架构编写。

VMBus 没有记录。本文档提供了 VMBus 的高级概述及其工作方式,但详细信息只能从代码中辨别出来。

共享内存

Hyper-V 和 Linux 之间的许多方面通信都基于共享内存。 这种共享通常通过以下方式完成

  • Linux 使用标准的 Linux 机制从其物理地址空间分配内存。

  • Linux 告诉 Hyper-V 分配内存的来宾物理地址 (GPA)。 许多共享区域都保持为 1 页,以便单个 GPA 足够。 较大的共享区域需要 GPA 列表,这些列表通常不需要在来宾物理地址空间中连续。 如何告诉 Hyper-V 有关 GPA 或 GPA 列表的方式各不相同。 在某些情况下,单个 GPA 会写入合成寄存器。 在其他情况下,GPA 或 GPA 列表会在 VMBus 消息中发送。

  • Hyper-V 将 GPA 转换为“实际”物理内存地址,并创建一个虚拟映射,可用于访问该内存。

  • Linux 稍后可以通过告诉 Hyper-V 将共享 GPA 设置为零来撤销之前建立的共享。

Hyper-V 的页面大小为 4 KB。 传达给 Hyper-V 的 GPA 可以采用页码的形式,并且始终描述 4 KB 的范围。 由于 x86/x64 上的 Linux 来宾页面大小也为 4 KB,因此从来宾页面到 Hyper-V 页面的映射是 1 对 1 的。 在 arm64 上,Hyper-V 支持具有 arm64 架构定义的 4/16/64 KB 页面的来宾。 如果 Linux 使用 16 或 64 KB 页面,则 Linux 代码必须小心,只能以 4 KB 页面的形式与 Hyper-V 通信。 HV_HYP_PAGE_SIZE 和相关宏用于与 Hyper-V 通信的代码中,以便它在所有配置中都能正常工作。

如 TLFS 中所述,在 Hyper-V 和 Linux 来宾之间共享的几个内存页面是“覆盖”页面。 使用覆盖页面,Linux 使用分配来宾内存并告诉 Hyper-V 已分配内存的 GPA 的常用方法。 但 Hyper-V 随后会将该物理内存页面替换为它已分配的页面,并且原始物理内存页面在来宾 VM 中不再可访问。 Linux 可以像访问最初分配的内存一样正常访问该内存。“覆盖”行为仅在 Linux 最初建立共享并插入覆盖页面时才可见,因为该页面的内容(如 Linux 所见)发生了变化。 同样,如果 Linux 撤销共享,内容也会发生变化,在这种情况下,Hyper-V 会删除覆盖页面,并且最初由 Linux 分配的来宾页面再次变得可见。

在 Linux 对 kdump 内核或任何其他内核执行 kexec 之前,应撤销与 Hyper-V 共享的内存。Hyper-V 可能会在新的内核将共享页面用于其他目的后修改共享页面或删除覆盖页面,从而损坏新的内核。Hyper-V 不提供“设置所有内容”操作来宾 VM,因此 Linux 代码必须在执行 kexec 之前单独撤销所有共享。请参阅 hv_kexec_handler() 和 hv_crash_handler()。 但是,崩溃/panic 路径仍然存在清理漏洞,因为某些共享页面是使用每个 CPU 的合成寄存器设置的,并且没有机制可以撤销运行 panic 路径的 CPU 以外的 CPU 的共享页面。

CPU 管理

Hyper-V 无法从正在运行的 VM 热添加或热删除 CPU。 但是,Windows Server 2019 Hyper-V 及更早版本可能会为来宾提供 ACPI 表,指示的 CPU 数量多于 VM 中实际存在的 CPU 数量。 像往常一样,Linux 将这些额外的 CPU 视为潜在的热添加 CPU,并将其报告为这样,即使 Hyper-V 永远不会实际热添加它们。 从 Windows Server 2022 Hyper-V 开始,ACPI 表仅反映 VM 中实际存在的 CPU,因此 Linux 不会报告任何热添加 CPU。

可以使用正常的 Linux 机制将 Linux 来宾 CPU 脱机,前提是没有 VMBus 通道中断分配给该 CPU。 有关如何重新分配 VMBus 通道中断以允许将 CPU 脱机的更多详细信息,请参阅有关 VMBus 中断的部分。

32 位和 64 位

在 x86/x64 上,Hyper-V 支持 32 位和 64 位来宾,并且 Linux 将构建并以任一版本运行。 虽然 32 位版本预计可以工作,但很少使用,并且可能遭受未检测到的回归。

在 arm64 上,Hyper-V 仅支持 64 位来宾。

字节顺序

Hyper-V 和来宾 VM 之间的所有通信都使用小端格式,无论是在 x86/x64 还是 arm64 上。 Hyper-V 不支持 arm64 上的大端格式,并且 Linux 代码在访问与 Hyper-V 共享的数据时不使用字节顺序宏。

版本控制

当前的 Linux 内核可以与 Windows Server 2012 Hyper-V 的旧版 Hyper-V 正常运行。 对在 Windows Server 2008/2008 R2 的原始 Hyper-V 版本上运行的支持已被删除。

Hyper-V 上的 Linux 来宾会在 dmesg 中输出其运行的 Hyper-V 版本。 此版本采用 Windows 内部版本号的形式,仅用于显示目的。 Linux 代码不会在运行时测试此版本号,以确定可用的特性和功能。 Hyper-V 通过 Hyper-V 提供给来宾的合成 MSR 中的标志指示特性/功能可用性,并且来宾代码会测试这些标志。

VMBus 有其自己的协议版本,该版本在来宾到 Hyper-V 的初始 VMBus 连接期间协商。 此版本号也在启动期间输出到 dmesg。 代码中的一些位置会检查此版本号,以确定是否存在特定功能。

此外,VMBus 上的每个合成设备也有一个协议版本,该版本与 VMBus 协议版本分开。 这些合成设备的设备驱动程序通常会协商设备协议版本,并且可能会测试该协议版本以确定是否存在特定设备功能。

代码打包

与 Hyper-V 相关的代码出现在 Linux 内核代码树中的三个主要区域

  1. drivers/hv

  2. arch/x86/hyperv 和 arch/arm64/hyperv

  3. 各个设备驱动程序区域,例如 drivers/scsi、drivers/net、drivers/clocksource 等。

一些杂项文件出现在其他位置。 请参阅 MAINTAINERS 文件中“Hyper-V/Azure 核心和驱动程序”和“适用于 HyperV 合成视频设备的 DRM 驱动程序”下的完整列表。

只有在设置了 CONFIG_HYPERV 时,才会构建 #1 和 #2 中的代码。 同样,只有在设置了 CONFIG_HYPERV 时,才会构建大多数与 Hyper-V 相关的驱动程序的代码。

#1 和 #3 中的大多数与 Hyper-V 相关的代码都可以构建为模块。 #2 中的特定于架构的代码必须内置。 此外,drivers/hv/hv_common.c 是跨架构的通用底层代码,必须内置。