Hello KVM

Table of Contents

1. Hello KVM

1.1. hello world

https://github.com/dpw/kvm-hello-world

https://www.cse.iitb.ac.in/~mythili/virtcc/pa/pa1.html

一个简单的基于 KVM 的 userspace 的 hypervisor 的工作流程是:

  1. 通过 ioctl (KVM_CREATE_VM) 创建 vm
  2. 通过 mmap 分配一段内存, 用 ioctl(KVM_SET_USER_MEMORY_REGION) 把它设置为 vm 的 `物理`内存, 具体如何工作的参考
  3. 通过 ioctl(KVM_CREATE_VCPU) 创建 vcpu
  4. 通过 mmap(vcpu->fd) 返回一段内存(struct kvm_run), kvm 和 hypervisor 可以通过它交换数据, 例如 exit_reason, io 请求的信息等
  5. 加载代码到 vm 的物理内存 (通过第 2 步的 mmap)
  6. 通过 ioctl(KVM_GET_SREGS) 和 ioctl(KVM_SET_SREGS) 设置 guest 代码的入口 (例如修改 x86 的 rip)
  7. 通过 ioctl(KVM_RUN) 执行 guest 代码
  8. hypervisor 会阻塞在这个 ioctl 上, 当 ioctl 返回后, 检查 kvm_run 中的信息. 例如, 若 kvm_run->exit_reaon 为 KVM_EXIT_HLT, 则表示 guest 执行结束, 若 exit_reason 为 KVM_EXIT_IO, 则表示需要进行 io, hypervisor 需要从 kvm_run->io 中获得具体的 io 请求的信息, 然后代表 guest 进行 io 处理
  9. 若 guest 没有结束, 则重复 7

1.1.1. x86 保护模式

要开启 x86 保护模式, 需要:

  1. 设置 gdt (globa description table) 中对应的 cs 的 segment descriptor
  2. 设置 cr0

KVM_GET_SREGS 和 KVM_SET_SREGS 使用的 kvm_sregs 结构中, 大部分直接对应寄存器的值 (例如 kvm_sregs->cr0), 但涉及到 cs, ss 等 segment register 时 (例如 kvm_sregs->cs) 它包含的实际是 cs 对应的 segment descriptor 的数据. kvm 会负责把它和 gdt 里对应的数据对应起来.

具体代码如下:

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;

    if (ioctl(vcpu->fd, KVM_SET_SREGS, &sregs) < 0) {
        perror("KVM_SET_SREGS");
        exit(1);
    }
}

1.1.2. 使用分页

在保护模式下通过配置 cr0 可以开启分页, 同时需要配置 cr3 为 pd (page directory) 基址, 例如:

static void setup_paged_32bit_mode(struct vm *vm, struct kvm_sregs *sregs) {
    uint32_t pd_addr = 0x2000;
    uint32_t *pd = (void *)(vm->mem + pd_addr);

    /* A single 4MB page to cover the memory region */
    pd[0] = PDE32_PRESENT | PDE32_RW | PDE32_USER | PDE32_PS;
    /* Other PDEs are left zeroed, meaning not present. */

    sregs->cr3 = pd_addr;
    sregs->cr4 = CR4_PSE;
    sregs->cr0 = CR0_PE | CR0_MP | CR0_ET | CR0_NE | CR0_WP | CR0_AM | CR0_PG;
    sregs->efer = 0;

    if (ioctl(vcpu->fd, KVM_SET_SREGS, &sregs) < 0) {
        perror("KVM_SET_SREGS");
        exit(1);
    }
}

上面的代码中只设置了 pd[0] 的一项, 通过 cr4 开启了 4MB page (默认为 4KB page), 且其对应 dir ([31..22]) 为默认值 0, 且, 意味着 4MB 范围内的 va (虚拟地址) 和 pa (物理地址) 是相同的

另外, 代码中设置 pd 地址为 0x2000 (8K), 是因为 guest image 被 load 到 0x0, 且它的大小不超过 8K, 因此这个地址是空闲可用的.

1.2. VMX

https://nixhacker.com/developing-hypervisior-from-scratch-part-1/

https://www.cse.iitb.ac.in/~mythili/virtcc/slides_pdf/04-hwvirt-kvmqemu.pdf

使用 VMX 实现一个简化的 kvm: https://github.com/shubham0d/ProtoVirt

VMX 主要的指令有:

  • VMXON - Enable VMX
  • VMXOFF - Disable VMX
  • VMLAUNCH - Start/enter VM
  • VMRESUME - Re-enter VM
  • VMCLEAR - Null out/reinitialize VMCS
  • VMPTRLD - Load the current VMCS
  • VMPTRST - Store the current VMCS
  • VMREAD - Read values from VMCS
  • VMWRITE - Write values to VMCS
  • VMCALL - Exit virtual machine to VMM
  • VMFUNC - Invoke a VM function in VMM without exiting guest operation

VMX 的核心是 VMCS (vm control structure), 它是由 host 分配的 buffer, 且 host 和 guest 都能操作它.

它包含了以下信息:

  1. control
  2. host state
  3. guest state
  4. exit reason

VMCS 的作用有:

  • kvm 的许多操作都是通过读写 VMCS 实现的, 例如 KVM_SET_SREGS 实际就是修改了 VMCS 中 guest state 中的 sreg 部分, 例如:

    void vmx_set_cr0(struct kvm_vcpu *vcpu, unsigned long cr0):
      // ...
      vmcs_writel(GUEST_CR0, hw_cr0);
        __vmcs_writel(field, value);
          vmx_asm2(vmwrite, "r"(field), "rm"(value), field, value);
      // ...
    
  • VMCS 同时也控制着 non-root 模式下 guest 怎么运行, 例如执行哪些指令时需要通过VM Exit 退回到 root 模式
  • kvm_run 结构体中的 exit_reason 等也是从 VMCS 复制过来的.
  • guest 执行时会通过 VMCS 恢复 guest 上下文, 从而读到修改后寄存器值

1.3. qemu

qemu kvm 与 QEMU TCG 属于不同的 accelerator, 都需要实现 AccelOpsClass, kvm 对应的 ops->create_vcpu_thread 为 kvm_start_vcpu_thread, 最终会执行 kvm_cpu_exec

kvm_init:
  kvm_memory_listener_register(s, &s->memory_listener, &address_space_memory, 0, "kvm-memory");
    // ...
    kvm_region_add(MemoryListener *listener,MemoryRegionSection *section);
      kvm_set_user_memory_region(kml, mem, false);
        kvm_vm_ioctl(s, KVM_SET_USER_MEMORY_REGION, &mem);

int kvm_cpu_exec(CPUState *cpu):
  do:
    if (cpu->vcpu_dirty):
      kvm_arch_put_registers(cpu, KVM_PUT_RUNTIME_STATE);
        ret = kvm_getput_regs(x86_cpu, 1);
          kvm_getput_reg(&regs.rip, &env->eip, set);
      cpu->vcpu_dirty = false;

    run_ret = kvm_vcpu_ioctl(cpu, KVM_RUN, 0);

    switch (run->exit_reason):
      case KVM_EXIT_IO:
        kvm_handle_io(run->io.port, attrs,
                          (uint8_t *)run + run->io.data_offset,
                          run->io.direction,
                          run->io.size,
                          run->io.count);
        break;

1.4. hardware vs. software virtualization

https://docs.oracle.com/en/virtualization/virtualbox/6.0/admin/swvirt-details.html

https://cseweb.ucsd.edu/~yiying/cse291j-winter20/reading/Virtualize-CPU.pdf

软件虚拟化大约分成几种情况:

  1. guest 和 hypervisor 都运行在 ring 3, hypervisor 通过模拟来执行每一条 guest 指令, 例如 spike
  2. guest 运行在 ring 3, hypervisor 通过 driver 运行在 ring 0, guest 可以直接大部分指令, 但是当 guest 执行特权指令时, 有两种情况:
    1. 处理器会发生 trap, hypervisor 处理这个 trap
    2. 处理器不会发生 trap (例如 x86 上的 popf), 这时需要 hypervisor 提前把 popf 翻译, 解释或 patch 成 trap 指令, 例如 virtualbox, vmware
    3. 处理器不会发生 trap, 需要直接修改 guest 代码, 让它用 trap 代替前面的指令, 即半虚拟化 (paravirtualization), 例如 xen

硬件虚拟化时, guest 运行在 ring0, 大部分特权指令都不是问题, 但某些特定指令例如 IO 或 MMU 相关的指令还是需要 guest 能 VMEXIT 以便 hypervisor 接手做特殊处理.

硬件虚拟化并不是完美的, 例如 guest 进行 IO 操作可能会这样操作:

void nic_write_buffer(char* buf, int size) {
    for (int i = 0; i < N; i++) {
        outb(NIC_TX_BUF, *buf++);
    }
}

这个操作都会导致大量的 VMEXIT, 每个 VMEXIT 大约需要 200 个 cycle, 会造成巨大的开销.

如果我们能够修改 guest 的代码, 可以变成这样:

void nic_write_buffer(char* buf, int size) {
    vmm_write(NIC_TX_BUF, buf, size);
}

只需要一次 VMEXIT 即可, 但这需要修改 guest 和 hypervisor 的代码, 相当于 IO 的半虚拟化, qemu 的 virtio 即是这种思想.

1.5. MMU Virtualization

https://www.linux-kvm.org/page/Memory

https://www.cse.iitb.ac.in/~mythili/virtcc/slides_pdf/06-memvirt.pdf

MMU 虚拟化涉及到几个不同的地址:

  1. GVA, guest virtual address
  2. GPA, guest physical address
  3. HVA, host virtual address
  4. HPA, host physical address

guest 有自己的页表, 但页表查到的 GPA 并不是真正的物理地址. guest 上地址查找的过程是: GVA -> GPA -> HVA -> HPA

  1. GVA -> GPA 由 guest 页表负责
  2. GPA -> HVA 由 host 自己维护, 例如 kvm 中的 KVM_SET_MEMORY_REGION
  3. HVA -> HPA 由 host 页表负责

通过 VMX 的 EPT (Extended Page Tables) 或软件的 shadow page table 可以快速完成 GPA -> HPA 的过程.

1.6. IO Virtualization

https://howtovms.wordpress.com/2018/04/24/virtio/

https://www.cse.iitb.ac.in/~mythili/virtcc/slides_pdf/07-iovirt.pdf

https://blogs.oracle.com/linux/post/introduction-to-virtio

https://www.redhat.com/en/blog/deep-dive-virtio-networking-and-vhost-net

kvm 默认的 IO 处理方式是模拟的:

  1. 对于 pio (Programmed IO), 处理器的 trap 导致 VMEXIT, userspace hypervisor 进行 IO 并把数据通过 kvm_run 返回给 guest
  2. 对于 mmio (Memory Mapped IO), shadow page fault 导致 VMEXIT, kvm 负责确认 memory 访问来自 mmio page, 在 kvm_run 中构造 KVM_EXIT_MMIO 数据 (pa, is_write, data, len), 通过 VMEXIT 返回给 userspace hypervisor 处理. 同时 kvm 不会填充 shadow page table, 确保下次 mmio 还可以触发 page fault

qemu virtio 的处理方式是半虚拟化的: virtio 通过 guest 端的 virtio front-end driver 打包 IO 请求, 通过一次 VMEXIT 发送给 hypervisor 端的 back-end driver, 双方用 ring buffer 交换数据

另外, 还可以用 VFIO (Virtual Function IO) 以 pass through 的方式访问外设.

1.7. RISC-V H extension

https://arxiv.org/pdf/2103.14951.pdf

file:///home/sunway/download/riscv-privileged-20211203.pdf

https://github.com/kvm-riscv/linux

H 扩展没有 VMCS, 相关的 host, guest 的上下文都保存在 kvm_vcpu_arch 中. 修改寄存器时直接修改 kvm_vcpu_arch 中的数据, 例如 kvm_riscv_vcpu_set_reg_core

VS 有自己独立的 superior CSR (例如 vsepc, vsatp), 可以加速 VS 和 HS 之间的上下文切换: 这些 CSR 在 HS/VS 切换时不需要保存, 只在多个 VS 切换时需要保存, 参考 kvm_arch_vcpu_load

HS 与 VS 之间的切换是通过标准的 ecall/sret, 参考 kvm-riscv 的 __kvm_riscv_switch_to 函数

Author: [email protected]
Date: 2023-02-27 Mon 16:59
Last updated: 2024-08-17 Sat 15:51

知识共享许可协议