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 空间基址的可用段数量不多。