PCI 直通设备

在 Hyper-V 客户虚拟机中,PCI 直通设备(也称为虚拟 PCI 设备或 vPCI 设备)是直接映射到虚拟机物理地址空间的物理 PCI 设备。客户机设备驱动程序可以直接与硬件交互,而无需主机虚拟机监控程序的干预。与由虚拟机监控程序虚拟化的设备相比,这种方法提供了更高的设备带宽访问和更低的延迟。该设备在客户机看来应该与在裸机上运行时一样,因此无需对该设备的 Linux 设备驱动程序进行任何更改。

vPCI 设备的 Hyper-V 术语是“离散设备分配”(DDA)。有关 Hyper-V DDA 的公共文档可在此处找到:DDA

DDA 通常用于存储控制器(例如 NVMe)和 GPU。NIC 的类似机制称为 SR-IOV,它通过允许客户机设备驱动程序直接与硬件交互来产生相同的好处。请参阅此处的 Hyper-V 公共文档:SR-IOV

对 vPCI 设备的讨论包括 DDA 和 SR-IOV 设备。

设备呈现

Hyper-V 在 vPCI 设备运行时为其提供完整的 PCI 功能,因此该设备的 Linux 设备驱动程序可以不作更改地使用,前提是它使用正确的 Linux 内核 API 来访问 PCI 配置空间并与其他 Linux 集成。但是,PCI 设备的初始检测及其与 Linux PCI 子系统的集成必须使用 Hyper-V 特定的机制。因此,Hyper-V 上的 vPCI 设备具有双重身份。它们最初通过标准的 VMBus“提供”机制作为 VMBus 设备呈现给 Linux 客户机,因此它们具有 VMBus 身份并显示在 /sys/bus/vmbus/devices 下。Linux 中 drivers/pci/controller/pci-hyperv.c 中的 VMBus vPCI 驱动程序通过构造 PCI 总线拓扑并在 Linux 中创建所有正常的 PCI 设备数据结构来处理新引入的 vPCI 设备,如果 PCI 设备是通过裸机系统上的 ACPI 发现的,则会存在这些数据结构。一旦设置了这些数据结构,该设备在 Linux 中也具有正常的 PCI 身份,并且 vPCI 设备的正常 Linux 设备驱动程序可以像在裸机上的 Linux 中运行一样运行。由于 vPCI 设备通过 VMBus 提供机制动态呈现,因此它们不会出现在 Linux 客户机的 ACPI 表中。可以在 VM 生命周期的任何时候(而不仅仅是在初始引导期间)向 VM 添加或从 VM 中删除 vPCI 设备。

通过这种方法,vPCI 设备同时是 VMBus 设备和 PCI 设备。为了响应 VMBus 提供消息,hv_pci_probe() 函数运行并建立与 Hyper-V 主机上的 vPCI VSP 的 VMBus 连接。该连接具有单个 VMBus 通道。该通道用于与 vPCI VSP 交换消息,以便在 Linux 中设置和配置 vPCI 设备。一旦该设备在 Linux 中完全配置为 PCI 设备,则仅当 Linux 将 vCPU 更改为在客户机中中断时,或者如果 vPCI 设备在 VM 运行时从 VM 中移除时才使用 VMBus 通道。设备的持续运行直接发生在设备的 Linux 设备驱动程序和硬件之间,VMBus 和 VMBus 通道不起任何作用。

PCI 设备设置

PCI 设备设置遵循 Hyper-V 最初为 Windows 客户机创建的序列,由于 Linux PCI 子系统的总体结构与 Windows 的不同,因此可能不太适合 Linux 客户机。尽管如此,通过 Linux 的 Hyper-V 虚拟 PCI 驱动程序中的一些黑客行为,虚拟 PCI 设备在 Linux 中设置,以便通用 Linux PCI 子系统代码和该设备的 Linux 驱动程序“正常工作”。

每个 vPCI 设备在 Linux 中都设置为具有主机桥的自己的 PCI 域。PCI domainID 源自分配给 VMBus vPCI 设备的实例 GUID 的字节 4 和 5。Hyper-V 主机不保证这些字节是唯一的,因此 hv_pci_probe() 有一个算法来解决冲突。冲突解决方法旨在在同一 VM 的重新启动中保持稳定,以便 PCI domainID 不会更改,因为 domainID 出现在某些设备的用户空间配置中。

hv_pci_probe() 分配一个客户机 MMIO 范围,用作该设备的 PCI 配置空间。此 MMIO 范围通过 VMBus 通道传达给 Hyper-V 主机,作为告诉主机该设备已准备好进入 d0 的一部分。请参阅 hv_pci_enter_d0()。当客户机随后访问此 MMIO 范围时,Hyper-V 主机将拦截访问并将其映射到物理设备 PCI 配置空间。

hv_pci_probe() 还从 Hyper-V 主机获取该设备的 BAR 信息,并使用此信息为 BAR 分配 MMIO 空间。然后设置该 MMIO 空间以与主机桥关联,以便当 Linux 中的通用 PCI 子系统代码处理 BAR 时它能够工作。

最后,hv_pci_probe() 创建根 PCI 总线。此时,Hyper-V 虚拟 PCI 驱动程序黑客行为已完成,并且用于扫描根总线的正常 Linux PCI 机制可用于检测设备、执行驱动程序匹配以及初始化驱动程序和设备。

PCI 设备移除

Hyper-V 主机可以在 VM 生命周期的任何时候启动从客户虚拟机中删除 vPCI 设备的操作。删除由 Hyper-V 主机上采取的管理操作启动,不受客户操作系统控制。

主机通过从主机到客户机通过与 vPCI 设备关联的 VMBus 通道发送的未经请求的“弹出”消息来通知客户虚拟机删除。收到此类消息后,Linux 中的 Hyper-V 虚拟 PCI 驱动程序会异步调用 Linux 内核 PCI 子系统调用来关闭并删除设备。当这些调用完成后,将通过 VMBus 通道向 Hyper-V 发回“弹出完成”消息,指示该设备已被删除。此时,Hyper-V 会向 Linux 客户机发送 VMBus 取消消息,Linux 中的 VMBus 驱动程序通过删除该设备的 VMBus 身份来处理该消息。一旦完成该处理,设备存在的所有痕迹都将从 Linux 内核中消失。取消消息还向客户机指示 Hyper-V 已停止在客户机中为 vPCI 设备提供支持。如果客户机尝试访问该设备的 MMIO 空间,则将是无效的引用。影响该设备的超调用会返回错误,并且将忽略在 VMBus 通道中发送的任何进一步消息。

在发送“弹出”消息后,Hyper-V 允许客户虚拟机 60 秒时间来干净地关闭设备并以“弹出完成”消息响应,然后再发送 VMBus 取消消息。如果由于任何原因,“弹出”步骤未在允许的 60 秒内完成,则 Hyper-V 主机将强制执行取消步骤,这可能会导致客户机中的级联错误,因为从客户机的角度来看,该设备现在不再存在,并且访问设备 MMIO 空间将失败。

由于弹出是异步的并且可能在客户虚拟机生命周期的任何时候发生,因此 Hyper-V 虚拟 PCI 驱动程序中的正确同步非常棘手。甚至在刚提供的 vPCI 设备尚未完全设置之前,就已经观察到弹出。多年来,Hyper-V 虚拟 PCI 驱动程序已多次更新,以修复在不适当的时间发生弹出时的竞争条件。修改此代码时必须小心,以防止重新引入此类问题。请参阅代码中的注释。

中断分配

Hyper-V 虚拟 PCI 驱动程序支持使用 MSI、多 MSI 或 MSI-X 的 vPCI 设备。分配将接收特定 MSI 或 MSI-X 消息中断的客户 vCPU 很复杂,因为 Linux IRQ 设置的方式映射到 Hyper-V 接口。对于单 MSI 和 MSI-X 情况,Linux 会调用 hv_compse_msi_msg() 两次,第一次调用包含一个虚拟 vCPU,第二次调用包含真实的 vCPU。此外,最终会调用 hv_irq_unmask()(在 x86 上)或设置 GICD 寄存器(在 arm64 上)以再次指定真实的 vCPU。这三个调用中的每一个都与 Hyper-V 交互,Hyper-V 必须决定哪个物理 CPU 应该接收中断,然后才能将其转发到客户虚拟机。不幸的是,Hyper-V 的决策过程有点受限,可能导致将物理中断集中在单个 CPU 上,从而导致性能瓶颈。有关如何解决此问题的详细信息,请参阅 hv_compose_msi_req_get_cpu() 函数上方的详细注释。

Hyper-V 虚拟 PCI 驱动程序将 irq_chip.irq_compose_msi_msg 函数实现为 hv_compose_msi_msg()。不幸的是,在 Hyper-V 上,实现需要向 Hyper-V 主机发送 VMBus 消息并等待指示收到回复消息的中断。由于 irq_chip.irq_compose_msi_msg 可以在保持 IRQ 锁定的情况下调用,因此在被中断唤醒之前执行正常的睡眠操作是行不通的。相反,hv_compose_msi_msg() 必须发送 VMBus 消息,然后轮询完成消息。此外,随着复杂性的增加,vPCI 设备可能会在轮询过程中被弹出/取消,因此也必须检测到这种情况。请参阅代码中有关这个非常棘手区域的注释。

Hyper-V 虚拟 PCI 驱动程序 (pci-hyperv.c) 中的大部分代码适用于在 x86 和 arm64 架构上运行的 Hyper-V 和 Linux 客户机。但在中断分配的管理方式上存在差异。在 x86 上,客户机中的 Hyper-V 虚拟 PCI 驱动程序必须发出一个 hypercall,以告知 Hyper-V 哪个客户机 vCPU 应被每个 MSI/MSI-X 中断中断,以及 x86_vector IRQ 域为该中断选择的 x86 中断向量号。此 hypercall 由 hv_arch_irq_unmask() 发出。在 arm64 上,Hyper-V 虚拟 PCI 驱动程序管理每个 MSI/MSI-X 中断的 SPI 分配。Hyper-V 虚拟 PCI 驱动程序将分配的 SPI 存储在 Hyper-V 模拟的架构 GICD 寄存器中,因此不需要像 x86 那样进行 hypercall。Hyper-V 不支持在 arm64 客户机虚拟机中使用 LPI 进行 vPCI 设备,因为它不模拟 GICv3 ITS。

Linux 中的 Hyper-V 虚拟 PCI 驱动程序支持驱动程序创建托管或非托管 Linux IRQ 的 vPCI 设备。如果通过 /proc/irq 接口更新非托管 IRQ 的 smp_affinity,则会调用 Hyper-V 虚拟 PCI 驱动程序,以告知 Hyper-V 主机更改中断目标,一切正常工作。但是,在 x86 上,如果 x86_vector IRQ 域由于 CPU 上的向量耗尽而需要重新分配中断向量,则没有通知 Hyper-V 主机更改的路径,并且会发生故障。幸运的是,客户机虚拟机在受限的设备环境中运行,在 CPU 上使用所有向量的情况不会发生。由于此类问题只是一个理论上的担忧,而不是一个实际的担忧,因此尚未解决。

DMA

默认情况下,Hyper-V 在创建虚拟机时会固定主机中的所有客户机虚拟机内存,并对物理 IOMMU 进行编程,以允许虚拟机对所有内存进行 DMA 访问。因此,将 PCI 设备分配给虚拟机并允许客户机操作系统对 DMA 传输进行编程是安全的。物理 IOMMU 可防止恶意客户机启动对属于主机或主机上其他虚拟机的内存的 DMA。从 Linux 客户机的角度来看,此类 DMA 传输处于“直接”模式,因为 Hyper-V 不在客户机中提供虚拟 IOMMU。

Hyper-V 假定物理 PCI 设备始终执行缓存一致性 DMA。在 x86 上运行时,架构需要此行为。在 arm64 上运行时,该架构允许缓存一致性和非缓存一致性设备,每个设备的具体行为在 ACPI DSDT 中指定。但是,当 PCI 设备分配给客户机虚拟机时,该设备不会出现在 DSDT 中,因此 Hyper-V VMBus 驱动程序会将 ACPI DSDT 中 VMBus 节点的缓存一致性信息传播到所有 VMBus 设备,包括 vPCI 设备(因为它们具有作为 VMBus 设备和 PCI 设备的双重身份)。请参见 vmbus_dma_configure()。当前 Hyper-V 版本始终指示 VMBus 具有缓存一致性,因此 arm64 上的 vPCI 设备始终被标记为缓存一致,并且 CPU 不会执行任何同步操作作为 dma_map/unmap_*() 调用的一部分。

vPCI 协议版本

如前所述,在 vPCI 设备设置和拆卸期间,消息通过 Hyper-V 主机和 Linux 客户机中的 Hyper-v vPCI 驱动程序之间的 VMBus 通道传递。某些消息在较新版本的 Hyper-V 中已进行了修订,因此客户机和主机必须就使用的 vPCI 协议版本达成一致。该版本在首次建立 VMBus 通道上的通信时进行协商。请参见 hv_pci_protocol_negotiation()。较新版本的协议将支持扩展到具有 64 个以上 vCPU 的虚拟机,并提供有关 vPCI 设备的其他信息,例如它在底层硬件中最接近的客户机虚拟 NUMA 节点。

客户机 NUMA 节点亲和性

当 vPCI 协议版本提供时,vPCI 设备的客户机 NUMA 节点亲和性将作为 Linux 设备信息的一部分存储,供 Linux 驱动程序后续使用。请参见 hv_pci_assign_numa_node()。如果协商的协议版本不支持主机提供 NUMA 亲和性信息,则 Linux 客户机会将设备的 NUMA 节点默认为 0。但是,即使协商的协议版本包括 NUMA 亲和性信息,主机提供此类信息的能力也取决于某些主机配置选项。如果客户机收到 NUMA 节点值“0”,则可能表示 NUMA 节点 0,也可能表示“没有可用信息”。不幸的是,无法从客户机端区分这两种情况。

CoCo 虚拟机中的 PCI 配置空间访问

Linux PCI 设备驱动程序使用 Linux PCI 子系统提供的一组标准函数访问 PCI 配置空间。在 Hyper-V 客户机中,这些标准函数映射到 Hyper-V 虚拟 PCI 驱动程序中的函数 hv_pcifront_read_config() 和 hv_pcifront_write_config()。在普通虚拟机中,这些 hv_pcifront_*() 函数直接访问 PCI 配置空间,并且访问会陷入到 Hyper-V 进行处理。但在 CoCo 虚拟机中,内存加密会阻止 Hyper-V 读取客户机指令流来模拟访问,因此 hv_pcifront_*() 函数必须调用 hypercall,并使用明确的参数来描述要进行的访问。

配置块后通道

Hyper-V 主机和 Linux 中的 Hyper-V 虚拟 PCI 驱动程序共同实现了主机和客户机之间的非标准后通道通信路径。后通道路径使用通过与 vPCI 设备关联的 VMBus 通道发送的消息。函数 hyperv_read_cfg_blk() 和 hyperv_write_cfg_blk() 是为 Linux 内核的其他部分提供的主要接口。在编写本文时,这些接口仅由 Mellanox mlx5 驱动程序用于将诊断数据传递到在 Azure 公有云中运行的 Hyper-V 主机。函数 hyperv_read_cfg_blk() 和 hyperv_write_cfg_blk() 在单独的模块中实现 (pci-hyperv-intf.c, 在 CONFIG_PCI_HYPERV_INTERFACE 下),该模块在非 Hyper-V 环境中运行时会有效地将其桩化。