diff --git a/articles/20220802-kvm-user-app.md b/articles/20220802-kvm-user-app.md new file mode 100644 index 0000000000000000000000000000000000000000..9c4805b10cb4e5be2dea4d33b0f520a250e43c51 --- /dev/null +++ b/articles/20220802-kvm-user-app.md @@ -0,0 +1,769 @@ +> Author: 潘夏凯 <13212017962@163.com>
+> Date: 2022/08/02
+> Revisor: Falcon, taotieren, Bin Meng, tjytimi, walimis
+> Project: [RISC-V Linux 内核剖析](https://gitee.com/tinylab/riscv-linux)
+> Proposal: [RISC-V 虚拟化技术调研与分析](https://gitee.com/tinylab/riscv-linux/issues/I5E4VB) + +# KVM 虚拟化:用户态程序 + +## 概览 + +本文以 kvm-hello-world, kvmtool 和 QEMU 为例,分析了基于 KVM API 的虚拟化实现中所用到的用户态程序的结构。包括虚拟机、vCPU 的创建、初始化和运行。 + +## 软件版本 + +| 软件 | 提交 ID 或版本号 | 仓库链接 | +| --------------- | ---------------------------------------- | -------------------------------------- | +| Linux Kernel | 5.19-rc5 | https://www.kernel.org/ | +| kvm-hello-world | e9ab0f26e892fa3794f2f991144be7fa45ddd082 | https://github.com/dpw/kvm-hello-world | +| kvmtool | 6a1f699108e5c2a280d7cd1f1ae4816b8250a29f | https://github.com/kvmtool/kvmtool | +| QEMU | a74c66b1b933b37248dd4a3f70a14f779f8825ba | https://www.qemu.org/ | + +## KVM 虚拟化术语 + +| 层次 | 名称 | 功能 | +| ----------------- | ----------------------------------------------- | ------------------------------------- | +| Virtualized Layer | Guest Applications | 运行在 Guest 中的应用程序 | +| Virtualized Layer | VM (Virtual Machine) | 虚拟机 | +| User Layer | User Application | 调用 KVM API 创建 VM | +| Kernel Layer | Include KVM module in Linux Kernel (hypervisor) | 用于支持管理的功能抽象 | +| Hardware Layer | Host Machine | 由 CPU, Memory, Disk 等硬件构成的系统 | + +## KVM 简介 + +RedHat 网站的一篇 [博文][1] 对 KVM 做了简要介绍,现整理摘录如下: + +- Hypervisor 应提供什么功能? + + 作为运行在宿主机的 Hypervisor,它应该提供一些操作系统级组件,例如内存管理器,进程调度进程,输入/输出(I / O)堆栈,设备驱动进程,安全管理器,网络堆栈等,以运行虚拟机。 + +- KVM 如何工作? + + KVM 是内置于 Linux 内核中的模块,它重用了内核中描述的上述组件,并为 VM 提供专用的虚拟硬件如网卡、GPU、CPU、内存和磁盘。每个 VM 都作为常规 Linux 进程实现,它由虚拟机管理进程中的标准 Linux 调度器进行调度。 + +## kvm-hello-world + +kvm-hello-world 是一个使用 KVM API 管理 VM 的精简示例。我们将从此应用开始,演示如何使用 KVM 创建虚拟机的简单方法。该示例基于 x86 架构的 CPU,x86 架构的虚拟化功能有两种主流实现,即英特尔的 VT-x 或 AMD 的 AMD-V。 + +该示例的主体就是代码文件 `kvm-hello-world.c`。 + +### VM (虚拟机) 和 vCPU (虚拟处理器) 的创建 + +下表对比了 VM 和 vCPU 的创建过程: + +| 项目 | VM(虚拟机)的创建 | vCPU 的创建 | +| ---------------------------- | -------------------------- | --------------------------- | +| 初始化函数 | vm_init | vcpu_init | +| 挂载 /dev/kvm 设备 | open(/dev/kvm) | +| `ioctl()` 检查 KVM 软件版本 | KVM_GET_API_VERSION | +| `ioctl()` 创建 VM 或 vCPU | KVM_CREATE_VM | KVM_CREATE_VCPU | +| `mmap()` 申请虚拟机内存 | vm->mem, mem_size | vm->kvm_run, vcpu_mmap_size | +| 额外设置 | `madvise` | +| | `memreg` initiation | +| `ioctl()` 初始化申请到的内存 | KVM_SET_USER_MEMORY_REGION | + +接下来,对照代码进行详细分析: + +```C +// +// kvm-hello-world/kvm-hello-world.c: line 66 +// + +struct vm +{ + int sys_fd; + int fd; + char *mem; +}; + +void vm_init(struct vm *vm, size_t mem_size) +{ + int api_ver; + struct kvm_userspace_memory_region memreg; + + /* 打开设备 /dev/kvm,检查KVM版本 */ + /* Open /dev/kvm and checks the version. */ + vm->sys_fd = open("/dev/kvm", O_RDWR); + // ... + api_ver = ioctl(vm->sys_fd, KVM_GET_API_VERSION, 0); + // ... + + /* 调用 KVM_CREATE_VM 创建虚拟机 */ + /* Makes a KVM_CREATE_VM call to creates a VM. */ + vm->fd = ioctl(vm->sys_fd, KVM_CREATE_VM, 0); // ... + + /* 调用 mmap() 函数为虚拟机申请内存 */ + /* Uses mmap to allocate some memory for the VM. */ + vm->mem = mmap(NULL, mem_size, ..., -1, 0); // ... + + /* 调用 KVM_SET_USER_MEMORY_REGION 初始化申请到的内存区域 */ + /* Make a KVM_SET_USER_MEMORY_REGION call to set memory */ + if (ioctl(vm->fd, KVM_SET_USER_MEMORY_REGION, &memreg) < 0) + /* ... */ +} +``` + +`vcpu_init()` 创建 vCPU: + +```C +// +// kvm-hello-world/kvm-hello-world.c: line 134 +// + +struct vcpu +{ + int fd; + struct kvm_run *kvm_run; +}; + +void vcpu_init(struct vm *vm, struct vcpu *vcpu) +{ + int vcpu_mmap_size; + + /* 调用 KVM_CREATE_VCPU 为已创建的虚拟机创建虚拟CPU,调用 mmap() 函数将VCPU映射到指定内存区域 */ + /* Makes a KVM_CREATE_VCPU call to creates a VCPU within the VM, and mmap its control area. */ + vcpu->fd = ioctl(vm->fd, KVM_CREATE_VCPU, 0); + /* ... */ + + vcpu_mmap_size = ioctl(vm->sys_fd, KVM_GET_VCPU_MMAP_SIZE, 0); + if (vcpu_mmap_size <= 0) + { + perror("KVM_GET_VCPU_MMAP_SIZE"); + exit(1); + } + + vcpu->kvm_run = mmap(NULL, vcpu_mmap_size, PROT_READ | PROT_WRITE, + MAP_SHARED, vcpu->fd, 0); + if (vcpu->kvm_run == MAP_FAILED) + { + perror("mmap kvm_run"); + exit(1); + } +} +``` + +### VM 运行前准备 + +在创建 VM 和 vCPU 并为它们分配内存后,在正式运行 VM 之前,还需要做一些准备工作。这些准备工作在函数 `run_xxx_mode()` 中进行,在 `run_kvm()` 被调用之前完成,细节如下表所示: + +| 项目 | 运行实模式 | 运行其他模式 | +| ----------- | ---------------------------------------------------- | ---------------- | +| definition | `sregs`, `regs` | | +| test sregs | KVM_GET_SREGS | +| setup sregs | `sregs.cs.selector = 0; sregs.cs.base = 0;` | `setup_xxx_mode` | +| set sregs | KVM_SET_SREGS | +| setup regs | KVM_SET_REGS | +| set regs | `memcpy(vm->mem, guestX, guestX_end - guestX);` X=16 | X=32, 64, 64 | +| return | run_kvm: `ioctl(vcpu->fd, KVM_RUN, 0)` | + +总体来看,准备工作可以分为三个步骤:`KVM_GET_SREGS` 测试,`sregs` 的初始化与传入 VM,`regs` 的初始化与传入 VM。 + +下方代码是 x86 准备运行保护模式的代码实现。 + +```C +// +// kvm-hello-world/kvm-hello-world.c: line 228 +// + +extern const unsigned char guest32[], guest32_end[]; + +int run_protected_mode(struct vm *vm, struct vcpu *vcpu) +{ + struct kvm_sregs sregs; + struct kvm_regs regs; + + // 测试是否能成功获取 VM 的非通用寄存器 + // test sregs + printf("Testing protected mode\n"); + if (ioctl(vcpu->fd, KVM_GET_SREGS, &sregs) < 0) { + // ... + } + // 初始化非通用寄存器 + // setup & set sregs + setup_protected_mode(&sregs); + if (ioctl(vcpu->fd, KVM_SET_SREGS, &sregs) < 0) { + // ... + } + + // 初始化通用寄存器 + // setup & set regs + memset(®s, 0, sizeof(regs)); + regs.rflags = 2; + regs.rip = 0; + if (ioctl(vcpu->fd, KVM_SET_REGS, ®s) < 0) { + // ... + } + + // 初始化 VM 的内存区域 + memcpy(vm->mem, guest32, guest32_end - guest32); + + return run_vm(vm, vcpu, 4); +} + +``` + +如下代码是保护模式下 `sregs` 初始化函数 `setup_protected_mode` 的实现, `sregs` 和 `regs` 在 `/usr/include/x86_64-linux-gnu/asm/kvm.h` 中定义。`kvm_sregs` 用于表示不同架构 vCPU 的特殊寄存器。 + +```C +// +// kvm-hello-world/kvm-hello-world.c: line 247 +// + +/* 特定模式(此处为x86保护模式)下非通用寄存器的赋值 */ +/* mode setup: assign values to sregs */ +static void setup_protected_mode(struct kvm_sregs *sregs) +{ + struct kvm_segment seg = { + .base = 0, + .limit = 0xffffffff, + .selector = 1 << 3, + .present = 1, + .type = 11, /* Code: execute, read, accessed */ + .dpl = 0, + .db = 1, + .s = 1, /* Code/data */ + .l = 0, + .g = 1, /* 4KB granularity */ + }; + + sregs->cr0 |= CR0_PE; /* enter protected mode */ + + sregs->cs = seg; + + seg.type = 3; /* Data: read/write, accessed */ + seg.selector = 2 << 3; + sregs->ds = sregs->es = sregs->fs = sregs->gs = sregs->ss = seg; +} +``` + +在 x86 架构中, `kvm_regs` 定义如下,它们作为 vCPU 的通用寄存器,将在后续运行中用来保存 vCPU 的状态。 + +```C +// +// /usr/include/x86_64-linux-gnu/asm/kvm.h: line 148 +// + +/* for KVM_GET_SREGS and KVM_SET_SREGS */ +struct kvm_sregs { + /* out (KVM_GET_SREGS) / in (KVM_SET_SREGS) */ + struct kvm_segment cs, ds, es, fs, gs, ss; + struct kvm_segment tr, ldt; + struct kvm_dtable gdt, idt; + __u64 cr0, cr2, cr3, cr4, cr8; + __u64 efer; + __u64 apic_base; + __u64 interrupt_bitmap[(KVM_NR_INTERRUPTS + 63) / 64]; +}; + +// +// /usr/include/x86_64-linux-gnu/asm/kvm.h: line 115 +// + +/* for KVM_GET_REGS and KVM_SET_REGS */ +struct kvm_regs { + /* out (KVM_GET_REGS) / in (KVM_SET_REGS) */ + __u64 rax, rbx, rcx, rdx; + __u64 rsi, rdi, rsp, rbp; + __u64 r8, r9, r10, r11; + __u64 r12, r13, r14, r15; + __u64 rip, rflags; +}; +``` + +### 在指定模式下运行 VM + +通过命令行参数确定了虚拟机将运行在什么模式之下,即确定了 `run_xxx_mode()` 中的 `xxx` 的值,并完成上一节的准备工作之后,最后一步就是调用 `run_kvm()` 来运行 VM。 + +`run_kvm()` 函数包含一个无限循环,实现两个功能:一个是通过 `ioctl(vcpu->fd, KVM_RUN, 0)` 运行 vCPU,另一个是判断退出 VM 的原因(如 `HLT` 指令,IO)并作出对应处理。对应代码如下: + +```C +// +// kvm-hello-world/kvm-hello-world.c: line 156 +// + +int run_vm(struct vm *vm, struct vcpu *vcpu, size_t sz) +{ + struct kvm_regs regs; + uint64_t memval = 0; + + // 无限循环,执行 KVM_RUN + // an infinite loop for KVM_RUN + for (;;) + { + if (ioctl(vcpu->fd, KVM_RUN, 0) < 0) + { + perror("KVM_RUN"); + exit(1); + } + + // VM 退出处理:HLT(halt)或者有 Input/Output 中断请求 + // exit reason handling (HLT, IO) + switch (vcpu->kvm_run->exit_reason) + { + case KVM_EXIT_HLT: + goto check; + + case KVM_EXIT_IO: + /* ... */ + + default: + /* ... */ + } + } +} +``` + +### 小结 + +综上所述,kvm-hello-world 的实现是较为清晰的,可以分为如下步骤: + +1. 打开 `/dev/kvm`,检查 KVM API 版本。 +2. 用 `KVM_CREATE_VM` 创建虚拟机, 使用 `mmap` 为虚拟机申请内存。 +3. 用 `KVM_CREATE_VCPU` 创建虚拟机 CPU,使用 `mmap` 为 CPU 申请内存区域(存储寄存器等信息)。 +4. 设置 CPU 寄存器和虚拟机内存初始值。 +5. 调用 `KVM_RUN` 执行 CPU。 + +为方便查阅,整个过程汇总成下表: + +| step | related data structure | purpose | execution result | +| ---- | ----------------------------- | -------------------- | ----------------------------------------------------------------- | +| 1 | `vm->sys_fd` | mount | `vm->sys_fd = open("/dev/kvm", O_RDWR);` | +| 1 | `vm->sys_fd` | check version | `api_ver = ioctl(vm->sys_fd, KVM_GET_API_VERSION, 0);` | +| 2 | from `sys_fd` to `fd` of `vm` | create VM | `vm->fd = ioctl(vm->sys_fd, KVM_CREATE_VM, 0);` | +| 2 | `vm->mem` | allocate VM memory | `vm->mem = mmap(NULL, mem_size, ..., -1, 0);` | +| 3 | `vcpu->vm_fd` | create vcpu | `vcpu->fd = ioctl(vm->fd, KVM_CREATE_VCPU, 0);` | +| 3 | `vcpu->kvm_run` | allocate vcpu memory | `vcpu->kvm_run = mmap(NULL, vcpu_mmap_size, ..., vcpu->fd, 0);` | +| 4 | `vcpu->vm_fd` | set sregs of vcpu | `ioctl(vcpu->fd, KVM_SET_SREGS, &sregs)` | +| 4 | `vcpu->vm_fd` | set regs of vcpu | `ioctl(vcpu->fd, KVM_SET_REGS, ®s)` | +| 4 | `vm->mem` | set VM memory | `memcpy(vm->mem, guestX, guestX_end - guestX);` X depends on mode | +| 5 | `vcpu->fd` | execute vCPU | `ioctl(vcpu->fd, KVM_RUN, 0)` | + +```mermaid +graph LR + +vmsz[vcpu_mmap_size] + +subgraph kernel +kvm[`/dev/kvm`] +km[memory] +end + + + +subgraph struct vm +kvm-- 1. open -->msfd[vm->sys_fd] +msfd-- 2. KVM_CREATE_VM-->mfd[vm->fd] +km-- 2. mmap -->mm[vm->mem] +end + +subgraph struct vcpu +mfd-- 3. KVM_CREATE_VCPU--->ufd[vcpu->fd] +ur[vcpu->kvm_run] +km-- 3. mmap-->ur +end + +msfd-- 3. KVM_GET_VCPU_MMAP_SIZE-->vmsz +vmsz-- 3. mmap -->ur + +ufd-- 4. KVM_SET_xREGS-->ufd +mm-- 4. memcpy--->mm + +ufd-- 5. execute --->ufd + +``` + +主函数调用上述函数实现,从命令行读入参数,创建并运行虚拟机: + +```C +// +// kvm-hello-world/kvm-hello-world.c: line 440 +// +int main(int argc, char **argv) +{ + struct vm vm; + struct vcpu vcpu; + enum + { + REAL_MODE, + PROTECTED_MODE, + PAGED_32BIT_MODE, + LONG_MODE, + } mode = REAL_MODE; + + // 处理命令行参数以判断 VM 以什么模式运行 + // parse cmd arg to verify running mode (real, protected, ...) + while ((opt = getopt(argc, argv, "rspl")) != -1) + { + switch (opt) {...} + } + + // VM 和 vCPU 的初始化 + // VM and vCPU init + vm_init(&vm, 0x200000); + vcpu_init(&vm, &vcpu); + + // run vm + switch (mode) + { + case REAL_MODE: + return !run_real_mode(&vm, &vcpu); + case ... + } + + return 1; +} +``` + +## kvmtool + +[kvmtool][4] 是一个支持多架构的用户态程序的实现,而 kvm-hello-world 当前实现仅支持 x86 架构,此节将结合上述对于用户态程序架构的分析考察 kvmtool 的实现,尤其是 RISC-V 架构不同于 x86 架构的 vCPU 的初始化。 + +### VM 的创建 + +```C +// +// kvm.c: line 436 +// +int kvm__init(struct kvm *kvm) +{ + int ret; + + if (!kvm__arch_cpu_supports_vm()) { + pr_err("Your CPU does not support hardware virtualization"); + ret = -ENOSYS; + goto err; + } + + /* 挂载设备 /dev/kvm */ + /* mount and validate whether it succeed */ + kvm->sys_fd = open(kvm->cfg.dev, O_RDWR); // ... + + /* 检查 KVM SPI 版本 */ + /* KVM API version check */ + ret = ioctl(kvm->sys_fd, KVM_GET_API_VERSION, 0); // ... + + /* 创建 VM */ + /* Create VM and validate the result */ + kvm->vm_fd = ioctl(kvm->sys_fd, KVM_CREATE_VM, kvm__get_vm_type(kvm)); // ... + + /* 检查 KVM 与架构相关的扩展 */ + /* check kvm extension related to architecture */ + if (kvm__check_extensions(kvm)) { /* ... */ } + + /* 申请 VM 内存 */ + /* Allocate guest memory */ + kvm__arch_init(kvm); + + /* 初始化申请到的 VM 内存 */ + /* Initialize memory */ + INIT_LIST_HEAD(&kvm->mem_banks); + kvm__init_ram(kvm); + + /* 加载内核镜像文件 */ + /* load guest kernel image (load firmware/BIOS if necessary for guest) */ + if (!kvm->cfg.firmware_filename) { /* ... */} + if (kvm->cfg.firmware_filename) { /* ... */ } + + return 0; +} +core_init(kvm__init); +``` + +### vCPU 的创建 + +vCPU 的创建是通过 `kvm-cpu.c` 文件中调用不同架构的 vCPU 创建函数来实现的。不同架构的 vCPU 实现在 `kvmtool/{arch}` 文件夹下,如 `kvmtool/riscv/`。 + +#### 创建 vCPU 的统一接口 + +`kvm_cpu__init` 用于创建指定数目的 vCPU,之后每个特定架构的 vCPU 的创建均通过调用 `kvmtool/{arch}/kvm-cpu.c` 中的函数来实现。 + +kvmtool 总的 vCPU 创建函数如下: + +```C +// +// kvm-cpu.c: line 260 +// +int kvm_cpu__init(struct kvm *kvm) +{ + int max_cpus, recommended_cpus, i; + + max_cpus = kvm__max_cpus(kvm); + recommended_cpus = kvm__recommended_cpus(kvm); + + if (kvm->cfg.nrcpus > max_cpus) { + printf(" # Limit the number of CPUs to %d\n", max_cpus); + kvm->cfg.nrcpus = max_cpus; + } else if (kvm->cfg.nrcpus > recommended_cpus) { + printf(" # Warning: The maximum recommended amount of VCPUs" + " is %d\n", recommended_cpus); + } + + kvm->nrcpus = kvm->cfg.nrcpus; + + task_eventfd = eventfd(0, 0); + if (task_eventfd < 0) { + pr_warning("Couldn't create task_eventfd"); + return task_eventfd; + } + + kvm->cpus = calloc(kvm->nrcpus + 1, sizeof(void *)); + if (!kvm->cpus) { + pr_warning("Couldn't allocate array for %d CPUs", kvm->nrcpus); + return -ENOMEM; + } + + for (i = 0; i < kvm->nrcpus; i++) { + kvm->cpus[i] = kvm_cpu__arch_init(kvm, i); + if (!kvm->cpus[i]) { + pr_warning("unable to initialize KVM VCPU"); + goto fail_alloc; + } + } + + return 0; + +fail_alloc: + for (i = 0; i < kvm->nrcpus; i++) + free(kvm->cpus[i]); + return -ENOMEM; +} +base_init(kvm_cpu__init); +``` + +#### RISC-V vCPU 的创建 + +在 kvmtool 中,一个 RISC-V 架构的 vCPU 的创建通过如下函数实现: + +```C +// +// {arch}/kvm-cpu.c: riscv/kvm-cpu.c, line 48 +// +struct kvm_cpu *kvm_cpu__arch_init(struct kvm *kvm, unsigned long cpu_id) +{ + struct kvm_cpu *vcpu; + u64 timebase = 0; + unsigned long isa = 0; + int coalesced_offset, mmap_size; + struct kvm_one_reg reg; + + vcpu = calloc(1, sizeof(struct kvm_cpu)); // ... + + vcpu->vcpu_fd = ioctl(kvm->vm_fd, KVM_CREATE_VCPU, cpu_id); // ... + + // ... + + mmap_size = ioctl(kvm->sys_fd, KVM_GET_VCPU_MMAP_SIZE, 0); // ... + + vcpu->kvm_run = mmap(NULL, mmap_size, PROT_RW, MAP_SHARED, vcpu->vcpu_fd, 0); + + // ... + + /* 设置对应的 ISA */ + /* set isa, test KVM_SET_ONE_REG */ + reg.id = RISCV_CONFIG_REG(isa); + reg.addr = (unsigned long)&isa; + + // ... + + /* 设置 vCPU 参数 */ + /* Populate the vcpu structure. */ + vcpu->kvm = kvm; + vcpu->cpu_id = cpu_id; + vcpu->riscv_isa = isa; + vcpu->riscv_xlen = __riscv_xlen; + vcpu->riscv_timebase = timebase; + vcpu->is_running = true; + + return vcpu; +} +``` + +此函数主要完成如下功能:调用 `KVM_CREATE_VCPU` 创建 vCPU 并设置 `kvm`, `cpu_id`, `isa`, `xlen` 等属性。 + +### vCPU 的运行 + +上述创建过程并未完成 vCPU 的寄存器初始化工作,vCPU 的初始化将在运行前完成,如本小结所示。在 kvmtool 中,`main.c` 中的 `main()` 函数获取命令行参数并进行参数解析,之后通过如下调用过程实现 vCPU 的运行: + +```mermaid +graph + +subgraph main.c +M[main]--->hkc[handle_kvm_command] +end + +hkc--->hc + +subgraph kvm-cmd.c +cs(struct cmd_struct kvm_commands) +hc[handle_command]-.-cs-.-kgc[kvm_get_command]-.->p===cs + +end + + +subgraph builtin-command.c + +subgraph builtin-run.c +kcrun[kvm_cmd_run]--->kvm_cmd_run_work--->kvm_cpu_thread +end +kcr[kvm_cmd_resume] +others[...] +end + +cs-.->kcrun +cs-.->kcr +cs-.->others + + +kvm_cpu_thread--->start + +subgraph kvm-cpu.c +base_init==>init +init[kvm_cpu__init] +start[kvm_cpu__start] +end + +start--->kvm_cpu__reset_vcpu +init--->kvm_cpu__arch_init +subgraph riscv/kvm-cpu.c +kvm_cpu__reset_vcpu +kvm_cpu__arch_init +end + +``` + +## QEMU + +### VM, vCPU 的创建与初始化 + +在 QEMU 中,KVM 是作为虚拟化加速器而存在的,其代码实现在 `qemu/accel/` 文件夹下,VM 和 vCPU 的创建、初始化和运行的通用代码都在 `qemu/accel/kvm/kvm-all.c` 中实现,而后`qemu/accel/kvm/kvm-accel-ops.c` 统一对其进行调用实现虚拟机加速的功能,如 vCPU 线程创建函数 `kvm_vcpu_thread_fn` 通过调用 `qemu/accel/kvm/kvm-all.c` 中的 `kvm_init_vcpu` 函数初始化一个 vCPU,通过调用 `kvm_destroy_vcpu` 函数销毁 vCPU: + +```C +// +// accel/kvm/kvm-accel-ops.c: line 27 +// +static void *kvm_vcpu_thread_fn(void *arg) +{ + CPUState *cpu = arg; + int r; + // ... + r = kvm_init_vcpu(cpu, &error_fatal); + // ... + kvm_destroy_vcpu(cpu); + // ... + return NULL; +} +``` + +VM 通过 `kvm_init` 函数创建,该函数在 `accel/kvm/kvm-all.c` 中实现: + +```c +// +// qemu/accel/kvm/kvm-all.c: line 2318 +// +static int kvm_init(MachineState *ms) { + // ... + KVMState *s; + // ... + s->fd = qemu_open_old("/dev/kvm", O_RDWR); /* ... */ + ret = kvm_ioctl(s, KVM_GET_API_VERSION, 0); /* ... */ + // ... + do { + ret = kvm_ioctl(s, KVM_CREATE_VM, type); + } while (ret == -EINTR); +} +``` + +vCPU 在函数 `kvm_init_vcpu` 中实现,代码如下: + +```c +// +// qemu/accel/kvm/kvm-all.c: line 443 +// +static int kvm_get_vcpu(KVMState *s, unsigned long vcpu_id) +{ + // ... + + /* 调用 KVM_CREATE_VCPU 创建 vCPU */ + /* create vcpu from KVM_CREATE_VCPU */ + return kvm_vm_ioctl(s, KVM_CREATE_VCPU, (void *)vcpu_id); +} + +// +// qemu/accel/kvm/kvm-all.c: line 461 +// +int kvm_init_vcpu(CPUState *cpu, Error **errp) +{ + /* 调用 kvm_get_vcpu() 创建 vCPU */ + /* create vcpu from kvm_get_vcpu() */ + ret = kvm_get_vcpu(s, kvm_arch_vcpu_id(cpu)); + // ... + /* 为 vCPU 申请内存 */ + /* allocate vcpu memory */ + mmap_size = kvm_ioctl(s, KVM_GET_VCPU_MMAP_SIZE, 0); // ... + cpu->kvm_run = mmap(NULL, mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED, + cpu->kvm_fd, 0); // ... + /* 初始化 vCPU */ + /* init vcpu, implemented in /qemu/target/riscv/kvm.c */ + ret = kvm_arch_init_vcpu(cpu); + // ... +} +``` + +特定架构的 vCPU 具有不同的初始化方式,RISC-V vCPU 的初始化函数如下: +其中 `kvm_riscv_reg_id` 根据传入的 config 确定位宽(32 或 64),之后 `kvm_get_one_reg` 函数取寄存器组中指示 ISA 的寄存器的值并返回。 + +```c +// +// target/riscv/kvm.c: line 397 +// +int kvm_arch_init_vcpu(CPUState *cs) +{ + int ret = 0; + target_ulong isa; + RISCVCPU *cpu = RISCV_CPU(cs); + CPURISCVState *env = &cpu->env; + uint64_t id; + qemu_add_vm_change_state_handler(kvm_riscv_vm_state_change, cs); + /* 配置寄存器映射为类型 1 */ + /* Config registers are mapped as type 1 */ + // #define KVM_REG_RISCV_CONFIG (0x01 << KVM_REG_RISCV_TYPE_SHIFT) + // #define KVM_REG_RISCV_CONFIG_REG(name) \ + // (offsetof(struct kvm_riscv_config, name) / sizeof(unsigned long)) + id = kvm_riscv_reg_id(env, KVM_REG_RISCV_CONFIG, + KVM_REG_RISCV_CONFIG_REG(isa)); + ret = kvm_get_one_reg(cs, id, &isa); + if (ret) { + return ret; + } + env->misa_ext = isa; + return ret; +} +``` + +## 总结 + +### 用户态程序的结构 + +用户态程序依据是否架构相关可以分为两部分,一部分是通用的、架构无关的代码,其功能包括 VM、vCPU 等的创建、虚拟内存的申请,另一部分是架构相关的代码,具体包括 vCPU 的寄存器组的初始化。 + +结合 kvm-hello-world, kvmtool, QEMU 中创建 KVM 虚拟机的源码分析,我们可以得知,不同架构虚拟机的创建过程中,主要的不同来自于虚拟机 CPU 的架构不同,如 x86, RISC-V, ARM。所有的用户态程序为了支持特定架构的虚拟机,均需要针对其目标架构做单独处理。如 kvmtool 中的 x86, riscv, powerpc 等文件夹下的 `kvm.c`, `kvm-cpu.c` 就是针对这些架构的特别实现,类似的还有 QEMU 中 `target/riscv/kvm.c` 的实现。 + +### 架构在用户态程序里的体现 + +综上可知,在用户态程序的实现中,需要对不同的架构做出不同的处理,具体而言则是 vCPU 创建之后的寄存器初始化各有不同,所以需要各自单独处理。对于 RISC-V vCPU 的初始化而言,相较于 x86 架构需要分别针对实模式、保护模式对段寄存器等分别进行不同的初始化,RISC-V vCPU 的初始化较为简单,仅需依据 vCPU 的 `config` 针对 `pc`, `a0`, `a1` 进行设置。 + +下面将参考用户态程序中架构相关的代码实现,结合特定架构的指令集标准,分析 KVM 中 CPU 虚拟化的具体机制。 + +## 参考资料 + +- [What is KVM?][1] +- [Linux Kernel][2] +- [kvm-hello-world][3] +- [kvmtool][4] +- [QEMU][5] + +[1]: https://www.redhat.com/en/topics/virtualization/what-is-KVM#%E7%BA%A2%E5%B8%BDkvm%E8%99%9A%E6%8B%9F%E5%8C%96 +[2]: https://www.kernel.org/ +[3]: https://github.com/dpw/kvm-hello-world +[4]: https://github.com/kvmtool/kvmtool +[5]: https://www.qemu.org/