第二篇从 QEMU 启动路径出发,看了一台 RISC-V 虚拟机如何被 QEMU 拼出来、如何通过 device tree 交给 Guest Linux、以及 TCG 和 KVM 在 CPU 执行路径上的差异。第三篇进入架构层:RISC-V Hypervisor extension,简称 H-extension,究竟怎样让 Host Linux 管理 Guest Linux?
1. 本篇要回答的问题
虚拟化最核心的问题之一是权限。
Guest Linux 需要像普通内核一样运行:
- 管理自己的页表
- 处理中断和异常
- 调度自己的进程
- 访问 supervisor CSR
- 执行
sret等特权指令
但它又不能真的拥有整台机器的最高控制权。
所以问题变成:
当 Guest Linux 以为自己运行在 S-mode 时,RISC-V 硬件和 Host Linux 究竟为它制造了怎样的“虚拟 supervisor 世界”?
RISC-V H-extension 的目标,就是让 guest kernel 尽量保持普通 supervisor kernel 的运行模型,同时让 host kernel 可以截获、隔离和管理它。
2. 没有虚拟化时的 RISC-V 执行模式
先回到没有 H-extension 的普通 RISC-V 系统。
常见执行模式可以简化成三层:
+-----------------------------+ | U-mode | | user applications | +-----------------------------+ ^ | v +-----------------------------+ | S-mode | | supervisor OS kernel | +-----------------------------+ ^ | v +-----------------------------+ | M-mode | | machine firmware / SBI | +-----------------------------+每一层大致承担不同职责。
M-mode 是最高权限,通常运行 firmware 或 SBI implementation,例如 OpenSBI。它可以访问 machine-level CSR,管理最底层的 trap、timer、IPI、hart 启停等能力。
S-mode 通常运行操作系统内核,例如 Linux。它管理用户进程、虚拟内存、中断和设备驱动,但在需要更底层平台服务时,会通过 SBI 调用 M-mode。
U-mode 运行普通用户态程序。它不能直接执行 supervisor 或 machine 级别的特权操作。
在这个模型里,Linux kernel 运行在 S-mode,它看到的 supervisor 状态是真实 supervisor 状态。
3. 加入 H-extension 后的新模式
引入 H-extension 后,RISC-V 增加了面向虚拟化的执行语义。
最重要的是多出两类 guest 视角的模式:
+-----------------------------+ | VU-mode | | guest user applications | +-----------------------------+ ^ | v +-----------------------------+ | VS-mode | | guest supervisor kernel | +-----------------------------+ ^ | v +-----------------------------+ | HS-mode | | host supervisor kernel | +-----------------------------+ ^ | v +-----------------------------+ | M-mode | | machine firmware / SBI | +-----------------------------+这里有两个关键概念。
第一,HS-mode 不是一个全新的独立 privilege mode,而是启用 hypervisor 能力后的 host supervisor 运行环境。Host Linux kernel 通常运行在 HS-mode。
第二,VS-mode 是 guest supervisor mode。Guest Linux kernel 以为自己运行在 S-mode,但硬件会把它放在 virtual supervisor 上下文中。
这句话很重要:
Guest Linux 看到的是 supervisor 世界,Host Linux 看到的是被虚拟化的 supervisor 世界。
因此 guest kernel 可以继续执行很多原本属于 supervisor kernel 的逻辑,而 host kernel 仍能在关键边界上拥有最终控制权。
4. Host 与 Guest 的状态分离
虚拟化不能只靠“多一个模式”。真正麻烦的是状态。
普通 Linux kernel 会使用很多 supervisor 状态,例如:
sstatussiestvecsscratchsepcscausestvalsatp
如果 host kernel 和 guest kernel 共用同一组状态,就会互相踩踏。
H-extension 的做法是引入 virtual supervisor state。
Guest Linux 访问的很多 supervisor CSR,在虚拟化上下文里会对应到 VS 版本,例如:
vsstatusvsievstvecvsscratchvsepcvscausevstvalvsatp
这样一来,Host Linux 可以保留自己的 supervisor 状态,Guest Linux 则拥有一套看起来像 supervisor 的虚拟状态。
可以画成这样:
Host Linux in HS-mode uses: sstatus, sie, stvec, satp, ... Guest Linux in VS-mode uses: vsstatus, vsie, vstvec, vsatp, ...从 guest kernel 的角度,它仍然像普通 Linux 一样设置 trap vector、打开中断、切换页表。
从 host/KVM 的角度,这些状态属于某个 vCPU context,需要在 vCPU 运行前恢复,在 vCPU 退出后保存。
5. vCPU context:Guest CPU 的软件影子
在 KVM 里,一个虚拟 CPU 不只是一个线程。
它至少需要保存:
- 通用寄存器
- guest supervisor CSR
- virtual supervisor CSR
- guest timer 状态
- guest interrupt 状态
- floating point/vector 状态
- 当前 guest PC
- trap/exit 相关信息
可以把 vCPU context 理解成 Guest CPU 的软件影子。
QEMU vCPU thread | | ioctl(KVM_RUN) v Linux KVM vCPU context | | restore guest state v RISC-V CPU runs Guest Linux | | trap / exit v Linux KVM saves guest state当 QEMU 调用KVM_RUN时,KVM 会把 vCPU context 装载到硬件可见的状态里,然后进入 guest。
当 guest 发生 exit 时,KVM 再把相关状态保存回来,并决定是在内核里处理,还是返回 QEMU。
6.hstatus:控制虚拟化执行环境
hstatus是 H-extension 中非常关键的 CSR 之一。
它保存和 hypervisor 执行状态有关的信息,用来控制或反映 guest 运行时的一些行为。
具体 bit 很多,第一篇不需要死记。先建立几个方向:
- 当前是否处在 virtualized execution 相关上下文中
- guest 访问某些状态时如何解释
- trap 返回时要回到什么虚拟化状态
- guest 使用的 XLEN 等模式信息
对读代码来说,更重要的不是背下每个 bit,而是知道hstatus属于“host 用来控制 guest 执行环境”的那组寄存器。
后续读 Linux KVM/RISC-V 时,看到 KVM 在 vCPU run 前后设置或保存hstatus,要把它理解成:
Host 正在为 Guest Linux 准备或恢复一个可以运行的 virtual supervisor 世界。
7.hedeleg和hideleg:哪些 trap 交给 Guest 自己处理
虚拟化并不意味着所有 trap 都要回到 host。
如果 Guest Linux 自己能够处理某些异常或中断,最好让它直接处理,否则每次都陷入 host 会非常昂贵。
RISC-V 使用 delegation 机制控制 trap 的流向。
在 hypervisor 场景里,两个关键 CSR 是:
hedeleg:hypervisor exception delegationhideleg:hypervisor interrupt delegation
它们的作用是决定哪些异常或中断可以交给 VS-mode 的 guest supervisor 处理。
简化理解:
trap happens while guest is running | +--> delegated to VS-mode Guest Linux | +--> trapped to HS-mode Host Linux/KVM如果一个 trap 被委派给 guest,Guest Linux 会像普通 kernel 一样进入自己的 trap handler。
如果没有被委派,它会陷入 HS-mode,由 Host Linux/KVM 判断该如何处理。
这种机制的目标是减少不必要的 host 介入,同时保留隔离边界。
8.hgatp:第二阶段地址翻译的入口
内存隔离离不开地址翻译。
在虚拟化场景中,Guest Linux 管理自己的页表,但它管理的是 guest physical address,不是真正的 host physical address。
完整路径是:
Guest virtual address | | stage 1: controlled by Guest Linux, via vsatp v Guest physical address | | stage 2: controlled by Host Linux/KVM, via hgatp v Host physical address这里vsatp和hgatp的分工很清楚。
vsatp属于 guest 的第一阶段地址翻译。Guest Linux 以为自己像普通内核一样切换页表。
hgatp属于 host 管理的第二阶段地址翻译。Host Linux/KVM 用它限制 guest physical address 能映射到哪里。
这带来几个关键性质:
- Guest 可以正常管理自己的用户进程地址空间
- Host 可以把 guest 内存限制在指定 memory slot 中
- Guest 不能直接访问 host kernel 或其他 VM 的内存
- KVM 可以通过 stage-2 fault 进行缺页处理、dirty logging、write protection
所以hgatp可以看成 RISC-V 虚拟化内存隔离的核心入口。
9. Guest trap 的几种走向
当 Guest Linux 正在运行时,可能发生很多类型的事件。
例如:
- guest 用户态程序触发 page fault
- guest kernel 访问一个不存在的 MMIO 地址
- guest timer 到期
- guest 执行 SBI call
- guest 访问某个需要 host 介入的 CSR
- stage-2 page fault
- 外部中断到达
这些事件不一定走同一条路径。
可以粗略分成三类。
9.1 Guest 自己处理
例如 guest 用户态进程的普通 page fault。
这类异常属于 guest OS 的正常职责,理想情况下可以直接交给 Guest Linux 处理。
Guest user page fault | v Guest Linux page fault handler9.2 KVM 在内核态处理
例如某些虚拟 timer、部分 CSR、stage-2 fault。
这类事件需要 host 介入,但不一定需要返回 QEMU。
Guest trap | v Host Linux KVM | | update vCPU state / page table / timer v Resume guest9.3 返回 QEMU 用户态处理
例如访问 QEMU 负责模拟的 MMIO 设备。
Guest MMIO access | v KVM exit to userspace | v QEMU device model handles access | v KVM_RUN again理解这三类走向非常重要。
因为虚拟化性能不是只取决于“有没有硬件辅助”,还取决于 exit 频率、exit 去哪里处理、能不能减少往返用户态的次数。
10. SBI 在 Guest 虚拟化中的位置
RISC-V 系统里,SBI 是 supervisor binary interface。
普通 Linux 通过 SBI 请求底层 firmware 提供某些服务。
在 guest 场景里,Guest Linux 也可能执行 SBI call,例如:
- 设置 timer
- 发送 IPI
- console putchar,取决于环境
- hart state management
- system reset
问题是:Guest Linux 看到的 SBI 服务未必直接来自真实 M-mode firmware。
可能的路径包括:
Guest Linux SBI call | +--> handled/emulated by KVM | +--> forwarded to host SBI / firmware | +--> returned to QEMU for userspace handling具体走哪条路径取决于 SBI extension、KVM 实现和平台能力。
所以读 RISC-V KVM 时,SBI call 是一个很好的观察点:它能同时牵出 guest trap、KVM emulation、timer/IPI、以及 host firmware 的边界。
11. H-extension 与 QEMU/KVM 的分工
现在可以把三层关系重新压缩一下。
QEMU - creates VM and vCPU through /dev/kvm - builds machine model and devices - handles userspace exits Linux KVM/RISC-V - owns vCPU context - configures hstatus/hedeleg/hideleg/hgatp - enters and exits guest - handles traps that can stay in kernel RISC-V H-extension - provides VS/VU execution semantics - provides virtual supervisor state - provides two-stage address translation - provides trap routing controlsQEMU 不需要自己模拟所有 privileged behavior。
Linux KVM 也不需要自己实现完整设备模型。
RISC-V 硬件也不需要知道某个 virtio block 背后是哪个 host 文件。
这三者的边界清楚,虚拟化系统才容易扩展。
12. 读 Linux KVM/RISC-V 代码时可以带着哪些问题
进入源码前,建议先带着问题读,而不是按文件顺序硬啃。
几个适合作为入口的问题:
- vCPU 创建时,KVM 初始化了哪些 guest CSR?
KVM_RUN前,哪些状态会被写入硬件 CSR?- guest exit 后,KVM 如何判断 exit reason?
- 哪些 trap 会被 KVM 直接处理?
- 哪些 trap 会返回 QEMU?
- stage-2 page table 在哪里创建和更新?
- timer 和 IPI 是如何注入给 guest 的?
- SBI call 是在哪里被解析和处理的?
对应源码入口可以先看:
arch/riscv/kvm/vcpu.carch/riscv/kvm/vcpu_exit.carch/riscv/kvm/vcpu_sbi.carch/riscv/kvm/vcpu_timer.carch/riscv/kvm/mmu.carch/riscv/include/asm/kvm_host.hvirt/kvm/kvm_main.c
这些文件的具体函数路径留到后续文章展开。
13. 本篇小结
这一篇的重点是把 RISC-V H-extension 的作用放进 QEMU/KVM 的整体路径里。
第一,Guest Linux 并不是直接运行在真实 S-mode 中。
它运行在 VS-mode 中,看到的是一套 virtual supervisor state,例如vsstatus、vstvec、vsepc、vsatp。
第二,Host Linux/KVM 运行在 HS-mode 中。
它负责保存和恢复 vCPU context,配置hstatus、hedeleg、hideleg、hgatp,并决定 guest trap 的处理路径。
第三,内存隔离依赖 two-stage translation。
Guest Linux 通过vsatp管理自己的第一阶段页表,Host Linux/KVM 通过hgatp管理第二阶段页表。
第四,不同 trap 有不同归宿。
有些交给 Guest Linux 自己处理,有些由 KVM 在内核态处理,有些返回 QEMU 用户态设备模型。
可以把本篇压缩成一句话:
RISC-V H-extension 为 Guest Linux 制造了一个可以相信的 supervisor 世界,同时让 Host Linux/KVM 在这个世界之外保留最终控制权。
14. 下一篇预告:Linux KVM/RISC-V 的 vCPU 运行路径
下一篇可以从 Linux kernel 源码视角进入KVM_RUN。
会讨论:
- QEMU 如何通过 ioctl 进入 KVM
- KVM 如何创建 VM 和 vCPU
- RISC-V vCPU context 在 kernel 中如何表示
KVM_RUN前后 guest state 如何保存和恢复- guest exit reason 如何被分类
- 哪些 exit 留在 kernel,哪些返回 QEMU
- 读
arch/riscv/kvm/vcpu.c和vcpu_exit.c时应该抓哪条主线