POWER 上的嵌套 KVM

简介

本文档解释了如果虚拟机监控程序(hypervisor)实现了相关 hypercall,客户机操作系统如何充当虚拟机监控程序并通过 hypercall 运行嵌套客户机。术语 L0、L1 和 L2 用于指代不同的软件实体。L0 是通常被称为“宿主机(host)”或“虚拟机监控程序(hypervisor)”的 hypervisor 模式实体。L1 是直接在 L0 下运行的客户机虚拟机,由 L0 启动和控制。L2 是充当虚拟机监控程序的 L1 所启动和控制的客户机虚拟机。

现有 API

Linux/KVM 自 2018 年起就开始支持作为 L0 或 L1 的嵌套(Nesting)功能

添加了 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 一旦退出,L0 就会将 L2 的状态交还给 L1。当运行 L2 时,完整的 L2 vCPU 状态总是在 L1 之间进行双向传输。L0 不会在 L2 vCPU 上保留任何状态(除了 L0 在 L1 -> L2 进入和 L2 -> L1 退出时的简短序列中)。

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

L1 可以在不事先通知 L0 的情况下运行任何 L2 或 vCPU。它只需使用 h_enter_nested() 启动 vCPU。每当调用 h_enter_nested() 时,L2 和 vCPU 的创建都是隐式完成的。

在本文档中,我们将此现有 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() 创建任何关联的 vCPUs()。获取和设置 vCPU 状态也可以使用 h_guest_{g|s}et hcall 来执行。

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,然后使用从 GET 返回的标志的一个子集来调用 SET。此过程允许 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 唯一的 vCPU ID。此 vCPU ID 由 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 相关联的状态(整个客户机范围或特定于 vCPU)。此信息通过客户机状态缓冲区(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 状态。此信息通过客户机状态缓冲区(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(这是为了性能考虑)。这些 GSB 的位置必须使用 ID 为 0x0c00 和 0x0c01 的 H_GUEST_SET_STATE() 调用进行预先注册(见下表)。

输入 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 删除调用。

可以提供一个标志来删除所有客户机。这用于在 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

客户机状态缓冲区

客户机状态缓冲区(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 标准)

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

GSB 标头

偏移字节

大小字节

用途

0

4

元素数量

4

客户机状态缓冲区元素

GSB 元素

偏移字节

大小字节

用途

0

2

ID

2

2

值的大小

4

同上

GSB 元素中的 ID 指定了要设置的内容。这包括架构状态(例如 GPR、VSR、SPRs),以及有关分区的某些元数据,例如时基偏移量和分区级页表信息。

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 分区级表地址

  • 0x08 地址位数

  • 0x10 根目录大小

0x0006

0x10

RW

G

进程表信息

  • 0x0 进程级表地址

  • 0x8 表大小。

0x0007- 0x07FF

保留

0x0800

0x08

R

H

L1-Lpar 的 L0 客户机管理空间当前的字节使用量。

0x0801

0x08

R

H

L1-Lpar 的 L0 客户机管理空间中可用的最大字节数

0x0802

0x08

R

H

L1-Lpar 的 L0 客户机页表管理空间当前的字节使用量

0x0803

0x08

R

H

L1-Lpar 的 L0 客户机页表管理空间中可用的最大字节数

0x0804

0x08

R

H

由于超分从 L0 客户机页表管理空间中累计回收的字节数

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 改变了这一点,使得即使 L2 的 vCPU 不再运行,L0 也会保留 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 状态时,它不需要将其写入 L0,直到该 L2 vCPU 再次运行。因此,当 L1 更新状态(例如通过 kvm_set_one_reg() 调用)时,它写入内部的 L1 副本,并且仅在通过 H_GUEST_VCPU_RUN() 输入缓冲区再次运行 L2 时,才将此副本刷新到 L0。

L1 对状态的这种惰性更新避免了不必要的 H_GUEST_{G|S}ET_STATE() 调用。