Powerenv 上的 PCI Express I/O 虚拟化资源¶
Wei Yang <weiyang@linux.vnet.ibm.com>
Benjamin Herrenschmidt <benh@au1.ibm.com>
Bjorn Helgaas <bhelgaas@google.com>
2014 年 8 月 26 日
本文档描述了 PowerKVM 上 PCI MMIO 资源大小调整和分配的硬件要求,以及通用 PCI 代码如何处理此要求。前两节描述了可分区端点的概念以及在 P8 (IODA2) 上的实现。接下来的两节讨论了在 IODA2 上启用 SRIOV 的注意事项。
1. 可分区端点简介¶
可分区端点 (PE) 是一种将与设备或一组设备关联的各种资源分组的方法,以便在分区之间提供隔离(即,过滤 DMA、MSI 等),并提供一种冻结导致错误的设备以限制不良数据传播可能性的机制。
因此,在硬件中,有一个 PE 状态表,其中包含每对 PE 的“冻结”状态位(一个用于 MMIO,一个用于 DMA,它们一起设置但可以独立清除)。
当 PE 被冻结时,任何方向的所有存储都会被丢弃,并且所有加载都会返回全 1 的值。MSI 也被阻止。还有一些状态会捕获导致冻结的错误的详细信息等,但这并不重要。
有趣的部分是各种 PCIe 事务(MMIO、DMA 等)如何与其相应的 PE 匹配。
以下部分粗略描述了我们在 P8 (IODA2) 上拥有的内容。请记住,这都是每个 PHB(PCI 主桥)的情况。每个 PHB 都是一个完全独立的硬件实体,它复制了整个逻辑,因此有自己的一组 PE 等。
2. P8 (IODA2) 上可分区端点的实现¶
P8 每个 PHB 最多支持 256 个可分区端点。
入站
对于 DMA、MSI 和入站 PCIe 错误消息,我们有一个表(在内存中,但由芯片在硬件中访问),它提供了 PCIe RID(总线/设备/功能)与 PE 编号之间的直接对应关系。我们称之为 RTT。
对于 DMA,我们为每个 PE 提供一个完整的地址空间,该地址空间可以包含两个“窗口”,具体取决于 PCI 地址位 59 的值。每个窗口都可以配置为通过“TCE 表”(IOMMU 转换表)重新映射,该表具有此处未描述的各种可配置特性。
对于 MSI,我们在地址空间中有两个窗口(一个位于 32 位空间的顶部,另一个位于更高的位置),它们通过地址和 MSI 值的组合,将导致触发每个桥的 2048 个中断之一。中断控制器描述符表中也有一个 PE#,它与从 RTT 获取的 PE# 进行比较,以“授权”设备发出该特定中断。
错误消息只使用 RTT。
出站。那是棘手的部分。
与其他 PCI 主桥一样,Power8 IODA2 PHB 支持从 CPU 地址空间到 PCI 地址空间的“窗口”。有一个 M32 窗口和十六个 M64 窗口。它们具有不同的特性。首先是它们的共同点:它们将 CPU 地址空间的可配置部分转发到 PCIe 总线,并且大小必须是自然对齐的 2 的幂。其余的则不同
M32 窗口
大小限制为 4GB。
删除地址的最高位(高于大小),并用可配置的值替换它们。这通常用于生成 32 位 PCIe 访问。我们从 FW 在启动时配置该窗口,并且不从 Linux 中触摸它;它通常设置为将 CPU 到 PCIe 0x8000_0000..0xffff_ffff 的 2GB 地址空间部分转发。(注意:顶部 64KB 实际上保留给 MSI,但这此时不是问题;我们只需要确保 Linux 不在那里分配任何内容,但如果我们尝试,M32 逻辑会忽略它并在该空间中转发)。
它被分成 256 个大小相等的段。芯片中的一个表将每个段映射到 PE#。这允许将 MMIO 空间的部分以段粒度分配给 PE。对于 2GB 窗口,段粒度为 2GB/256 = 8MB。
现在,这是我们今天在 Linux 中使用的“主要”窗口(不包括 SR-IOV)。我们基本上使用强制桥 MMIO 窗口与段对齐/粒度的技巧,以便可以将桥后面的空间分配给 PE。
理想情况下,我们希望能够在 PE 中拥有单独的功能,但这将意味着使用完全不同的地址分配方案,其中可以将各个功能 BAR“分组”以适合一个或多个段。
M64 窗口
大小必须至少为 256MB。
不转换地址(PCIe 上的地址与 PowerBus 上的地址相同)。还有一种方法可以设置 PowerBus 不传递的最高 14 位,但我们不使用它。
可以配置为分段。当未分段时,我们可以为整个窗口指定 PE#。当分段时,一个窗口有 256 个段;但是,没有表用于将段映射到 PE#。段号是 PE#。
支持重叠。如果一个地址被多个窗口覆盖,则有一个定义的顺序来确定哪个窗口适用。
我们有代码(与 M32 内容相比相当新)利用它来处理 64 位空间中的大型 BAR
我们配置一个 M64 窗口来覆盖由 FW 为 PHB 分配的整个地址空间区域(大约 64GB,忽略 M32 的空间,它来自不同的“保留”)。我们将其配置为分段。
然后,我们执行与 M32 相同的操作,使用桥对齐技巧,以匹配那些巨大的段。
由于我们无法重新映射,因此我们还有两个额外的约束
我们在分配 64 位空间后执行 PE# 分配,因为我们使用的地址直接确定 PE#。然后,我们更新使用 32 位和 64 位空间的设备的 M32 PE#,或将剩余的 PE# 分配给仅 32 位的设备。
我们无法在硬件中“分组”段,因此如果设备最终使用多个段,我们最终将获得多个 PE#。有一种硬件机制可以将冻结状态级联到“伴随” PE,但这仅适用于 PCIe 错误消息(通常用于当您冻结交换机时,它会冻结其所有子项)。所以我们在软件中执行此操作。在这种情况下,我们损失了一点 EEH 的有效性,但这是我们找到的最好的方法。因此,当任何一个 PE 冻结时,我们会冻结该“域”的其他 PE。因此,我们引入了“主 PE”的概念,该 PE 用于 DMA、MSI 等,以及用于剩余 M64 段的“辅助 PE”。
我们希望研究使用“单 PE”模式下的其他 M64 窗口来覆盖特定的 BAR,以解决其中的一些问题,例如对于具有非常大的 BAR 的设备(例如,GPU)。这很有意义,但我们还没有完成。
3. PowerKVM 上 SR-IOV 的注意事项¶
SR-IOV 背景
PCIe SR-IOV 功能允许单个物理功能 (PF) 支持多个虚拟功能 (VF)。PF 的 SR-IOV 功能中的寄存器控制 VF 的数量以及它们是否启用。
当启用 VF 时,它们在配置空间中像正常的 PCI 设备一样出现,但 VF 配置空间头中的 BAR 是不寻常的。对于非 VF 设备,软件使用配置空间头中的 BAR 来发现 BAR 大小并为其分配地址。对于 VF 设备,软件使用PF SR-IOV 功能中的 VF BAR 寄存器来发现大小并分配地址。VF 配置空间头中的 BAR 是只读的零。
当编程 PF SR-IOV 功能中的 VF BAR 时,它会为所有相应的 VF(n) BAR 设置基地址。例如,如果对 PF SR-IOV 功能进行编程以启用八个 VF,并且它具有 1MB 的 VF BAR0,则该 VF BAR 中的地址会设置 8MB 区域的基地址。此区域分为八个连续的 1MB 区域,每个区域都是一个 VF 的 BAR0。请注意,即使 VF BAR 描述的是 8MB 区域,对齐要求也是针对单个 VF,即在此示例中为 1MB。
有几种在 PE 中隔离 VF 的策略
M32 窗口:有一个 M32 窗口,它分为 256 个大小相等的段。最精细的粒度是 256MB 窗口,带有 1MB 段。1MB 或更大的 VF BAR 可以映射到此窗口中的单独 PE。每个段都可以通过查找表单独映射到 PE,因此这非常灵活,但当所有 VF BAR 的大小相同时,它效果最佳。如果它们的大小不同,则整个窗口必须足够小,以便段大小与最小的 VF BAR 匹配,这意味着较大的 VF BAR 跨越多个段。
非分段 M64 窗口:一个非分段的 M64 窗口会完全映射到一个单独的 PE,因此它只能隔离一个 VF。
单分段 M64 窗口:一个分段的 M64 窗口可以像 M32 窗口一样使用,但是这些分段不能单独映射到 PE(分段号就是 PE#),因此灵活性不高。一个拥有多个 BAR 的 VF 将不得不位于多个 PE 的“域”中,这不如单个 PE 的隔离性好。
多分段 M64 窗口:通常,每个窗口被分成 256 个大小相等的段,并且段号是 PE#。但是,如果我们使用多个 M64 窗口,它们可以设置为不同的基地址和不同的段大小。如果我们有每个都具有 1MB BAR 和 32MB BAR 的 VF,我们可以使用一个 M64 窗口来分配 1MB 的段,另一个 M64 窗口来分配 32MB 的段。
最后,计划使用 M64 窗口进行 SR-IOV,这将在接下来的两个部分中进行更详细的描述。对于给定的 VF BAR,我们需要有效地保留整个 256 个段(256 * VF BAR 大小),并将 VF BAR 定位在 M64 窗口内空闲的段/PE 范围的开头。
当然,目标是能够为每个 VF 提供单独的 PE。
IODA2 平台有 16 个 M64 窗口,用于将 MMIO 范围映射到 PE#。每个 M64 窗口定义一个 MMIO 范围,该范围分为 256 段,每段对应一个 PE。
我们决定利用这个 M64 窗口将 VF 映射到单独的 PE,因为 SR-IOV VF BAR 的大小都相同。
但是这样做又引入了另一个问题:total_VFs 通常小于 M64 窗口段的数量,因此如果我们将一个 VF BAR 直接映射到一个 M64 窗口,则 M64 窗口的某些部分将映射到另一个设备的 MMIO 范围。
IODA 支持 256 个 PE,因此分段窗口包含 256 段,因此如果 total_VFs 小于 256,我们就会遇到图 1.0 中的情况,其中 M64 窗口的段 [total_VFs, 255] 可能映射到其他设备上的某些 MMIO 范围
0 1 total_VFs - 1 +------+------+- -+------+------+ | | | ... | | | +------+------+- -+------+------+ VF(n) BAR space 0 1 total_VFs - 1 255 +------+------+- -+------+------+- -+------+------+ | | | ... | | | ... | | | +------+------+- -+------+------+- -+------+------+ M64 window Figure 1.0 Direct map VF(n) BAR space我们目前的解决方案是分配 256 个段,即使 VF(n) BAR 空间不需要那么多,如图 1.1 所示
0 1 total_VFs - 1 255 +------+------+- -+------+------+- -+------+------+ | | | ... | | | ... | | | +------+------+- -+------+------+- -+------+------+ VF(n) BAR space + extra 0 1 total_VFs - 1 255 +------+------+- -+------+------+- -+------+------+ | | | ... | | | ... | | | +------+------+- -+------+------+- -+------+------+ M64 window Figure 1.1 Map VF(n) BAR space + extra分配额外的空间可确保整个 M64 窗口将被分配给这个 SR-IOV 设备,并且没有空间可用于其他设备。请注意,这仅扩展了软件中保留的空间;仍然只有 total_VFs 个 VF,它们仅响应段 [0, total_VFs - 1]。硬件中没有任何东西响应段 [total_VFs, 255]。
4. 对通用 PCI 代码的影响¶
PCIe SR-IOV 规范要求 VF(n) BAR 空间的基地址与单个 VF BAR 的大小对齐。
在 IODA2 中,MMIO 地址决定 PE#。如果地址在 M32 窗口中,我们可以通过更新将段转换为 PE# 的表来设置 PE#。同样,如果地址在非分段的 M64 窗口中,我们可以设置该窗口的 PE#。但是,如果它在分段的 M64 窗口中,则段号就是 PE#。
因此,控制 VF 的 PE# 的唯一方法是更改 VF BAR 中 VF(n) BAR 空间的基地址。如果 PCI 内核分配了 VF(n) BAR 空间所需的精确空间量,则 VF BAR 值是固定的,无法更改。
另一方面,如果 PCI 内核分配了额外的空间,则只要整个 VF(n) BAR 空间仍然在内核分配的空间内,就可以更改 VF BAR 值。
理想情况下,段大小将与单个 VF BAR 大小相同。然后,每个 VF 将位于其自己的 PE 中。VF BAR(以及 PE#)是连续的。如果 VF0 在 PE(x) 中,则 VF(n) 在 PE(x+n) 中。如果我们分配 256 个段,则 VF0 的 PE# 有 (256 - numVFs) 个选择。
如果段大小小于 VF BAR 大小,则需要几个段才能覆盖一个 VF BAR,并且一个 VF 将位于多个 PE 中。这是可能的,但隔离性不如前者好,并且它减少了 PE# 的选择数量,因为 VF(n) BAR 空间不是只消耗 numVFs 个段,而是会消耗 (numVFs * n) 个段。这意味着可用于调整 VF(n) BAR 空间基址的可用段数量不多。