概述

Linux 内核包含各种代码,用于作为 Microsoft Hyper-V 虚拟机管理程序上的完全启动的来宾运行。Hyper-V 主要由裸机虚拟机管理程序以及在父分区中运行的虚拟机管理服务组成(大致相当于 KVM 和 QEMU)。来宾虚拟机在子分区中运行。在本文档中,对 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 将已分配内存的来宾物理地址 (GPA) 告知 Hyper-V。许多共享区域都保持为 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 页面的映射是一对一的。在 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 使用通常的方法来分配来宾内存,并将已分配内存的 GPA 告知 Hyper-V。但是,Hyper-V 随后会将其已分配的页面替换为该物理内存页面,并且来宾 VM 中不再可访问原始物理内存页面。Linux 可以像访问其最初分配的内存一样正常访问该内存。“覆盖”行为仅可见,因为(由 Linux 查看的)页面内容在 Linux 最初建立共享并且插入覆盖页面时会发生变化。同样,如果 Linux 撤销共享,则内容也会发生变化,在这种情况下,Hyper-V 会删除覆盖页面,并且 Linux 最初分配的来宾页面会再次变为可见。

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

CPU 管理

Hyper-V 不具备从正在运行的 VM 中热添加或热删除 CPU 的能力。但是,Windows Server 2019 Hyper-V 及更早版本可能会为来宾提供 ACPI 表,该表指示的 CPU 比 VM 中实际存在的 CPU 多。正常情况下,Linux 会将这些额外的 CPU 视为潜在的热添加 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 是跨架构的通用底层代码,必须内置。