[Virtualization](四):Linux KVM/RISC-V 的 vCPU 运行路径

第三篇从 RISC-V H-extension 出发,讨论了 HS-mode、VS-mode、virtual supervisor state、hstatushedeleghideleghgatp。这一篇把视角切到 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_RUN ioctl,如何穿过 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,例如 hedeleghideleg
  • 配置 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.h
  • virt/kvm/kvm_main.c
  • arch/riscv/kvm/vcpu.c
  • arch/riscv/kvm/vcpu_exit.c
  • arch/riscv/kvm/vcpu_sbi.c
  • arch/riscv/kvm/vcpu_timer.c
  • arch/riscv/kvm/mmu.c
  • arch/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、内存性能和隔离之间的关系
相关推荐
城管不管2 小时前
ReAct、Plan-and-Execute、Reflection 三大智能 Agent 范式核心区别
java·人工智能·算法·spring·ai·动态规划
似的8352 小时前
一步一步学习使用FireMonkey动画() 使用TAnimator类创建动画
linux·学习·nginx
IT小白杨3 小时前
从环境制备到自动化工作流:多账号运营的工程化架构拆解
java·经验分享·自动化·安全架构·指纹浏览器
豆瓣鸡3 小时前
算法日记 - Day3
java·开发语言·算法
萧瑟余晖3 小时前
Java深入解析篇九之NIO详解
java·网络·nio
The Chosen One9854 小时前
高进度算法模板速记(待完善)
java·前端·算法
我不管我就要叫小猪4 小时前
嵌入式Linux----网络通信
linux·运维·服务器
泡沫冰@4 小时前
ECS 的介绍和使用
linux·服务器·网络
姜太小白5 小时前
【Linux】df -h 卡住问题的通用排查与解决方案总结
linux·运维·php