传统的非 VHE 模式
HostOS 和 Guest OS 运行在 EL1 级别,HostOS 上的 Qemu 运行在 EL0 级别,KVM 运行在 EL2 级别。
一个 VM 对应 HostOS 上一个 QEMU 进程;一个 vCPU 对应 HostOS 上一个 QEMU 线程。
当 CPU 上运行 Host Context 时,时钟中断直接进入 HostOS;当 CPU 上运行 Guest Context 时,时钟中断直接进入 KVM。
时钟中断进入 KVM 时,KVM 保存 Guest Context, 设置 Host Context(此时通过设置 HCR_EL2.IMO 改变了后续的时钟中断陷入行为),后 ERET 进入 HostOS. HostOS 继续处理此时钟中断,调用 scheduler 决定之后运行哪个线程。
关于调用栈的冻结状态
cpp
// QEMU 代码 (User Space)
// ... 准备工作 ...
int ret = ioctl(vcpu_fd, KVM_RUN, 0); // <--- QEMU 线程停在这里!
// ... 此时 QEMU 陷入内核态,等待内核返回 ...
// ... 只有当 VM 发生需要 QEMU 处理的事件(如 IO)时,才会运行到下一行 ...
if (ret < 0) handle_error();
switch (run->exit_reason) { ... }
当 CPU 上要从 HostOS 切换到 GuestOS 时
- QEMU 执行 ioctl(vcpu_fd, KVM_RUN, 0); 这是一个系统调用
- 进入 HostOS 后,HostOS 执行 kvm_* 一系列工作,最终执行到 kvm_call_hyp. 这是一个 hypervisor call, HostOS 中断在这里,后进入 EL2
- EL2 的 KVM 保存 Host 上下文,加载 Guest 上下文,ERET 后进入 EL1 执行 GuestOS
- 发生 Exit 事件(比如 Host 时钟中断),Guest -> KVM
- KVM 保存 Guest ctx 加载 Host ctx, ERET
- HostOS 认为 hvc 执行完毕
- HostOS 检查 hvc 返回的原因
- 时钟中断但当前线程时间片没用完,他会继续调用 hvc, 这个过程完全不涉及 QEMU, QEMU 继续冻结在 ioctl 那里
- 时间片用完了,HostOS 调用 schedule, QEMU 线程被挂起,CPU 运行其他线程
- Guest 发起 IO 请求:HostOS 从系统调用返回 QEMU, QEMU 去处理 IO
- 在 7.3 的情况下,QEMU 进程从 ioctl 返回,执行后续的代码
从上面这个流程可以看出,KVM 其实应该分为两个部分,一个是以内核模块形式运行在 EL1 的部分,另一个是运行在 EL2 的微型 Hypervisor 代码。
EL1 的部分是虚拟化逻辑的核心,他处理 IO、内存管理、指令模拟、中断注入。它通过 HVC 指令驱动 EL2。EL2 的部分仅是利用位于 EL2 的特权,按 EL1 部分的指示切换硬件。
EL1 部分的 KVM 都做了什么,是怎么利用 Linux 的
1. GuestOS 的内存(IPA)是谁分配的
- VM 启动时,QEMU 进程会在用户态调用 mmap,获取一段 HVA
- QEMU 会通过 ioctl 告诉 KVM 模块这段 HVA, 且告诉 KVM 在 GuestOS 的视角下,这段 HVA 应该对应哪段 GPA
- 当 GuestOS 试图访问 GPA 时,此时 stage2 页表空空,触发缺页异常,陷入 KVM
- KVM 内核模块拿着 GPA 查对应的 HVA, 用 get_user_pages() 查 HPA(可能 hostos 此时才真正为 HVA 分配 HPA)
- 查到 HPA 后,建立 GPA -> HPA 的 stage-2 映射
- KVM 模块返回 Guest
2. GuestOS 的栈地址是怎么指定的
栈空间是内存的一部分,但最初的 Guest ctx 中的 sp 指针指向哪里?这个问题还比较复杂。
非 VHE 模式的缺陷
通过上面的描述不难看出,如果在执行 GuestOS 时发生时钟中断或缺页等异常,就会连续触发四次特权级切换。
- GuestOS -> KVM EL2
- KVM EL2 -> HostOS KVM EL1
- HostOS KVM EL1 -> KVM EL2
- KVM EL2 -> GuestOS
为切换 Host Guest Context, KVM EL2 就必不可少,为利用 Linux 的功能实现 hypervisor, KVM EL1 kernel module 就必不可少,所以上述四次特权级切换就无法避免,这样性能开销就很大,所以才引入了 VHE 模式。