POWER 上的嵌套 KVM¶
简介¶
本文档解释了如果虚拟机监控程序实现了超级调用,则来宾操作系统如何充当虚拟机监控程序并通过超级调用运行嵌套来宾。术语 L0、L1 和 L2 用于指代不同的软件实体。L0 是通常称为“主机”或“虚拟机监控程序”的虚拟机监控程序模式实体。L1 是直接在 L0 下运行的来宾虚拟机,由 L0 启动和控制。L2 是由充当虚拟机监控程序的 L1 启动和控制的来宾虚拟机。
现有 API¶
自 2018 年以来,Linux/KVM 一直支持作为 L0 或 L1 的嵌套
添加了 L0 代码
commit 8e3f5fc1045dc49fd175b978c5457f5f51e7a2ce
Author: Paul Mackerras <[email protected]>
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 <[email protected]>
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 运行时,L2 vCPU 的完整状态始终从 L1 传输到 L1 和从 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、运行 L2 并删除 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 嵌套虚拟机监控程序的功能。这包括作为 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 虚拟机监控程序的功能。此处传递的标志集与 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),可以在后续 HCALL 中使用该 ID 来标识 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 关联的状态(来宾范围或 vCPU 特定)。此信息通过来宾状态缓冲区 (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: takeOwnershipOfVcpuState Indicate the L1 is taking
over ownership of the VCPU state and that the L0 can free
the storage holding the state. The VCPU state will need to
be returned to the Hypervisor via H_GUEST_SET_STATE prior
to H_GUEST_RUN_VCPU being called for this VCPU. The data
returned in the dataBuffer is in a Hypervisor internal
format.
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),则在输出 GSB 中提供 GPR3-12,因为这是服务 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 状态缓冲区 (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 |
线程 Guest 范围 |
详细信息 |
---|---|---|---|---|
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 - 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 状态时,它不需要将其写入 L0,直到该 L2 vcpu 再次运行时。因此,当 L1 更新状态(例如通过 kvm_set_one_reg() 调用)时,它会写入内部 L1 副本,并且仅当 L2 通过 H_GUEST_VCPU_RUN() 输入缓冲区再次运行时才将此副本刷新到 L0。
L1 的这种延迟更新状态避免了不必要的 H_GUEST_{G|S}ET_STATE() 调用。