POWER 上的嵌套 KVM

简介

本文档介绍了如果 hypervisor 实现了 hypercall,guest 操作系统如何充当 hypervisor 并通过使用 hypercall 运行嵌套 guest。术语 L0、L1 和 L2 用于指代不同的软件实体。L0 是通常称为“主机”或“hypervisor”的 hypervisor 模式实体。L1 是直接在 L0 下运行并由 L0 启动和控制的 guest 虚拟机。L2 是由充当 hypervisor 的 L1 启动和控制的 guest 虚拟机。

现有 API

自 2018 年以来,Linux/KVM 一直支持作为 L0 或 L1 的嵌套

添加了 L0 代码

commit 8e3f5fc1045dc49fd175b978c5457f5f51e7a2ce
Author: Paul Mackerras <paulus@ozlabs.org>
Date:   Mon Oct 8 16:31:03 2018 +1100
KVM: PPC: Book3S HV: Framework and hcall stubs for nested virtualization

添加了 L1 代码

commit 360cae313702cdd0b90f82c261a8302fecef030a
Author: Paul Mackerras <paulus@ozlabs.org>
Date:   Mon Oct 8 16:31:04 2018 +1100
KVM: PPC: Book3S HV: Nested guest entry via hypercall

此 API 主要使用单个 hcall h_enter_nested()。L1 调用此函数以告知 L0 使用给定状态启动 L2 vCPU。然后,L0 启动此 L2 并运行直到达到 L2 退出条件。一旦 L2 退出,L2 的状态将由 L0 返回给 L1。完整的 L2 vCPU 状态始终在运行 L2 时从 L1 传输到 L1。L0 不保留任何关于 L2 vCPU 的状态(除了 L0 在 L1 -> L2 进入和 L2 -> L1 退出时的短暂序列中)。

L0 保留的唯一状态是分区表。L1 使用 h_set_partition_table() hcall 注册它的分区表。L0 拥有的关于 L2 的所有其他状态都是缓存状态(例如影子页表)。

L1 可以运行任何 L2 或 vCPU,而无需首先通知 L0。它只需使用 h_enter_nested() 启动 vCPU。L2 和 vCPU 的创建是在调用 h_enter_nested() 时隐式完成的。

在本文档中,我们将此现有 API 称为 v1 API。

新的 PAPR API

新的 PAPR API 更改了 v1 API,使得创建 L2 和关联的 vCPU 是显式的。在本文档中,我们将其称为 v2 API。

h_enter_nested() 被 H_GUEST_VCPU_RUN() 替换。在调用此函数之前,L1 必须使用 h_guest_create() 显式创建 L2,并使用 h_guest_create_vCPU() 创建任何关联的 vCPU()。可以使用 h_guest_{g|s}et hcall 执行获取和设置 vCPU 状态的操作。

L1 创建 L2、运行它并删除它的基本执行流程是

  • L1 和 L0 使用 H_GUEST_{G,S}ET_CAPABILITIES() 协商功能(通常在 L1 启动时)。

  • L1 请求 L0 使用 H_GUEST_CREATE() 创建 L2 并接收一个令牌

  • L1 请求 L0 使用 H_GUEST_CREATE_VCPU() 创建一个 L2 vCPU

  • L1 和 L0 使用 H_GUEST_{G,S}ET() hcall 传递 vCPU 状态

  • L1 请求 L0 运行运行 H_GUEST_VCPU_RUN() hcall 的 vCPU

  • L1 使用 H_GUEST_DELETE() 删除 L2

以下是各个 hcall 的更多详细信息

HCALL 详细信息

提供此文档是为了让您全面了解 API。它并不旨在提供实现 L1 或 L0 所需的所有详细信息。有关更多详细信息,请参阅最新版本的 PAPR。

所有这些 HCALL 都是由 L1 发送给 L0 的。

H_GUEST_GET_CAPABILITIES()

调用此函数以获取 L0 嵌套 hypervisor 的功能。这包括诸如支持作为 L2 的 CPU 版本(例如 POWER9、POWER10)等功能

H_GUEST_GET_CAPABILITIES(uint64 flags)

Parameters:
  Input:
    flags: Reserved
  Output:
    R3: Return code
    R4: Hypervisor Supported Capabilities bitmap 1

H_GUEST_SET_CAPABILITIES()

调用此函数以告知 L0 L1 hypervisor 的功能。此处传递的标志集与 H_GUEST_GET_CAPABILITIES() 相同

通常,将首先调用 GET,然后调用 SET,其中包含从 GET 返回的标志的子集。此过程允许 L0 和 L1 协商一组商定的功能

H_GUEST_SET_CAPABILITIES(uint64 flags,
                         uint64 capabilitiesBitmap1)
Parameters:
  Input:
    flags: Reserved
    capabilitiesBitmap1: Only capabilities advertised through
                         H_GUEST_GET_CAPABILITIES
  Output:
    R3: Return code
    R4: If R3 = H_P2: The number of invalid bitmaps
    R5: If R3 = H_P2: The index of first invalid bitmap

H_GUEST_CREATE()

调用此函数以创建 L2。将返回已创建的 L2 的唯一 ID(类似于 LPID),该 ID 可用于后续 HCALL 上以标识 L2

H_GUEST_CREATE(uint64 flags,
               uint64 continueToken);
Parameters:
  Input:
    flags: Reserved
    continueToken: Initial call set to -1. Subsequent calls,
                   after H_Busy or H_LongBusyOrder has been
                   returned, value that was returned in R4.
  Output:
    R3: Return code. Notable:
      H_Not_Enough_Resources: Unable to create Guest VCPU due to not
      enough Hypervisor memory. See H_GUEST_CREATE_GET_STATE(flags =
      takeOwnershipOfVcpuState)
    R4: If R3 = H_Busy or_H_LongBusyOrder -> continueToken

H_GUEST_CREATE_VCPU()

调用此函数以创建与 L2 关联的 vCPU。应传递 L2 id(从 H_GUEST_CREATE() 返回)。此外,还传入一个唯一的(对于此 L2)vCPUid。此 vCPUid 由 L1 分配

H_GUEST_CREATE_VCPU(uint64 flags,
                    uint64 guestId,
                    uint64 vcpuId);
Parameters:
  Input:
    flags: Reserved
    guestId: ID obtained from H_GUEST_CREATE
    vcpuId: ID of the vCPU to be created. This must be within the
            range of 0 to 2047
  Output:
    R3: Return code. Notable:
      H_Not_Enough_Resources: Unable to create Guest VCPU due to not
      enough Hypervisor memory. See H_GUEST_CREATE_GET_STATE(flags =
      takeOwnershipOfVcpuState)

H_GUEST_GET_STATE()

调用此函数以获取与 L2 关联的状态(guest 范围或 vCPU 特定)。此信息通过 Guest State Buffer (GSB) 传递,GSB 是一种标准格式,如本文档稍后所述,以下是必要的详细信息

这可以获取 L2 范围或 vCPU 特定信息。L2 范围的示例是时基偏移或进程范围的页表信息。vCPU 特定的示例是 GPR 或 VSR。flags 参数中的一位指定此调用是 L2 范围还是 vCPU 特定的,GSB 中的 ID 必须与此匹配。

L1 提供指向 GSB 的指针作为此调用的参数。还提供了与要设置的状态关联的 L2 和 vCPU ID。

L1 仅写入 GSB 中的 ID 和大小。L0 写入 GSB 中每个 ID 的关联值

H_GUEST_GET_STATE(uint64 flags,
                         uint64 guestId,
                         uint64 vcpuId,
                         uint64 dataBuffer,
                         uint64 dataBufferSizeInBytes);
Parameters:
  Input:
    flags:
       Bit 0: getGuestWideState: Request state of the Guest instead
         of an individual VCPU.
       Bit 1: getHostWideState: Request stats of the Host. This causes
         the guestId and vcpuId parameters to be ignored and attempting
         to get the VCPU/Guest state will cause an error.
       Bits 2-63: Reserved
    guestId: ID obtained from H_GUEST_CREATE
    vcpuId: ID of the vCPU pass to H_GUEST_CREATE_VCPU
    dataBuffer: A L1 real address of the GSB.
      If takeOwnershipOfVcpuState, size must be at least the size
      returned by ID=0x0001
    dataBufferSizeInBytes: Size of dataBuffer
  Output:
    R3: Return code
    R4: If R3 = H_Invalid_Element_Id: The array index of the bad
          element ID.
        If R3 = H_Invalid_Element_Size: The array index of the bad
           element size.
        If R3 = H_Invalid_Element_Value: The array index of the bad
           element value.

H_GUEST_SET_STATE()

调用此函数以设置 L2 范围或 vCPU 特定的 L2 状态。此信息通过 Guest State Buffer (GSB) 传递,以下是必要的详细信息

这可以设置 L2 范围或 vCPU 特定信息。L2 范围的示例是时基偏移或进程范围的页表信息。vCPU 特定的示例是 GPR 或 VSR。flags 参数中的一位指定此调用是 L2 范围还是 vCPU 特定的,GSB 中的 ID 必须与此匹配。

L1 提供指向 GSB 的指针作为此调用的参数。还提供了与要设置的状态关联的 L2 和 vCPU ID。

L1 写入 GSB 中的所有值,L0 仅读取此调用的 GSB

H_GUEST_SET_STATE(uint64 flags,
                  uint64 guestId,
                  uint64 vcpuId,
                  uint64 dataBuffer,
                  uint64 dataBufferSizeInBytes);
Parameters:
  Input:
    flags:
       Bit 0: getGuestWideState: Request state of the Guest instead
         of an individual VCPU.
       Bit 1: returnOwnershipOfVcpuState Return Guest VCPU state. See
         GET_STATE takeOwnershipOfVcpuState
       Bits 2-63: Reserved
    guestId: ID obtained from H_GUEST_CREATE
    vcpuId: ID of the vCPU pass to H_GUEST_CREATE_VCPU
    dataBuffer: A L1 real address of the GSB.
      If takeOwnershipOfVcpuState, size must be at least the size
      returned by ID=0x0001
    dataBufferSizeInBytes: Size of dataBuffer
  Output:
    R3: Return code
    R4: If R3 = H_Invalid_Element_Id: The array index of the bad
          element ID.
        If R3 = H_Invalid_Element_Size: The array index of the bad
           element size.
        If R3 = H_Invalid_Element_Value: The array index of the bad
           element value.

H_GUEST_RUN_VCPU()

调用此函数以运行 L2 vCPU。L2 和 vCPU ID 作为参数传入。vCPU 使用先前使用 H_GUEST_SET_STATE() 设置的状态运行。当 L2 退出时,L1 将从此 hcall 恢复。

此 hcall 还具有关联的输入和输出 GSB。与 H_GUEST_{S,G}ET_STATE() 不同,这些 GSB 指针不是作为参数传递给 hcall 的(这样做是为了提高性能)。必须使用 ID 0x0c00 和 0x0c01 通过 H_GUEST_SET_STATE() 调用预先注册这些 GSB 的位置(参见下表)。

输入 GSB 可能仅包含要设置的 VCPU 特定元素。如果不需要设置任何内容,此 GSB 也可能包含零元素(即 GSB 的前 4 个字节为 0)。

在 hcall 退出时,输出缓冲区将填充 L0 确定的元素。退出原因是包含在 GPR4 中的(即 NIP 放在 GPR4 中)。返回的元素取决于退出类型。例如,如果退出原因是 L2 执行 hcall(GPR4 = 0xc00),则 GPR3-12 在输出 GSB 中提供,因为这可能是服务 hcall 所需的状态。如果需要其他状态,则 L1 可能会调用 H_GUEST_GET_STATE()。

为了在 L2 中合成中断,在调用 H_GUEST_RUN_VCPU() 时,L1 可能会设置一个标志(作为 hcall 参数),并且 L0 将在 L2 中合成中断。或者,L1 可能会使用 H_GUEST_SET_STATE() 或 H_GUEST_RUN_VCPU() 输入 GSB 来设置适当的状态,从而自行合成中断

H_GUEST_RUN_VCPU(uint64 flags,
                 uint64 guestId,
                 uint64 vcpuId,
                 uint64 dataBuffer,
                 uint64 dataBufferSizeInBytes);
Parameters:
  Input:
    flags:
       Bit 0: generateExternalInterrupt: Generate an external interrupt
       Bit 1: generatePrivilegedDoorbell: Generate a Privileged Doorbell
       Bit 2: sendToSystemReset”: Generate a System Reset Interrupt
       Bits 3-63: Reserved
    guestId: ID obtained from H_GUEST_CREATE
    vcpuId: ID of the vCPU pass to H_GUEST_CREATE_VCPU
  Output:
    R3: Return code
    R4: If R3 = H_Success: The reason L1 VCPU exited (ie. NIA)
          0x000: The VCPU stopped running for an unspecified reason. An
            example of this is the Hypervisor stopping a VCPU running
            due to an outstanding interrupt for the Host Partition.
          0x980: HDEC
          0xC00: HCALL
          0xE00: HDSI
          0xE20: HISI
          0xE40: HEA
          0xF80: HV Fac Unavail
        If R3 = H_Invalid_Element_Id, H_Invalid_Element_Size, or
          H_Invalid_Element_Value: R4 is offset of the invalid element
          in the input buffer.

H_GUEST_DELETE()

调用此函数以删除 L2。所有关联的 vCPU 也将被删除。不提供特定的 vCPU 删除调用。

可以提供一个标志来删除所有 guest。在 kdump/kexec 的情况下,这用于重置 L0

H_GUEST_DELETE(uint64 flags,
               uint64 guestId)
Parameters:
  Input:
    flags:
       Bit 0: deleteAllGuests: deletes all guests
       Bits 1-63: Reserved
    guestId: ID obtained from H_GUEST_CREATE
  Output:
    R3: Return code

Guest 状态缓冲区

Guest State Buffer (GSB) 是通过 H_GUEST_{G,S}ET() 和 H_GUEST_VCPU_RUN() 调用在 L1 和 L0 之间传递关于 L2 状态的主要方法。

状态可能与整个 L2(例如时基偏移)或特定的 L2 vCPU(例如 GPR 状态)关联。只有 L2 VCPU 状态才能通过 H_GUEST_VCPU_RUN() 设置。

GSB 中的所有数据都是大端字节序(这是 PAPR 中的标准)。

Guest 状态缓冲区具有一个标头,该标头给出了元素的数量,后跟 GSB 元素本身。

GSB 标头

偏移字节

大小字节

目的

0

4

元素数量

4

Guest 状态缓冲区元素

GSB 元素

偏移字节

大小字节

目的

0

2

ID

2

2

值的大小

4

如上

GSB 元素中的 ID 指定要设置的内容。这包括诸如 GPR、VSR、SPR 等架构状态,以及诸如时基偏移和分区范围页表信息等关于分区的一些元数据。

ID

大小字节

RW

(H)ost (G)uest (T)hread 范围

详细信息

0x0000

RW

TG

NOP 元素

0x0001

0x08

R

G

L0 vCPU 状态的大小。参见:H_GUEST_GET_STATE:flags = takeOwnershipOfVcpuState

0x0002

0x08

R

G

运行 vCPU 输出缓冲区的大小

0x0003

0x04

RW

G

逻辑 PVR

0x0004

0x08

RW

G

TB 偏移(L1 相对)

0x0005

0x18

RW

G

分区范围页表信息

  • 0x00 Addr part 范围表

  • 0x08 地址位数

  • 0x10 根目录大小

0x0006

0x10

RW

G

进程表信息

  • 0x0 地址进程范围表

  • 0x8 表大小。

0x0007- 0x07FF

保留

0x0800

0x08

R

H

L0 的 Guest 管理空间中用于 L1-Lpar 的当前使用量(以字节为单位)。

0x0801

0x08

R

H

L0 的 Guest 管理空间中用于 L1-Lpar 的最大可用字节数

0x0802

0x08

R

H

L0 的 Guest 页表管理空间中用于 L1-Lpar 的当前使用量(以字节为单位)

0x0803

0x08

R

H

L0 的 Guest 页表管理空间中用于 L1-Lpar 的最大可用字节数

0x0804

0x08

R

H

由于过度提交,从 L0 Guest 的页表管理空间回收的累积字节数

0x0805- 0x0BFF

保留

0x0C00

0x10

RW

T

运行 vCPU 输入缓冲区

  • 0x0 缓冲区地址

  • 0x8 缓冲区大小。

0x0C01

0x10

RW

T

运行 vCPU 输出缓冲区

  • 0x0 缓冲区地址

  • 0x8 缓冲区大小。

0x0C02

0x08

RW

T

vCPU VPA 地址

0x0C03- 0x0FFF

保留

0x1000- 0x101F

0x08

RW

T

GPR 0-31

0x1020

0x08

T

T

HDEC 过期 TB

0x1021

0x08

RW

T

NIA

0x1022

0x08

RW

T

MSR

0x1023

0x08

RW

T

LR

0x1024

0x08

RW

T

XER

0x1025

0x08

RW

T

CTR

0x1026

0x08

RW

T

CFAR

0x1027

0x08

RW

T

SRR0

0x1028

0x08

RW

T

SRR1

0x1029

0x08

RW

T

DAR

0x102A

0x08

RW

T

DEC 过期 TB

0x102B

0x08

RW

T

VTB

0x102C

0x08

RW

T

LPCR

0x102D

0x08

RW

T

HFSCR

0x102E

0x08

RW

T

FSCR

0x102F

0x08

RW

T

FPSCR

0x1030

0x08

RW

T

DAWR0

0x1031

0x08

RW

T

DAWR1

0x1032

0x08

RW

T

CIABR

0x1033

0x08

RW

T

PURR

0x1034

0x08

RW

T

SPURR

0x1035

0x08

RW

T

IC

0x1036- 0x1039

0x08

RW

T

SPRG 0-3

0x103A

0x08

W

T

PPR

0x103B 0x103E

0x08

RW

T

MMCR 0-3

0x103F

0x08

RW

T

MMCRA

0x1040

0x08

RW

T

SIER

0x1041

0x08

RW

T

SIER 2

0x1042

0x08

RW

T

SIER 3

0x1043

0x08

RW

T

BESCR

0x1044

0x08

RW

T

EBBHR

0x1045

0x08

RW

T

EBBRR

0x1046

0x08

RW

T

AMR

0x1047

0x08

RW

T

IAMR

0x1048

0x08

RW

T

AMOR

0x1049

0x08

RW

T

UAMOR

0x104A

0x08

RW

T

SDAR

0x104B

0x08

RW

T

SIAR

0x104C

0x08

RW

T

DSCR

0x104D

0x08

RW

T

TAR

0x104E

0x08

RW

T

DEXCR

0x104F

0x08

RW

T

HDEXCR

0x1050

0x08

RW

T

HASHKEYR

0x1051

0x08

RW

T

HASHPKEYR

0x1052

0x08

RW

T

CTRL

0x1053

0x08

RW

T

DPDES

0x1054- 0x1FFF

保留

0x2000

0x04

RW

T

CR

0x2001

0x04

RW

T

PIDR

0x2002

0x04

RW

T

DSISR

0x2003

0x04

RW

T

VSCR

0x2004

0x04

RW

T

VRSAVE

0x2005

0x04

RW

T

DAWRX0

0x2006

0x04

RW

T

DAWRX1

0x2007- 0x200c

0x04

RW

T

PMC 1-6

0x200D

0x04

RW

T

WORT

0x200E

0x04

RW

T

PSPB

0x200F- 0x2FFF

保留

0x3000- 0x303F

0x10

RW

T

VSR 0-63

0x3040- 0xEFFF

保留

0xF000

0x08

R

T

HDAR

0xF001

0x04

R

T

HDSISR

0xF002

0x04

R

T

HEIR

0xF003

0x08

R

T

ASDR

其他信息

不在 ptregs/hvregs 中的状态

在 v1 API 中,某些状态不在 ptregs/hvstate 中。这包括向量寄存器和一些 SPR。为了让 L1 为 L2 设置此状态,L1 在 h_enter_nested() 调用之前加载这些硬件寄存器,并且 L0 确保它们最终成为 L2 状态(通过不触摸它们)。

v2 API 删除了此功能,并通过 GSB 显式设置此状态。

L1 实现细节:缓存状态

在 v1 API 中,所有状态都在每次 h_enter_nested() hcall 上从 L1 发送到 L0,反之亦然。如果 L0 当前未运行任何 L2,则 L0 没有关于它们的状态信息。唯一的例外是分区表的位置,该位置通过 h_set_partition_table() 注册。

v2 API 更改了此功能,以便 L0 即使在其 vCPU 不再运行时也保留 L2 状态。这意味着 L1 仅需要在需要修改 L2 状态时或其值已过时时才需要与 L0 通信关于 L2 状态。这为性能优化提供了机会。

当 vCPU 从 H_GUEST_RUN_VCPU() 调用退出时,L1 在内部将所有 L2 状态标记为无效。这意味着如果 L1 想要知道 L2 状态(例如通过 kvm_get_one_reg() 调用),则需要调用 H_GUEST_GET_STATE() 来获取该状态。一旦读取,它将在 L1 中标记为有效,直到再次运行 L2。

此外,当 L1 修改 L2 vcpu 状态时,在再次运行该 L2 vcpu 之前,它不需要将其写入 L0。因此,当 L1 更新状态时(例如通过 kvm_set_one_reg() 调用),它会写入内部 L1 副本,并且仅当 L2 再次通过 H_GUEST_VCPU_RUN() 输入缓冲区运行时才将此副本刷新到 L0。

L1 通过这种延迟更新状态的方式避免了不必要的 H_GUEST_{G|S}ET_STATE() 调用。