VMBus¶
VMBus 是 Hyper-V 为来宾虚拟机提供的软件构造。它由控制路径和 Hyper-V 提供给来宾虚拟机的合成设备使用的公共设施组成。控制路径用于向来宾虚拟机提供合成设备,并在某些情况下撤销这些设备。公共设施包括用于在来宾虚拟机中的设备驱动程序和 Hyper-V 的合成设备实现之间进行通信的软件通道,以及允许 Hyper-V 和来宾虚拟机互相中断的信号原语。
VMBus 在 Linux 中被建模为总线,在运行的 Linux 来宾虚拟机中具有预期的 /sys/bus/vmbus 条目。VMBus 驱动程序 (drivers/hv/vmbus_drv.c) 与 Hyper-V 主机建立 VMBus 控制路径,然后将自身注册为 Linux 总线驱动程序。它实现了标准的总线功能,用于在总线上添加和删除设备。
Hyper-V 提供的大多数合成设备都有相应的 Linux 设备驱动程序。这些设备包括
SCSI 控制器
网卡
图形帧缓冲区
键盘
鼠标
PCI 设备直通
心跳
时间同步
关机
内存气球
与 Hyper-V 交换键/值对 (KVP)
Hyper-V 在线备份(又名 VSS)
来宾虚拟机可能具有合成 SCSI 控制器、合成网卡和 PCI 直通设备的多个实例。其他合成设备每个虚拟机仅限于一个实例。上面未列出的是 Hyper-V 提供的一小部分合成设备,这些设备仅供 Windows 来宾使用,而 Linux 没有相应的驱动程序。
Hyper-V 在描述合成设备时使用术语“VSP”和“VSC”。“VSP”指的是实现特定合成设备的 Hyper-V 代码,而“VSC”指的是来宾虚拟机中该设备的驱动程序。例如,合成网卡的 Linux 驱动程序称为“netvsc”,而合成 SCSI 控制器的 Linux 驱动程序称为“storvsc”。这些驱动程序包含诸如“storvsc_connect_to_vsp”之类的函数。
VMBus 通道¶
合成设备的实例使用 VMBus 通道在 VSP 和 VSC 之间进行通信。通道是双向的,用于传递消息。大多数合成设备使用单个通道,但是合成 SCSI 控制器和合成网卡可以使用多个通道来实现更高的性能和更大的并行性。
每个通道由两个环形缓冲区组成。这些是来自大学数据结构教科书的经典环形缓冲区。如果读写指针相等,则认为环形缓冲区为空,因此一个完整的环形缓冲区始终至少有一个字节未使用。“in”环形缓冲区用于从 Hyper-V 主机到访客的消息,“out”环形缓冲区用于从访客到 Hyper-V 主机的消息。在 Linux 中,“in”和“out”的指定是以来宾端为视角的。环形缓冲区是来宾和主机之间共享的内存,它们遵循标准范例,即内存由来宾分配,构成环形缓冲区的 GPA 列表被传达给主机。每个环形缓冲区都包含一个带有读取和写入索引以及一些控制标志的头页(4 Kbytes),后跟用于实际环的内存。环的大小由来宾中的 VSC 确定,并且特定于每个合成设备。构成环的 GPA 列表通过 VMBus 控制路径作为 GPA 描述符列表 (GPADL) 传达给 Hyper-V 主机。请参阅函数 vmbus_establish_gpadl()。
每个环形缓冲区都被映射到连续的 Linux 内核虚拟空间中,分为三个部分:1)4 Kbyte 的头页,2)构成环本身的内存,以及 3)构成环本身的内存的第二个映射。因为 (2) 和 (3) 在内核虚拟空间中是连续的,所以将数据复制到环形缓冲区和从环形缓冲区复制数据的代码无需担心环形缓冲区的回绕。一旦复制操作完成,读取或写入索引可能需要重置以指向第一个映射,但实际的数据复制不需要分为两个部分。此方法还允许轻松直接访问环中的复杂数据结构,而无需处理回绕。
在页面大小 > 4 Kbytes 的 arm64 上,头页仍然必须作为 4 Kbyte 区域传递给 Hyper-V。但是,实际环的内存必须与 PAGE_SIZE 对齐,并且大小必须是 PAGE_SIZE 的倍数,以便可以执行重复映射技巧。因此,头页的一部分未使用,并且不会传达给 Hyper-V。这种情况由 vmbus_establish_gpadl() 处理。
Hyper-V 对可以通过 GPADL 与主机共享的来宾内存总量施加了限制。此限制确保恶意来宾无法强制消耗过多的主机资源。对于 Windows Server 2019 及更高版本,此限制约为 1280 Mbytes。对于 Windows Server 2019 之前的版本,此限制约为 384 Mbytes。
VMBus 通道消息¶
在 VMBus 通道中发送的所有消息都具有标准标头,其中包括消息长度、消息有效负载的偏移量、一些标志和 transactionID。标头之后的消息部分对于每个 VSP/VSC 对都是唯一的。
消息遵循以下两种模式之一
单向:任何一方发送消息,并且不期望响应消息
请求/响应:一方(通常是访客)发送消息,并期望收到响应
transactionID(又名“requestID”)用于匹配请求和响应。某些合成设备允许同时进行多个请求,因此访客在发送请求时会指定一个 transactionID。Hyper-V 在匹配的响应中发回相同的 transactionID。
在 VSP 和 VSC 之间传递的消息是控制消息。例如,从 storvsc 驱动程序发送的消息可能是“执行此 SCSI 命令”。如果消息还暗示在访客和 Hyper-V 主机之间进行某些数据传输,则要传输的实际数据可以嵌入在控制消息中,也可以指定为 Hyper-V 主机将作为 DMA 操作访问的单独数据缓冲区。当数据大小较小并且将数据复制到环形缓冲区和从环形缓冲区复制数据的成本最小化时,使用前一种情况。例如,从 Hyper-V 主机到访客的时间同步消息包含实际时间值。当数据较大时,使用单独的数据缓冲区。在这种情况下,控制消息包含描述数据缓冲区的 GPA 列表。例如,storvsc 驱动程序使用此方法来指定执行磁盘 I/O 的数据缓冲区。
存在三个用于发送 VMBus 通道消息的函数
vmbus_sendpacket():仅控制消息和带有嵌入数据的消息 - 无 GPA
vmbus_sendpacket_pagebuffer():带有 GPA 列表的消息,用于标识要传输的数据。偏移量和长度与每个 GPA 相关联,以便可以定位访客内存的多个不连续区域。
vmbus_sendpacket_mpb_desc():带有 GPA 列表的消息,用于标识要传输的数据。单个偏移量和长度与 GPA 列表相关联。GPA 必须描述要定位的访客内存的单个逻辑区域。
从历史上看,Linux 来宾一直信任 Hyper-V 发送格式良好且有效的消息,并且合成设备的 Linux 驱动程序并未完全验证消息。随着完全加密来宾内存并允许来宾不信任虚拟机管理程序(AMD SEV-SNP,Intel TDX)的处理器技术的引入,信任 Hyper-V 主机不再是有效的假设。VMBus 合成设备的驱动程序正在更新,以完全验证从与 Hyper-V 共享的内存中读取的任何值,其中包括来自 VMBus 设备的消息。为了方便这种验证,访客从“in”环形缓冲区读取的消息将复制到不与 Hyper-V 共享的临时缓冲区。验证在此临时缓冲区中执行,而没有 Hyper-V 在验证后但在使用前恶意修改消息的风险。
合成中断控制器 (synic)¶
Hyper-V 为每个来宾 CPU 提供一个合成中断控制器,VMBus 使用该控制器进行主机-来宾通信。虽然每个 synic 定义了 16 个合成中断 (SINT),但 Linux 仅使用 16 个中的一个 (VMBUS_MESSAGE_SINT)。与 Hyper-V 主机和来宾 CPU 之间的通信相关的所有中断都使用该 SINT。
SINT 映射到单个每个 CPU 的架构中断(即,8 位 x86/x64 中断向量或 arm64 PPI INTID)。由于来宾中的每个 CPU 都有一个 synic 并且可能会收到 VMBus 中断,因此最好在 Linux 中将其建模为每个 CPU 的中断。此模型在 arm64 上效果很好,其中为 VMBUS_MESSAGE_SINT 分配了一个单个的每个 CPU 的 Linux IRQ。此 IRQ 在 /proc/interrupts 中显示为标记为“Hyper-V VMbus”的 IRQ。由于 x86/x64 缺乏对每个 CPU 的 IRQ 的支持,因此在所有 CPU 上静态分配了一个 x86 中断向量 (HYPERVISOR_CALLBACK_VECTOR),并显式编码为调用 vmbus_isr()。在这种情况下,没有 Linux IRQ,并且中断在 /proc/interrupts 中的“HYP”行中以聚合形式可见。
synic 提供了将架构中断多路分解为一个或多个逻辑中断,并将逻辑中断路由到 Linux 中正确的 VMBus 处理程序的方法。这种多路分解由 vmbus_isr() 和访问 synic 数据结构的相关函数完成。
synic 在 Linux 中不被建模为 irq 芯片或 irq 域,并且多路分解后的逻辑中断不是 Linux IRQ。因此,它们不会出现在 /proc/interrupts 或 /proc/irq 中。这些逻辑中断之一的 CPU 亲和性是通过 /sys/bus/vmbus 下的一个条目控制的,如下所述。
VMBus 中断¶
VMBus 提供了一种机制,当客户机在环形缓冲区中排队新消息时,可以中断主机。主机期望客户机仅当“出”环形缓冲区从空转换为非空时才发送中断。如果客户机在其他时间发送中断,则主机认为这些中断是不必要的。如果客户机发送过多不必要的中断,主机可能会通过暂停其执行几秒钟来限制该客户机,以防止拒绝服务攻击。
类似地,当主机在 VMBus 控制路径上发送新消息时,或者当由于主机插入新的 VMBus 通道消息导致 VMBus 通道“入”环形缓冲区从空转换为非空时,主机将通过 synic 中断客户机。控制消息流和每个 VMBus 通道“入”环形缓冲区都是独立的逻辑中断,它们由 vmbus_isr() 进行多路分解。它首先通过调用 vmbus_chan_sched() 来检查通道中断,该函数查看 synic 位图以确定哪些通道在此 CPU 上有挂起的中断。如果多个通道在此 CPU 上有挂起的中断,则会按顺序处理它们。当所有通道中断都已处理完毕时,vmbus_isr() 会检查并处理在 VMBus 控制路径上收到的任何消息。
VMBus 通道将中断的客户机 CPU 由客户机在创建通道时选择,并且主机被告知该选择。VMBus 设备大致分为两类
只需要一个 VMBus 通道的“慢速”设备。这些设备(例如键盘、鼠标、心跳和时间同步)产生的中断相对较少。它们的 VMBus 通道都分配给中断 VMBUS_CONNECT_CPU,它始终是 CPU 0。
可能使用多个 VMBus 通道以实现更高并行度和性能的“高速”设备。这些设备包括合成 SCSI 控制器和合成 NIC。它们的 VMBus 通道中断被分配给 VM 中可用 CPU 中分散的 CPU,以便可以并行处理多个通道上的中断。
VMBus 通道中断到 CPU 的分配在函数 init_vp_index() 中完成。此分配在正常的 Linux 中断亲和性机制之外完成,因此中断既不是“未管理”的中断,也不是“已管理”的中断。
可以在 /sys/bus/vmbus/devices/<设备GUID>/channels/<通道RelID>/cpu 中看到 VMBus 通道将中断的 CPU。在较新版本的 Hyper-V 上运行时,可以通过向此 sysfs 条目写入新值来更改 CPU。由于 VMBus 通道中断不是 Linux IRQ,因此在 /proc/interrupts 或 /proc/irq 中没有与单个 VMBus 通道中断对应的条目。
如果 Linux 客户机中的在线 CPU 上分配了 VMBus 通道中断,则不能将其脱机。任何此类通道中断必须首先按照上述说明手动重新分配给另一个 CPU。当没有通道中断分配给 CPU 时,可以将其脱机。
VMBus 通道中断处理代码被设计为即使在不是分配给通道的 CPU 上收到中断也能正常工作。具体来说,该代码不使用基于 CPU 的互斥来实现正确性。在正常操作中,Hyper-V 将中断分配的 CPU。但是,当通过 sysfs 更改分配给通道的 CPU 时,客户机并不知道 Hyper-V 何时会进行转换。即使在 Hyper-V 开始中断新的 CPU 之前存在时间延迟,该代码也必须正常工作。请参阅 target_cpu_store() 中的注释。
VMBus 设备创建/删除¶
Hyper-V 和 Linux 客户机具有单独的消息传递路径,用于合成设备的创建和删除。此路径不使用 VMBus 通道。请参阅 vmbus_post_msg() 和 vmbus_on_msg_dpc()。
第一步是客户机连接到通用的 Hyper-V VMBus 机制。作为建立此连接的一部分,客户机和 Hyper-V 就他们将使用的 VMBus 协议版本达成一致。这种协商允许较新的 Linux 内核在较旧的 Hyper-V 版本上运行,反之亦然。
然后,客户机告诉 Hyper-V “发送报价”。Hyper-V 为 VM 配置的每个合成设备向客户机发送报价消息。每个 VMBus 设备类型都有一个固定的 GUID,称为“类 ID”,并且每个 VMBus 设备实例也由一个 GUID 标识。来自 Hyper-V 的报价消息包含这两个 GUID,以便(在 VM 中)唯一标识设备。每个设备实例有一个报价消息,因此具有两个合成 NIC 的 VM 将收到两个具有 NIC 类 ID 的报价消息。报价消息的顺序可能因启动而异,并且不得假定在 Linux 代码中是一致的。报价消息也可能在 Linux 最初启动后很长时间才到达,因为 Hyper-V 支持向正在运行的 VM 添加设备,例如合成 NIC。一个新的报价消息由 vmbus_process_offer() 处理,它间接调用 vmbus_add_channel_work()。
收到报价消息后,客户机根据类 ID 识别设备类型,并调用正确的驱动程序来设置设备。驱动程序/设备匹配是使用标准 Linux 机制执行的。
设备驱动程序探针函数打开到相应 VSP 的主 VMBus 通道。它为通道环形缓冲区分配客户机内存,并通过向主机提供环形缓冲区内存的 GPA 列表来与 Hyper-V 主机共享环形缓冲区。请参阅 vmbus_establish_gpadl()。
设置环形缓冲区后,设备驱动程序和 VSP 通过主通道交换设置消息。这些消息可能包括协商 Linux VSC 和 Hyper-V 主机上的 VSP 之间要使用的设备协议版本。设置消息还可能包括创建额外的 VMBus 通道,这些通道被错误地命名为“子通道”,因为它们在创建后在功能上等同于主通道。
最后,设备驱动程序可能会像任何设备驱动程序一样在 /dev 中创建条目。
Hyper-V 主机可以向客户机发送“撤销”消息,以删除先前提供的设备。Linux 驱动程序必须随时处理此类撤销消息。撤销设备会调用设备驱动程序“删除”函数以干净地关闭设备并将其删除。一旦合成设备被撤销,Hyper-V 和 Linux 都不保留关于其先前存在的任何状态。这样的设备可能会在稍后重新添加,在这种情况下,它被视为一个全新的设备。请参阅 vmbus_onoffer_rescind()。