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 地址空间的 2GB 部分转发到 PCIe 0x8000_0000..0xffff_ffff。(注意:顶部 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 错误消息(通常用于冻结交换机,它会冻结其所有子项)。所以我们在 SW 中做。在这种情况下,我们失去了一点 EEH 的有效性,但这是我们发现的最好的。因此,当任何 PE 冻结时,我们会冻结该“域”的其他 PE。因此,我们引入了“主 PE”的概念,它用于 DMA、MSI 等,而“辅助 PE”用于剩余的 M64 段。

    我们希望研究在“单个 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 个大小相等的段。可能的最佳粒度是具有 1MB 段的 256MB 窗口。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_VF 通常小于 M64 窗口段的数量,因此如果我们直接将一个 VF BAR 映射到一个 M64 窗口,则 M64 窗口的某些部分将映射到另一个设备的 MMIO 范围。

IODA 支持 256 个 PE,因此分段窗口包含 256 个段,因此如果 total_VF 小于 256,则会出现图 1.0 中的情况,其中 M64 窗口的段 [total_VF, 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_VF 个 VF,它们仅响应段 [0, total_VF - 1]。硬件中没有任何东西响应段 [total_VF, 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 - numVF) 个选择。

如果段大小小于 VF BAR 大小,则需要多个段来覆盖 VF BAR,并且 VF 将位于多个 PE 中。这是可能的,但隔离效果不是很好,并且它减少了 PE# 选择的数量,因为 VF(n) BAR 空间将占用 (numVF * n) 个段,而不是仅占用 numVF 个段。这意味着没有那么多可用于调整 VF(n) BAR 空间基址的可用段。