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 vCPUL1 和 L0 使用 H_GUEST_{G,S}
ET()hcall 传输 vCPU 状态L1 请求 L0 通过运行
H_GUEST_VCPU_RUN()hcall 来运行 vCPUL1 使用
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 |
分区级页表信息
|
0x0006 |
0x10 |
RW |
G |
进程表信息
|
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 输入缓冲区
|
0x0C01 |
0x10 |
RW |
T |
运行 vCPU 输出缓冲区
|
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() 调用。