第三篇从 RISC-V H-extension 出发,讨论了 HS-mode、VS-mode、virtual supervisor state、
hstatus、hedeleg、hideleg和hgatp。这一篇把视角切到 Linux kernel:当 QEMU 调用KVM_RUN时,Linux KVM/RISC-V 究竟如何把一个普通线程变成 Guest CPU 的执行现场?

1. 本篇要追踪的问题
在 QEMU/KVM 模式下,QEMU 负责创建虚拟机外形,Linux KVM 负责把 guest vCPU 放到硬件上运行。
从 QEMU 视角看,关键动作很简单:
text
open("/dev/kvm")
ioctl(KVM_CREATE_VM)
ioctl(KVM_CREATE_VCPU)
ioctl(KVM_RUN)
但从 Linux kernel 视角看,KVM_RUN 背后要做很多事情:
- 找到对应的 VM 和 vCPU
- 准备 guest register/CSR state
- 配置 RISC-V hypervisor 相关状态
- 配置 stage-2 address translation
- 进入 guest
- 在 guest trap/exit 后回到 host
- 判断 exit 是否能在 kernel 中处理
- 必要时把 exit reason 返回给 QEMU
所以本篇的问题是:
QEMU 的一个
KVM_RUNioctl,如何穿过 Linux KVM,最终变成一次 RISC-V Guest Linux 的执行?
2. 从 /dev/kvm 开始
KVM 暴露给用户态 VMM 的主要入口是 /dev/kvm。
QEMU 打开这个设备文件后,会通过 ioctl 和内核交互。
一个极简模型如下:
text
QEMU process
|
| open /dev/kvm
v
KVM device file
|
| ioctl
v
Linux KVM core
|
| arch hooks
v
RISC-V KVM implementation
这里有两层分工。
KVM core 提供架构无关的通用框架,例如 VM 生命周期、vCPU 生命周期、memory slot、ioctl 分发、eventfd、irqfd 等。
RISC-V KVM 提供架构相关实现,例如 vCPU 寄存器布局、CSR 保存恢复、stage-2 page table、trap/exit 处理、SBI emulation、timer 注入等。
在源码上,可以先建立这个粗略映射:
text
virt/kvm/ KVM common core
arch/riscv/kvm/ RISC-V-specific KVM implementation
include/uapi/linux/kvm.h userspace ABI
3. VM 创建:先有一个容器
KVM_CREATE_VM 创建的是一个虚拟机容器。
这个 VM 还不是 CPU,也不是设备模型。它更像一个承载 guest address space、vCPU、memory slot 和架构状态的对象。
QEMU 创建 VM 后,会继续做几类事情:
- 设置 guest physical memory
- 创建一个或多个 vCPU
- 配置 irqchip 或中断相关能力
- 注册需要 KVM 参与的设备或事件
- 准备和 QEMU device model 的交互路径
可以把 VM 看成这样:
text
KVM VM
|
+-- memory slots
+-- vCPU 0
+-- vCPU 1
+-- vCPU ...
+-- architecture-specific VM state
RISC-V 的架构相关 VM 状态会和 stage-2 page table、中断控制、guest address space 等内容有关。
对源码阅读来说,VM 创建时不必急着看所有字段。先抓住一个主线:
VM 负责承载 guest physical address space,vCPU 负责承载 guest CPU execution state。
4. Memory slot:QEMU 内存如何变成 Guest RAM
第二篇提过,guest RAM 的生命周期和 QEMU 进程紧密相关。
QEMU 在用户态分配或映射一段内存,然后通过 KVM API 把它注册为 guest memory slot。
简化路径如下:
text
QEMU userspace memory
|
| KVM_SET_USER_MEMORY_REGION
v
KVM memory slot
|
| used by stage-2 mapping
v
Guest physical memory range
memory slot 里通常包含几类信息:
- guest physical address 起点
- size
- userspace address
- flags
- slot id
这一步的意义是:KVM 知道某段 guest physical address 应该对应到 QEMU 进程的哪段 host virtual memory。
在 RISC-V 上,当 guest 访问某个 guest physical address 时,KVM 需要通过 stage-2 translation 把它限制在这些合法内存范围内。
因此 memory slot 是 QEMU 和 KVM 之间的内存契约。
5. vCPU 创建:Guest CPU 的内核对象
KVM_CREATE_VCPU 创建一个 vCPU。
从 QEMU 视角看,一个 vCPU 通常对应一个 QEMU vCPU thread。
从 KVM 视角看,一个 vCPU 是一组可保存、可恢复、可运行的 guest CPU 状态。
text
QEMU vCPU thread
|
| owns fd for one KVM vCPU
v
KVM vCPU object
|
+-- general registers
+-- guest CSR state
+-- virtual supervisor state
+-- timer state
+-- interrupt state
+-- run structure shared with userspace
KVM 创建 vCPU 后,还会把一段 struct kvm_run 映射给 QEMU。
这段共享区域很重要:
- QEMU 通过它拿到 exit reason
- QEMU 通过它传递某些运行参数
- KVM 通过它把 MMIO、I/O、shutdown、debug 等信息返回给用户态
所以 KVM_RUN 不是只靠 ioctl 返回值传递状态,struct kvm_run 才是 QEMU 和 KVM 运行循环中的关键共享接口。
6. KVM_RUN 的总体循环
现在可以看最核心的循环。
text
QEMU vCPU thread
|
| ioctl(vcpu_fd, KVM_RUN)
v
KVM common run path
|
| architecture-specific vCPU run
v
RISC-V KVM prepares guest state
|
| enter guest
v
Guest Linux runs
|
| trap / exit
v
RISC-V KVM handles exit
|
+-- resume guest directly
|
+-- return to QEMU with exit reason
这个循环的关键不是"进入一次 guest 就结束",而是反复运行。
QEMU 的 vCPU thread 会不断调用 KVM_RUN。每次返回时,QEMU 检查 exit reason。如果是 MMIO,就调用对应设备模型;如果是 shutdown,就结束虚拟机;如果是可恢复事件,就处理后再次进入 KVM_RUN。
因此一台虚拟机的 CPU 执行,可以理解成:
QEMU 和 KVM 围绕
KVM_RUN进行的一次又一次控制权交接。
7. 进入 Guest 前:KVM 要准备什么
在真正进入 guest 前,KVM 需要把 vCPU context 变成硬件可执行状态。
RISC-V 相关准备大致包括:
- 恢复 guest 通用寄存器
- 恢复 guest PC
- 准备 virtual supervisor CSR
- 配置
hstatus - 配置 trap delegation,例如
hedeleg、hideleg - 配置 stage-2 root,例如
hgatp - 准备 guest timer 和 interrupt pending state
- 确保 host 状态可以在 exit 后恢复
可以画成:
text
KVM vCPU context
|
| load into hardware-visible state
v
RISC-V CPU state for guest entry
|
| enter VS/VU execution
v
Guest Linux continues
这里要注意:KVM 不是每次都完整重建所有状态。真实实现会做大量优化,例如 lazy save/restore、只更新 dirty state、按需刷新 TLB 或 stage-2 映射。
但理解主线时,可以先把它看成一次"恢复 guest 执行现场"。
8. Guest 运行中:哪些事件会导致 exit
Guest Linux 运行时,CPU 可能因为多种原因离开 guest 上下文。
常见类型包括:
- stage-2 page fault
- 访问需要 QEMU 模拟的 MMIO 设备
- SBI call
- guest timer 到期
- virtual interrupt 注入或处理
- guest 访问某些 CSR
- debug 或 single-step
- shutdown/reset
- host signal 或调度需求
这些事件不一定都会返回 QEMU。
KVM 会先判断:
text
exit happens
|
+-- can KVM handle it in kernel?
| |
| +-- yes: handle and resume guest
|
+-- must userspace handle it?
|
+-- return to QEMU with exit reason
这条分支决定了虚拟化性能和设备模型边界。
如果一个高频事件总是返回 QEMU,就会有大量 kernel/userspace 往返。virtio、vhost、中断优化、批处理等机制,很多都是为了降低这条路径的成本。
9. stage-2 fault:KVM 自己能处理的一类 exit
当 guest 访问某个 guest physical address,但 stage-2 page table 中还没有合适映射时,可能触发 stage-2 fault。
这类 fault 通常由 KVM 在内核态处理。
简化流程:
text
Guest memory access
|
| stage-2 translation miss/fault
v
Trap to HS-mode KVM
|
| find matching memory slot
| resolve host page
| install stage-2 PTE
v
Resume guest
这里 memory slot 非常关键。
KVM 需要判断 guest physical address 是否落在 QEMU 注册过的合法内存范围里。如果是,就可以建立或更新 stage-2 mapping。如果不是,就可能说明 guest 访问了 MMIO 区域,或者发生了非法访问。
所以 stage-2 fault 既是内存虚拟化路径,也是隔离边界。
10. MMIO exit:为什么需要回到 QEMU
不是所有 guest physical address 都对应 RAM。
例如 UART、virtio-mmio、PLIC/IMSIC、某些平台设备,都可能通过 MMIO 暴露给 guest。
当 guest 访问一个由 QEMU 设备模型负责的 MMIO 地址时,KVM 自己不知道这个设备的完整语义。
于是路径变成:
text
Guest MMIO load/store
|
v
Trap to KVM
|
| fill kvm_run MMIO exit information
v
Return to QEMU
|
| dispatch to device model
| emulate read/write
v
KVM_RUN again
struct kvm_run 会携带 MMIO 地址、读写方向、访问长度、数据等信息。QEMU 根据这些信息找到对应 MemoryRegion 或设备模型,完成模拟。
这就是为什么 QEMU 在 KVM 模式下仍然很重要。
KVM 负责把 guest 跑起来,但大量设备语义仍然在 QEMU 用户态。
11. SBI exit:架构接口如何被虚拟化
RISC-V guest 执行 SBI call 时,也可能触发 KVM 处理。
SBI call 的位置比较特殊,因为它不是普通设备 MMIO,也不是普通内存 fault,而是 supervisor OS 调用更底层平台服务的接口。
常见 SBI 服务包括:
- timer
- IPI
- remote fence
- hart state management
- system reset
- debug console,取决于实现
简化路径:
text
Guest Linux SBI call
|
v
Trap to KVM
|
+-- emulate in KVM
+-- forward to lower firmware if needed
+-- return to userspace for selected cases
例如 guest 设置 timer,KVM 可能把它转换成 host timer 事件,并在合适时机向 guest 注入 virtual timer interrupt。
读 RISC-V KVM 源码时,SBI 是一个很好的切入口,因为它能串起 trap decode、vCPU state、timer、IPI 和 guest return value。
12. 返回 Guest 前:exit 被处理后如何继续
无论 exit 是 KVM 自己处理,还是 QEMU 处理后再次调用 KVM_RUN,最终都要回到 guest。
这时 KVM 需要保证:
- guest PC 是否需要前进
- guest 寄存器中是否需要写入返回值
- pending interrupt 是否需要更新
- stage-2 mapping 是否已经可用
- MMIO read 的结果是否已经放回 guest
- SBI call 的 error/value 是否已经设置
- guest 是否应该继续运行还是进入 shutdown
例如一次 MMIO read:
text
Guest loads from MMIO address
|
v
KVM exits to QEMU
|
v
QEMU emulates device read
|
v
QEMU writes data into kvm_run
|
v
KVM_RUN resumes guest
|
v
Guest receives load result
这说明 exit handling 不只是"处理一个事件",还要维护 guest 继续执行时的语义一致性。
13. Host 调度与 vCPU 线程
从 host Linux 的角度看,QEMU vCPU thread 仍然是普通线程。
它会被 CFS 或其他调度策略调度,会被抢占,也会受到 CPU affinity、cgroup、NUMA placement 等影响。
这带来一个很现实的结论:
vCPU 是 guest 看到的 CPU,但在 host 上它首先是一个可调度的线程。
因此虚拟机性能不仅取决于 KVM 和硬件扩展,也取决于 host 调度和资源治理。
例如:
- vCPU thread 被频繁迁移,cache locality 可能变差
- vCPU 数量超过 pCPU 数量,guest 内部看到的 CPU 可能被 host 过度复用
- NUMA 放置不合理,guest memory access 可能跨节点
- noisy neighbor 会影响 vCPU 获得真实 CPU 时间
这也是为什么生产环境中常会关注 CPU pinning、isolcpus、hugepage、NUMA binding 等配置。
14. 读源码时的一条主线
读 Linux KVM/RISC-V 源码时,建议围绕一条主线走:
一个 vCPU 从 QEMU 调用
KVM_RUN开始,如何进入 guest,又如何因为 exit 回到 QEMU 或继续运行?
可以按这个顺序看:
text
userspace ABI
|
v
KVM common ioctl path
|
v
RISC-V vCPU run implementation
|
v
guest entry assembly / low-level path
|
v
trap/exit handling
|
v
return to guest or userspace
对应源码入口:
include/uapi/linux/kvm.hvirt/kvm/kvm_main.carch/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.h
不要一开始就试图把所有 field 都记住。
更有效的方式是抓住几个问题:
struct kvm_vcpu里哪些字段是 RISC-V 特有的?- guest register/CSR state 在哪里保存?
KVM_RUN前后哪些状态会改变?- exit reason 是在哪里被设置的?
- 哪些 exit 会直接 resume guest?
- 哪些 exit 会写入
struct kvm_run返回 QEMU?
这些问题能帮助你把分散的代码连成运行路径。
15. 一个最小心智模型
到这里,可以把 KVM_RUN 压缩成一个更小的模型。
text
QEMU says: run this vCPU
|
v
KVM loads: guest CPU state + stage-2 state + interrupt state
|
v
RISC-V hardware runs: VS/VU guest execution
|
v
Something exits
|
+-- KVM handles and resumes
|
+-- QEMU handles and calls KVM_RUN again
这个模型不覆盖所有细节,但足够支撑后续继续深入。
后面无论看 MMU、interrupt、timer、virtio、SBI,基本都可以问同一个问题:
这个事件是在 guest 内部完成,KVM 内核态完成,还是必须回到 QEMU 用户态完成?
这就是 QEMU/KVM 虚拟化路径里最重要的分界线之一。
16. 本篇小结
这一篇从 Linux KVM/RISC-V 视角看了 vCPU 运行路径。
第一,/dev/kvm 是 QEMU 进入 KVM 的主要 ABI 入口。
QEMU 通过 ioctl 创建 VM、注册 memory slot、创建 vCPU,并通过 KVM_RUN 请求内核运行 guest。
第二,VM 和 vCPU 分别承载不同语义。
VM 更像 guest address space 和资源容器;vCPU 则承载 guest CPU execution state。
第三,KVM_RUN 是 QEMU 和 KVM 的控制权交接点。
进入 guest 前,KVM 恢复 guest 状态并配置 RISC-V hypervisor 相关状态;exit 后,KVM 判断事件是在内核态处理,还是返回 QEMU。
第四,exit 的归属决定系统边界。
stage-2 fault 这类事件通常可以由 KVM 处理;MMIO 设备访问常常需要回到 QEMU;SBI call 则可能根据 extension 和实现走不同路径。
可以把本篇压缩成一句话:
KVM_RUN让 QEMU vCPU thread 暂时成为 Guest CPU 的载体,而 Linux KVM/RISC-V 负责在进入和退出之间维护这个虚拟 CPU 世界的真实性与边界。
17. 下一篇预告:RISC-V KVM 的内存虚拟化与 stage-2 page table
下一篇可以深入内存路径,重点看 guest physical address 如何被限制到 host memory slot 中。
会讨论:
- QEMU 如何通过
KVM_SET_USER_MEMORY_REGION注册 guest RAM - KVM memory slot 如何组织 guest physical address space
- RISC-V stage-2 page table 的角色
hgatp如何指向 second-stage translation root- stage-2 fault 如何被处理
- dirty logging 和 write protection 为什么对迁移重要
- hugepage、TLB flush、内存性能和隔离之间的关系