PCI Express I/O 虚拟化资源在 Powerenv 上的实现

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 代码如何处理这些需求。前两部分描述了可分区端点(Partitionable Endpoints)的概念及其在 P8 (IODA2) 上的实现。接下来的两部分讨论了在 IODA2 上启用 SR-IOV 的注意事项。

1. 可分区端点简介

可分区端点(PE)是一种将与某个设备或一组设备相关的各种资源进行分组的方法,以提供分区之间的隔离(即过滤 DMA、MSI 等),并提供一种机制来冻结引发错误的设备,从而限制错误数据传播的可能性。

因此,在硬件中有一个 PE 状态表,其中包含每个 PE 的一对“冻结”状态位(一个用于 MMIO,一个用于 DMA,它们会同时置位,但可以独立清除)。

当 PE 被冻结时,任何方向的所有 store 操作都会被丢弃,所有 load 操作都会返回全 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 中拥有独立的功能(function),但这将意味着使用完全不同的地址分配方案,在该方案中,各个功能的 BAR 可以被“分组”以契合一个或多个段。

    • M64 窗口

      • 大小必须至少为 256MB。

      • 不转换地址(PCIe 上的地址与 PowerBus 上的地址相同)。有一种方法还可以设置 PowerBus 未传送的高 14 位,但我们不使用此功能。

      • 可以配置为分段模式。当未分段时,我们可以为整个窗口指定 PE 编号。当分段时,一个窗口有 256 个段;然而,并没有将段映射到 PE 编号的表。段编号就是 PE 编号。

      • 支持重叠。如果一个地址被多个窗口覆盖,则存在一个明确的顺序来决定适用哪个窗口。

    我们有一些代码(与 M32 相关内容相比相当新)利用了这一点,用于处理 64 位空间中的大 BAR。

    我们配置一个 M64 窗口,以覆盖由固件为 PHB 分配的整个地址空间区域(大约 64GB,忽略 M32 的空间,它来自不同的“保留区”)。我们将其配置为分段模式。

    然后我们采取与 M32 相同的方法,使用桥对齐技巧,来匹配这些巨大的段。

    由于我们无法进行重映射,因此我们面临两个额外的限制:

    • 我们在 64 位空间分配完*之后*才进行 PE 编号分配,因为我们使用的地址直接决定了 PE 编号。然后,我们为同时使用 32 位和 64 位空间的设备更新 M32 PE 编号,或者将剩余的 PE 编号分配给仅使用 32 位的设备。

    • 我们无法在硬件中“分组”段,因此如果一个设备最终使用了多个段,我们最终会得到多个 PE 编号。存在一种硬件机制可使冻结状态级联到“伴随”PE,但这仅适用于 PCIe 错误消息(通常用于确保如果你冻结了一个交换机,它会冻结其所有子设备)。所以我们在软件中实现这一点。在这种情况下,我们损失了一点 EEH 的效力,但这是我们找到的最佳方案。因此,当任意一个 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 Capability 寄存器控制着 VF 的数量以及它们是否已启用。

    当启用 VF 时,它们在配置空间中看起来像普通的 PCI 设备,但 VF 配置空间头部中的 BAR 非常特殊。对于非 VF 设备,软件使用配置空间头部中的 BAR 来发现 BAR 大小并为其分配地址。对于 VF 设备,软件使用 PF SR-IOV Capability 中的 VF BAR 寄存器来发现大小并分配地址。VF 配置空间头部中的 BAR 是只读的零。

    当对 PF SR-IOV Capability 中的 VF BAR 进行编程时,它会设置所有对应 VF(n) BAR 的基址。例如,如果将 PF SR-IOV Capability 编程为启用八个 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 窗口,它们可以被设置为不同的基址和不同的段大小。如果我们拥有的 VF 分别具有 1MB BAR 和 32MB BAR,我们可以使用一个 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 * n) 个段,而不是仅消耗 numVFs 个段。这意味着用于调整 VF(n) BAR 空间基址的可用段变少了。