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 KB)组成,后面是实际环的内存。 环的大小由客户机中的 VSC 确定,并且特定于每个合成设备。 构成环的 GPA 列表通过 VMBus 控制路径作为 GPA 描述符列表 (GPADL) 传达给 Hyper-V 主机。 请参阅函数 vmbus_establish_gpadl()。
每个环形缓冲区被映射到三个部分的连续 Linux 内核虚拟空间中:1) 4 KB 标头页,2) 构成环本身的内存,3) 构成环本身的内存的第二次映射。 因为 (2) 和 (3) 在内核虚拟空间中是连续的,所以将数据复制到环形缓冲区和从环形缓冲区复制数据的代码不需要担心环形缓冲区回绕。 一旦复制操作完成,可能需要重置读写索引以指向第一次映射,但实际的数据复制不需要分成两个部分。 这种方法还允许轻松地直接在环中访问复杂的数据结构,而无需处理回绕。
在页面大小 > 4 KB 的 arm64 上,标头页仍然必须作为 4 KB 区域传递给 Hyper-V。 但是实际环的内存必须与 PAGE_SIZE 对齐,并且大小必须是 PAGE_SIZE 的倍数,以便可以进行重复映射技巧。 因此,标头页的一部分未使用,也不会传达给 Hyper-V。 这种情况由 vmbus_establish_gpadl() 处理。
Hyper-V 对可以通过 GPADL 与主机共享的客户机内存总量强制执行限制。 此限制确保恶意客户机无法强制消耗过多的主机资源。 对于 Windows Server 2019 及更高版本,此限制约为 1280 MB。 对于 Windows Server 2019 之前的版本,该限制约为 384 MB。
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 提供了一种机制,当客户机在环形缓冲区中排队新消息时,客户机可以中断主机。 主机期望客户机仅在“out”环形缓冲区从空转换为非空时才发送中断。 如果客户机在其他时间发送中断,则主机认为此类中断是不必要的。 如果客户机发送过多不必要的中断,主机可能会通过暂停其执行几秒钟来限制该客户机,以防止拒绝服务攻击。
类似地,当主机在 VMBus 控制路径上发送新消息时,或者当由于主机插入新 VMBus 通道消息而导致 VMBus 通道“in”环形缓冲区从空转换为非空时,主机将通过 synic 中断客户机。 控制消息流和每个 VMBus 通道“in”环形缓冲区是单独的逻辑中断,由 vmbus_isr() 多路分解。 它首先通过调用 vmbus_chan_sched() 来检查通道中断来进行多路分解,vmbus_chan_sched() 查看 synic 位图以确定哪些通道在此 CPU 上有挂起的中断。 如果多个通道在此 CPU 上有挂起的中断,则按顺序处理这些通道。 处理完所有通道中断后,vmbus_isr() 检查并处理在 VMBus 控制路径上接收到的任何消息。
VMBus 通道将中断的客户机 CPU 由客户机在创建通道时选择,并告知主机该选择。 VMBus 设备大致分为两类
“慢速”设备,只需要一个 VMBus 通道。 设备(如键盘、鼠标、心跳和 timesync)生成的中断相对较少。 它们的 VMBus 通道都分配为中断 VMBUS_CONNECT_CPU,它始终是 CPU 0。
“高速”设备,可以使用多个 VMBus 通道来实现更高的并行性和性能。 这些设备包括合成 SCSI 控制器和合成网卡。 它们的 VMBus 通道中断被分配给在 VM 中可用 CPU 中分散的 CPU,以便可以并行处理多个通道上的中断。
VMBus 通道中断到 CPU 的分配在函数 init_vp_index() 中完成。 此分配在正常的 Linux 中断亲和性机制之外完成,因此中断既不是“非托管”中断也不是“托管”中断。
可以在 /sys/bus/vmbus/devices/<deviceGUID>/channels/<channelRelID>/cpu 中看到 VMBus 通道将中断的 CPU。 在更高版本的 Hyper-V 上运行时,可以通过将新值写入此 sysfs 条目来更改 CPU。 由于 VMBus 通道中断不是 Linux IRQ,因此 /proc/interrupts 或 /proc/irq 中没有与单个 VMBus 通道中断对应的条目。
如果 Linux 客户机中的在线 CPU 有分配给它的 VMBus 通道中断,则不能将其脱机。 从内核 v6.15 开始,任何此类中断都会在脱机时自动重新分配给其他 CPU。 “其他”CPU 由实现选择,并且不进行负载平衡或以其他方式智能确定。 如果 CPU 再次联机,则先前分配给它的通道中断不会移回。 因此,在多个 CPU 脱机(可能再次联机)之后,中断到 CPU 的映射可能会被打乱且不是最佳的。 在这种情况下,必须手动重新建立最佳分配。 对于内核 v6.14 及更早版本,必须首先如上所述手动将任何冲突的通道中断重新分配给另一个 CPU。 然后,当没有通道中断分配给 CPU 时,可以将其脱机。
即使在分配给通道的 CPU 以外的 CPU 上收到中断,VMBus 通道中断处理代码也被设计为可以正常工作。 具体来说,该代码不使用基于 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 设备类型都有一个称为“类 ID”的固定 GUID,每个 VMBus 设备实例也由一个 GUID 标识。 来自 Hyper-V 的提供消息包含两个 GUID,以(在 VM 中)唯一地标识设备。 每个设备实例都有一个提供消息,因此具有两个合成网卡的 VM 将收到两个具有网卡类 ID 的提供消息。 提供消息的顺序可能因启动而异,并且不得假定在 Linux 代码中是一致的。 提供消息也可能在 Linux 最初启动后很久才到达,因为 Hyper-V 支持将设备(如合成网卡)添加到正在运行的 VM。 vmbus_process_offer() 处理新的提供消息,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()。
对于某些设备(如 KVP 设备),当主通道关闭时,Hyper-V 会自动发送撤消消息,这可能是由于将设备从其驱动程序中解除绑定造成的。 撤消会导致 Linux 删除设备。 但是,Hyper-V 会立即重新向客户机提供该设备,导致在 Linux 中创建一个新的设备实例。 对于其他设备(如合成 SCSI 和网卡设备),关闭主通道_不_会导致 Hyper-V 发送撤消消息。 设备继续存在于 Linux 的 VMBus 上,但没有驱动程序绑定到它。 相同的驱动程序或新驱动程序随后可以绑定到设备的现有实例。