QEMU-KVM CPU虚拟化

QEMU/KVM CPU虚拟化

KVM自内核2.6.20起被合入Linux,作为Linux的一个内核模块,在Linux启动时被动态加载。KVM利用了硬件辅助虚拟化的特性,能够高效的实现CPU和内存的虚拟化。
实际上,KVM无法单独使用,因为它既不提供I/O设备的模拟,也不支持对整体虚拟机的状态状态进行管理。它向用户态程序(如QEMU)暴露特殊的设备文件/dev/kvm作为借口,允许用户态程序利用它来实现最为关键的CPU和内存虚拟化,但还缺少I/O虚拟化需要的设备模型(Device Module),而QEMU正好可以弥补这块功能。

1. CPU Virtualization

1.1 CPU虚拟化基本概念

在大部分Hypervisor中,vCPU对应一个线程,通过分时复用的方式共享物理CPU。vCPU上下文切换流程如图所示。

  1. 当vCPU 1时间片用尽时,Hypervisor会中断vCPU 1的执行,vCPU 1陷入Hypervisor中(如图中 I )。
  2. 在此过程中,硬件将vCPU 1的寄存器状态保存到固定区域(如图中 1),并从中加载Hypervisor的寄存器状态(如图中 2)。
  3. Hypervisor进行vCPU调度,选择下一个运行的vCPU,保存Hypervisor的寄存器状态(如图中 3),并加载选定的vCPU 2的寄存器状态(如图中 4)
  4. 然后恢复vCPU 2运行(如图中 II)。

上述固定区域与系统架构实现密切相关。
vCPU上下文切换.png
vCPU不仅要高效的执行所有指令,还要能够正确的处理系统中出现的中断和异常事件。大部分异常(如除零错误、非法指令等)无需虚拟化,直接交给虚拟机操作系统处理即可。对于部分需要虚拟化的异常,需要陷入Hypervisor中进行相应的处理,没有固定的解决方案。
当发生外部中断时,外部设备会将中断请求发送到中断控制器,中断控制器再发送给CPU。以磁盘为例,物理环境下,操作系统发起读盘请求,磁盘将操作系统需要的数据放到指定的位置后给中断控制器发送一个中断请求,中断控制器接收后设置好内部相应的寄存器,CPU每次执行指令前,都会检查中断控制器中是否存在未处理的中断,若有则调用相应的ISR(中断服务例程)进行处理。
在虚拟机环境下,一个理想的方案是将物理中断交给Hypervisor处理,再由Hypervisor设置虚拟中断控制器,注入一个虚拟中断到虚拟机中。流程如下图所示:
虚拟中断.png

  1. 向虚拟磁盘发起读写请求
  2. 虚拟机下陷
  3. Hypervisor向物理磁盘发起读写请求
  4. 向中断控制器提交中断
  5. 执行中断服务例程
  6. Hypervisor设置虚拟磁盘与虚拟中断控制器
  7. 恢复虚拟机执行
  8. 虚拟机执行中断服务例程

上述流程对硬件有两点要求:1. 物理中断将会触发虚拟机下陷进入Hypervisor中处理。2. 虚拟机恢复运行时要先检查虚拟中断控制器是否有待处理中断。
这个方案解决了多虚拟机系统中物理中断的路由问题,但是对于直通设备却很不友好。为了解决这个问题,ELI(不退出中断)引入了Shadow IDT(影子中断描述符),区分直通设备产生的中断和其他物理中断,直通设备产生的中断直接递交给虚拟机处理,无需Hypervisor介入。DID(直接中断交付)则进一步通过将虚拟设备产生的中断转为物理IPI(处理器间中断)直接递交给虚拟机进行处理。

1.2 Intel VT-x 硬件辅助虚拟化

VMCS

Intel VT-x引入了VMCS来解决上下文切换问题,VMCS是内存中的一块区域,用于在VM-Entry和VM-Exit过程中国呢保存和加载Hypervisor和虚拟机的寄存器状态。此外,VMCS还包含一些控制域,用于控制CPU的行为。再多处理器虚拟机中,VMCS与vCPU一一对应,每个vCPU都拥有一个VMCS。

PIC & APIC

PIC
PIC,即Intel 8259A芯片,是单处理器(Uni-processor)时代广泛使用的中断控制器。它主要包含8个中断引脚IR0-IR7,用于连接外部设备以及三个内部寄存器:IMR(Interrupt Mask Register,中断屏蔽寄存器)、IRR(Interrupt Request Register,中断请求寄存器)和ISR(Interrupt Service Register,中断服务寄存器)。
APIC
APIC是20世纪90年代,Intel为了应对多处理器(Multi- Processor)架构提出的一整套中断处理方案。APIC适用于多处理器机器,每个CPU拥有一个LAPIC,整个机器拥有一个或多个IOAPIC,设备的中断信号先经由IOAPIC汇总,再分发给一个或多个CPU的LAPIC。在APIC架构下,LAPIC的中断来源可分为3类:

  1. 本地中断
  2. 通过IOAPIC接收到的外部中断
  3. 处理器间中断(IPI)

LAPIC收到中断消息后,确认自己是否为中断消息的目标,如果是,则对该中断消息进行进行一步处理,这一步称为中断路由,LAPIC接收到中断消息后,处理流程如下:

  1. 如果收到的中断是NMI,SMI,INIT,ExtINT或SIPI,则直接交给CPU处理,否则设置IRR寄存器中适当的位,将中断加入等待队列,这一步称为中断接受
  2. 对于IRR中阻塞的中断,APIC取出其中优先级最高的中断,交给CPU处理,并根据中断向量号设置ISR寄存器中相应的位。这一步称为中断确认
  3. CPU通过中断向量号索引IDT(中断描述符表)执行对应的中断处理例程,这一步称为中断交付
  4. 中断处理例程执行完毕时,应写入EOI寄存器,使得APIC从ISR队列中删除对应的项,结束该中断的处理(NMI,SMI,INIT,ExtINT或SIPI不需要写入EOI)

APIC中断处理流程:中断产生 --> 中断路由 --> 中断接受 --> 中断确认 --> 中断交付
MSI
MSI(Message Signaled Interrupt,消息告知中断)是PCI总线发展出的新型中断传递方式,它允许设备直接发送中断到LAPIC而无需经过IOAPIC。MSI本质上就是在中断发生时,不通过带外的中断信号线,而是通过带内的PCI写入事务来通知LAPCI中断的发生。从原理上来说,MSI产生的事务与一般的DMA事务并无本质区别,需要依赖特定平台的特殊机制从总线事务中区分出MSI并赋予其中断的语意。

中断虚拟化

默认情况下,虚拟机通过MMIO访问虚拟APIC,但是由于底层缺乏实际物理硬件供其访问,故Hypervisor需要截获这些MMIO请求进而访问存储在内存中的虚拟寄存器的值。Hypervisor通常会将虚拟机中APIC内存映射区域的EPT项设为不存在,于是虚拟机访问这段地址时会出发VM-Exit,陷入Hypervisor进行处理。
对于中断处理流程中的中断产生到中断确认,都可以通过设置虚拟寄存器实现相应的功能,但是对于中断交付,Intel VT-x中,是通过VMCS VM-Entry控制域中的VM-Entry中断信息字段实现的,每次触发VM-Entry时,CPU会检查该域,发现是否有带处理的事件并用向量号索引执行相应的处理函数。
APICv
APICv是Intel针对APIC虚拟化提出的优化技术,主要包括虚拟APIC访问优化和虚拟中断递交。
APICv引入虚拟APIC页的概念,相当于一个影子APIC,虚拟机对APIC的部分甚至全部访问都可以被硬件重定向为对虚拟APIC页的访问,这样就不需要频繁触发VM-Exit了。而虚拟中断递交通过VM- Execution控制域中的虚拟中断递交字段开启。开启后,虚拟中断注入VMCS客户机状态域中的客户机中断状态字段完成,其低8位为RVI(Requesting Virtual Interrupt,待处理虚拟中断),表示虚拟机待处理中断中优先级最高的中断向量号,相当于IRRV;其高8位为SVI(Servicing Virtual Interrupt,处理中虚拟中断),表示虚拟机正在处理中断中优先级最高的中断向量号,相当于ISRV。开启虚拟中断递交后,Hypervisor只需要设置RVI的值,在VM-Entry时,CPU将会根据RVI的值进行虚拟中断提交。

2. KVM API

The kvm API is a set of ioctls that are issued to control various aspects of a virtual machine. The ioctls belong to three classes:

  • System ioctls: These query and set global attributes which affect the whole kvm subsystem. In addition a system ioctl is used to create virtual machines.
  • VM ioctls: These query and set attributes that affect an entire virtual machine, for example memory layout. In addition a VM ioctl is used to create virtual cpus (vcpus) and devices. VM ioctls must be issued from the same process (address space) that was used to create the VM.
  • vcpu ioctls: These query and set attributes that control the operation of a single virtual cpu. vcpu ioctls should be issued from the same thread that was used to create the vcpu, except for asynchronous vcpu ioctl that are marked as such in the documentation. Otherwise, the first ioctl after switching threads could see a performance impact.
  • device ioctls: These query and set attributes that control the operation of a single device. device ioctls must be issued from the same process (address space) that was used to create the VM.
APITypeParametersReturnDescription
KVM_CREATE_VMsystemctl ioctlmachine type identifier (KVM_VM_*)a VM fd that can be used to control the new virtual machine
KVM_GET_MSR_INDEX_LIST, KVM_GET_MSR_FEATURE_INDEX_LISTsystem ioctlstruct kvm_msr_list (in/out)0 on success; -1 on error
KVM_CHECK_EXTENSIONsystem ioctl, vm ioctlextension identifier (KVM_CAP_*)0 if unsupported; 1 (or some other positive integer) if supported
KVM_GET_VCPU_MMAP_SIZEsystem ioctlnonesize of vcpu mmap area, in bytes
KVM_SET_MEMORY_REGIONvm ioctlstruct kvm_memory_region (in)0 on success, -1 on error
KVM_CREATE_VCPUvm ioctlvcpu id (apic id on x86)vcpu fd on success, -1 on error
KVM_RUNvcpu ioctlnone0 on success, -1 on errorThis ioctl is used to run a guest virtual cpu.
KVM_GET_REGSvcpu ioctlstruct kvm_regs(out)0 on success, -1 on error
KVM_SET_REGSvcpu ioctlstruct kvm_regs(in)0 on success, -1 on error
KVM_TRANSLATEvcpu ioctlstruct kvm_translation(in/out)0 on success, -1 on error
KVM_INTERRUPTvcpu ioctlstruct kvm_interrupt (in)0 on success, negative on failure
KVM_SET_CPUIDvpu ioctlstruct kvm_cpuid (in)0 on success, -1 on error
KVM_GET_LAPICvcpu ioctlstruct kvm_lapic_state (out)0 on success, -1 on error
KVM_GET_VCPU_EVENTSvcpu ioctlstruct kvm_vcpu_event (out)0 on success, -1 on error
KVM_SET_LAPICvcpu ioctlstruct kvm_lapic_state (in)0 on success, -1 on error
KVM_IOEVENTFDvm ioctlstruct kvm_ioeventfd (in)0 on success, !0 on error
KVM_CREATE_DEVICEvm ioctlstruct kvm_create_device (in/out)0 on success, -1 on error
KVM_SET_DEVICE_ATTR/KVM_GET_DEVICE_ATTRdevice ioctl, vm ioctl, vcpu ioctlstruct kvm_device_attr0 on success, -1 on error
KVM_SET_USER_MEMORY_REGIONvm ioctlstruct kvm_userspace_memory_region (in)0 on success, -1 on errorThis ioctl allows the user to create, modify or delete a guest physical memory slot.

3. kvm test

#include <err.h>
#include <fcntl.h>
#include <linux/kvm.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>

int main(void)
{
    int kvm, vmfd, vcpufd, ret;
    const uint8_t code[] = {
        0xba, 0xf8, 0x03, /* mov $0x3f8, %dx */
        0x00, 0xd8,       /* add %bl, %al */
        0x04, '0',        /* add $'0', %al */
        0xee,             /* out %al, (%dx) */
        0xb0, '\n',       /* mov $'\n', %al */
        0xee,             /* out %al, (%dx) */
        0xf4,             /* hlt */
    };
    uint8_t *mem;
    struct kvm_sregs sregs;
    size_t mmap_size;
    struct kvm_run *run;

    kvm = open("/dev/kvm", O_RDWR | O_CLOEXEC);
    if (kvm == -1)
        err(1, "/dev/kvm");

    /* Make sure we have the stable version of the API */
    ret = ioctl(kvm, KVM_GET_API_VERSION, NULL);
    if (ret == -1)
        err(1, "KVM_GET_API_VERSION");
    if (ret != 12)
        errx(1, "KVM_GET_API_VERSION %d, expected 12", ret);

    vmfd = ioctl(kvm, KVM_CREATE_VM, (unsigned long)0);
    if (vmfd == -1)
        err(1, "KVM_CREATE_VM");

    /* Allocate one aligned page of guest memory to hold the code. */
    mem = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    if (!mem)
        err(1, "allocating guest memory");
    memcpy(mem, code, sizeof(code));

    /* Map it to the second page frame (to avoid the real-mode IDT at 0). */
    struct kvm_userspace_memory_region region = {
        .slot = 0,
        .guest_phys_addr = 0x1000,
        .memory_size = 0x1000,
        .userspace_addr = (uint64_t)mem,
    };
    ret = ioctl(vmfd, KVM_SET_USER_MEMORY_REGION, &region);
    if (ret == -1)
        err(1, "KVM_SET_USER_MEMORY_REGION");

    vcpufd = ioctl(vmfd, KVM_CREATE_VCPU, (unsigned long)0);
    if (vcpufd == -1)
        err(1, "KVM_CREATE_VCPU");

    /* Map the shared kvm_run structure and following data. */
    ret = ioctl(kvm, KVM_GET_VCPU_MMAP_SIZE, NULL);
    if (ret == -1)
        err(1, "KVM_GET_VCPU_MMAP_SIZE");
    mmap_size = ret;
    if (mmap_size < sizeof(*run))
        errx(1, "KVM_GET_VCPU_MMAP_SIZE unexpectedly small");
    run = mmap(NULL, mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED, vcpufd, 0);
    if (!run)
        err(1, "mmap vcpu");

    /* Initialize CS to point at 0, via a read-modify-write of sregs. */
    ret = ioctl(vcpufd, KVM_GET_SREGS, &sregs);
    if (ret == -1)
        err(1, "KVM_GET_SREGS");
    sregs.cs.base = 0;
    sregs.cs.selector = 0;
    ret = ioctl(vcpufd, KVM_SET_SREGS, &sregs);
    if (ret == -1)
        err(1, "KVM_SET_SREGS");

    /* Initialize registers: instruction pointer for our code, addends, and
     * initial flags required by x86 architecture. */
    struct kvm_regs regs = {
        .rip = 0x1000,
        .rax = 2,
        .rbx = 2,
        .rflags = 0x2,
    };
    ret = ioctl(vcpufd, KVM_SET_REGS, &regs);
    if (ret == -1)
        err(1, "KVM_SET_REGS");

    /* Repeatedly run code and handle VM exits. */
    while (1) {
        ret = ioctl(vcpufd, KVM_RUN, NULL);
        if (ret == -1)
            err(1, "KVM_RUN");
        switch (run->exit_reason) {
        case KVM_EXIT_HLT:
            puts("KVM_EXIT_HLT");
            return 0;
        case KVM_EXIT_IO:
            if (run->io.direction == KVM_EXIT_IO_OUT && run->io.size == 1 && run->io.port == 0x3f8 && run->io.count == 1)
                putchar(*(((char *)run) + run->io.data_offset));
            else
                errx(1, "unhandled KVM_EXIT_IO");
            break;
        case KVM_EXIT_FAIL_ENTRY:
            errx(1, "KVM_EXIT_FAIL_ENTRY: hardware_entry_failure_reason = 0x%llx",
                 (unsigned long long)run->fail_entry.hardware_entry_failure_reason);
        case KVM_EXIT_INTERNAL_ERROR:
            errx(1, "KVM_EXIT_INTERNAL_ERROR: suberror = 0x%x", run->internal.suberror);
        default:
            errx(1, "exit_reason = 0x%x", run->exit_reason);
        }
    }
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Hack Rabbit

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值