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 |
分区范围页表信息
|
0x0006 |
0x10 |
RW |
G |
进程表信息
|
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 输入缓冲区
|
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 更改了此功能,以便 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() 调用。