PCI 直通设备¶
在 Hyper-V 客户虚拟机中,PCI 直通设备(也称为虚拟 PCI 设备,或 vPCI 设备)是直接映射到虚拟机物理地址空间的物理 PCI 设备。客户设备驱动程序可以直接与硬件交互,无需主机管理程序的中介。与由管理程序虚拟化的设备相比,这种方法为设备提供了更高的带宽访问和更低的延迟。该设备应像在裸机上运行时一样出现在客户机中,因此无需更改该设备的 Linux 设备驱动程序。
Hyper-V 中 vPCI 设备的术语是“离散设备分配”(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“offer”机制作为 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 offer 机制动态呈现,因此它们不会出现在 Linux 客户机的 ACPI 表中。vPCI 设备可以在虚拟机的整个生命周期中的任何时间添加到虚拟机或从虚拟机中移除,而不仅仅是在初始启动期间。
通过这种方法,vPCI 设备同时是 VMBus 设备和 PCI 设备。响应 VMBus offer 消息,hv_pci_probe() 函数运行并建立与 Hyper-V 主机上 vPCI VSP 的 VMBus 连接。该连接具有单个 VMBus 通道。该通道用于与 vPCI VSP 交换消息,以便在 Linux 中设置和配置 vPCI 设备。一旦设备在 Linux 中作为 PCI 设备完全配置,VMBus 通道仅在 Linux 更改客户机中要中断的 vCPU 时,或在虚拟机运行时 vPCI 设备从虚拟机中移除时使用。设备的持续操作直接在设备的 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() 有一个算法来解决冲突。冲突解决旨在在同一虚拟机的重启之间保持稳定,以便 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 主机可以在虚拟机的整个生命周期中的任何时间发起从客户虚拟机中移除 vPCI 设备。移除是由 Hyper-V 主机上的管理员操作引起的,不受客户机操作系统的控制。
通过与 vPCI 设备关联的 VMBus 通道,主机向客户机发送未经请求的“弹出 (Eject)”消息,以通知客户虚拟机设备已被移除。收到此类消息后,Linux 中的 Hyper-V 虚拟 PCI 驱动程序会异步调用 Linux 内核 PCI 子系统函数来关闭和移除设备。当这些调用完成后,一个“弹出完成 (Ejection Complete)”消息通过 VMBus 通道发送回 Hyper-V,表明设备已被移除。此时,Hyper-V 向 Linux 客户机发送一个 VMBus rescind 消息,Linux 中的 VMBus 驱动程序通过移除设备的 VMBus 身份来处理此消息。一旦该处理完成,设备存在的所有痕迹都将从 Linux 内核中消失。rescind 消息还向客户机表明 Hyper-V 已停止在客户机中为 vPCI 设备提供支持。如果客户机尝试访问该设备的 MMIO 空间,这将是无效引用。影响设备的 Hypercall 将返回错误,并且 VMBus 通道中发送的任何进一步消息都将被忽略。
发送 Eject 消息后,Hyper-V 允许客户虚拟机 60 秒的时间来干净地关闭设备并响应 Ejection Complete,然后才发送 VMBus rescind 消息。如果由于任何原因,Eject 步骤未在允许的 60 秒内完成,Hyper-V 主机将强制执行 rescind 步骤,这可能会导致客户机中出现级联错误,因为从客户机的角度来看,设备现在已不存在,并且访问设备 MMIO 空间将失败。
由于弹出是异步的,并且可以在客户虚拟机生命周期中的任何时候发生,因此 Hyper-V 虚拟 PCI 驱动程序中的正确同步非常棘手。甚至在刚提供的 vPCI 设备完全设置之前,就已经观察到弹出。多年来,Hyper-V 虚拟 PCI 驱动程序已多次更新,以修复在不合时宜的时间发生弹出时的竞态条件。修改此代码时必须小心,以防止重新引入此类问题。请参阅代码中的注释。
中断分配¶
Hyper-V 虚拟 PCI 驱动程序支持使用 MSI、multi-MSI 或 MSI-X 的 vPCI 设备。为特定 MSI 或 MSI-X 消息分配将接收中断的客户机 vCPU 是复杂的,因为 Linux 中 IRQ 的设置方式映射到 Hyper-V 接口。对于单 MSI 和 MSI-X 情况,Linux 会两次调用 hv_compose_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 存储在架构 GICD 寄存器中,Hyper-V 会模拟这些寄存器,因此不需要像 x86 那样进行 hypercall。Hyper-V 不支持在 arm64 客户虚拟机中为 vPCI 设备使用 LPI,因为它不模拟 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_*() 函数必须通过显式参数描述要进行的访问来调用 hypercalls。
配置块后向通道¶
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 环境中运行时有效地将它们stub out。