KVM
Kernel-based Virtual Machine(KVM)是一种内建于 Linux 中的开源虚拟化技术。使用linux kernel作为hypervisor(虚拟机管理程序),所有虚拟机监控程序都需要一些操作系统层面的组件才能运行虚拟机,如内存管理器、进程调度程序、输入/输出(I/O)堆栈、设备驱动程序、安全管理器以及网络堆栈等。由于 KVM 是 Linux 内核的一部分,因此所有这些组件它都有。每个虚拟机都像普通的 Linux 进程一样实施,由标准的 Linux 调度程序进行调度,并且使用专门的虚拟硬件,如网卡、图形适配器、CPU、内存和磁盘等。
KVM本质上是一个管理虚拟硬件的驱动,从存在形式来看,它包括两个内核模块:kvm.ko和kvm_intel.ko(或kvm_amd.ko),通过/dev/kvm(由KVM本身创建)作为管理接口,主要负责vCPU的创建、虚拟内存的分配、vCPU寄存器的读写、vCPU的运行。KVM 能够让linux成为一个hypervisor(虚拟机监控器),在支持VMX(virtual machine Extension)功能的x86处理器中,linux在原有的用户模式和内核模式之外增加了客户模式,虚拟机就是运行在客户模式中。KVM模块的职责就是就是打开并初始化VMX功能,提供相应的接口以支持虚拟机的运行。KVM通过调用Linux本身内核功能,实现对CPU的虚拟化和内存虚拟化。
### 内核模块
[localhost ~]$ lsmod | grep kvm
kvm_intel 142999 0
kvm 444314 1 kvm_intel
### 要使用KVM,首先要检查硬件是否有虚拟化支持
[localhost ~]$ grep -E 'vmx|svm' /proc/cpuinfo`
QEMU
Quick Emulator(QEMU)一款开源的模拟器及虚拟机监管器,本身并不包含或依赖KVM模块,QEMU是一个纯软件的实现,可以在没有KVM模块的情况下独立运行,但是性能比较低。QEMU有整套的虚拟机实现,包括处理器虚拟化、内存虚拟化以及I/O设备的虚拟化。QEMU有两种工作模式:系统模式,可以模拟出整个电脑系统,另一种是用户模式,可以运行不同与当前硬件平台的其他平台上的程序(比如在x86平台上运行跑在ARM平台上的程序)。
QEMU-KVM
现在流行的KVM虚拟化平台,修改了QEMU的代码,把模拟CPU、内存的代码换成了KVM,而网卡、显卡等留着,因此QEMU+KVM就成了一个完整的虚拟化平台。
从QEMU的角度来看,虚拟机运行期间,QWMU通过KVM提供的接口进行内核设置,由KVM模块负责将虚拟机置于处理器的VMX模式(客户模式)运行。QEMU使用了KVM的虚拟化功能,为自己的虚拟机提供了硬件虚拟化加速以提高虚拟机的性能(硬件CPU支持虚拟化)。
从KVM的角度来看,KVM只是内核模块,用户没发直接和内核模块交互,需要借助用户空间的管理工具,因此需要借助QEMU这个运行在用户态的工具,即KVM运行在QEMU(qemu-kvm)的进程上下文中,KVM和QEMU相辅相成,QEMU通过KVM得到了硬件虚拟化的性能,KVM通过QEMU来模拟设备并实现和内核空间KVM的交互。此外,由于QEMU模拟IO设备效率不高的原因,现在常常采用半虚拟化的virtio方式来虚拟IO设备。
我们经常使用的开源VirtualBox、Xen虚拟化产品,其核心底层的虚拟化部分就有集成和使用QEMU,理解了QEMU和KVM的关系,也就理解了VirtualBox、Xen等虚拟化产品集成和使用QEMU的关系。
代码
虚拟机vCPU调用KVM的接口来执行任务的流程如下(代码源自QEMU开发者Stefan的 技术博客 ):
open("/dev/kvm")
ioctl(KVM_CREATE_VM)
ioctl(KVM_CREATE_VCPU)
for (;;) {
ioctl(KVM_RUN)
switch (exit_reason) {
case KVM_EXIT_IO: /* ... */
case KVM_EXIT_HLT: /* ... */
}
}
KVM则利用硬件扩展直接将虚拟机代码运行于主机之上,一旦vCPU需要操作设备寄存器,vCPU将会停止并退回到QEMU,QEMU去模拟出操作结果。
QEMU在主机的用户态模拟虚拟机的硬件设备,vCPU对硬件的操作结果会在用户态进行模拟,如虚拟机需要将数据写入硬盘,实际结果是将数据写入主机中的一个镜像文件中。
虚拟机的内存空间被映射到QEMU的进程地址空间,在启动时分配。在虚拟机看来,QEMU所分配的主机上的虚拟地址空间为虚拟机的物理地址空间。
更详细的demo代码如下,从中可以很清晰的看出qemu-kvm的初始化以及虚拟机的运行过程。原文见参考资料链接,这里记录阅读过程中的一些批注和总结。
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <inttypes.h>
#include <pthread.h>
#include <sys/mman.h>
#include <linux/kvm.h>
#include <linux/errno.h>
#define KVM_API_VERSION 12
#define RAM_SIZE 128000000
#define VCPU_ID 0
#define DPRINTF(fmt, ...) \
do { fprintf(stderr, fmt, ## __VA_ARGS__); } while (0)
// accel/kvm/kvm-all.c KVMState
struct KVMState {
int fd;
int vmfd;
};
// include/sysemu/kvm_int.h KVMSlot
typedef struct KVMSlot
{
uint64_t start_addr;
uint64_t memory_size;
void *ram;
int slot;
int flags;
} KVMSlot;
// include/qom/cpu.h CPUState
// target/i386/cpu.h X86CPU
typedef struct CPUState {
int kvm_fd;
struct kvm_run *kvm_run;
} X86CPU;
struct KVMState *kvm_state;
// target/i386/kvm.c kvm_put_sregs
static int kvm_put_sregs(X86CPU *cpu) {
struct kvm_sregs sregs;
if (ioctl(cpu->kvm_fd, KVM_GET_SREGS, &sregs) < 0) {
fprintf(stderr, "KVM_GET_SREGS failed\n");
exit(1);
}
sregs.cs.base = 0x1000;
if (ioctl(cpu->kvm_fd, KVM_SET_SREGS, &sregs) < 0) {
fprintf(stderr, "KVM_SET_SREGS failed\n");
exit(1);
}
}
// target/i386/kvm.c kvm_getput_regs
static int kvm_getput_regs(X86CPU *cpu, int set) {
if(set) {
struct kvm_regs regs;
regs.rflags = 0x2;
if (ioctl(cpu->kvm_fd, KVM_SET_REGS, ®s) < 0) {
fprintf(stderr, "KVM_SET_REGS failed\n");
exit(1);
}
}
}
// target/i386/kvm.c kvm_arch_put_registers
int kvm_arch_put_registers(struct CPUState *cpu) {
int ret = 0;
kvm_put_sregs(cpu);
kvm_getput_regs(cpu, 1);
return ret;
}
/********************************************************************/
/*kvm-all*/
/********************************************************************/
// accel/kvm/kvm-all.c kvm_init_vcpu
int kvm_init_vcpu(struct CPUState *cpu) {
int ret = 0;
long mmap_size;
// ### 6. 创建vcpu,并为vCPU分配内存空间。
cpu->kvm_fd = ioctl(kvm_state->vmfd, KVM_CREATE_VCPU, VCPU_ID);
if (cpu->kvm_fd < 0) {
fprintf(stderr, "kvm_create_vcpu failed\n");
ret = -1;
goto err;
}
// ### 获取kvm为vCPU分配的内存空间
mmap_size = ioctl(kvm_state->fd, KVM_GET_VCPU_MMAP_SIZE, 0);
if (mmap_size < 0) {
ret = mmap_size;
fprintf(stderr, "KVM_GET_VCPU_MMAP_SIZE failed\n");
goto err;
}
// ### 将内存空间映射到用户态
cpu->kvm_run = mmap(NULL, mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED,
cpu->kvm_fd, 0);
if (cpu->kvm_run == MAP_FAILED) {
ret = -1;
fprintf(stderr, "mmap'ing vcpu state failed\n");
goto err;
}
return ret;
err:
if (cpu->kvm_fd >= 0) {
close(cpu->kvm_fd);
}
return ret;
}
// accel/kvm/kvm-all.c kvm_cpu_exec
int kvm_cpu_exec(struct CPUState *cpu)
{
struct kvm_run *run = cpu->kvm_run;
int ret, run_ret;
kvm_arch_put_registers(cpu);
/*线程进入循环,并捕获虚拟机退出原因,做相应的处理。这里的退出并不一定是虚拟机关机,虚拟机如果遇到IO操作,访问硬件设备,缺页中断等都会退出执行,
退出执行可以理解为将CPU执行上下文返回到QEMU。如果内核态的KVM不能处理就会交给应用层软件处理
*/
do{
sleep(1);
DPRINTF("start KVM_RUN\n");
// ### 7. 在vCPU上运行虚拟机,内存中已经有了vm的镜像,可以运行guest os
run_ret = ioctl(cpu->kvm_fd, KVM_RUN, 0);
if (run_ret < 0) {
fprintf(stderr, "error: kvm run failed %s\n",
strerror(-run_ret));
ret = -1;
break;
}
// ### 8. vm exit处理
switch (run->exit_reason) {
case KVM_EXIT_IO:
DPRINTF("handle_io\n");
DPRINTF("out port: %d, data: %d\n",
run->io.port,
*(int *)((char *)run + run->io.data_offset));
ret = 0;
break;
case KVM_EXIT_MMIO:
DPRINTF("handle_mmio\n");
ret = 0;
break;
case KVM_EXIT_IRQ_WINDOW_OPEN:
DPRINTF("irq_window_open\n");
ret = -1;
break;
case KVM_EXIT_SHUTDOWN:
DPRINTF("shutdown\n");
ret = -1;
break;
case KVM_EXIT_UNKNOWN:
fprintf(stderr, "KVM: unknown exit, hardware reason %" PRIx64 "\n",
(uint64_t)run->hw.hardware_exit_reason);
ret = -1;
break;
case KVM_EXIT_INTERNAL_ERROR:
DPRINTF("internal_error\n");
break;
case KVM_EXIT_SYSTEM_EVENT:
DPRINTF("system_event\n");
break;
default:
DPRINTF("kvm_arch_handle_exit\n");
break;
}
}while (ret == 0);
return ret;
}
// accel/kvm/kvm-all.c kvm_destroy_vcpu
int kvm_destroy_vcpu(struct CPUState *cpu) {
int ret = 0;
long mmap_size;
mmap_size = ioctl(kvm_state->fd, KVM_GET_VCPU_MMAP_SIZE, 0);
if (mmap_size < 0) {
ret = mmap_size;
fprintf(stderr, "KVM_GET_VCPU_MMAP_SIZE failed\n");
goto err;
}
ret = munmap(cpu->kvm_run, mmap_size);
if (ret < 0) {
goto err;
}
err:
close(cpu->kvm_fd);
return ret;
}
// vl.c main ->
// cccel/accel.c configure_accelerator -> accel_init_machine ->
// accel/kvm/kvm-all.c init_machine -> kvm_init
static int kvm_init() {
int ret;
//open /dev/kvm
// ### 1. 获取到kvm句柄
kvm_state->fd = open("/dev/kvm", O_RDWR);
if (kvm_state->fd < 0) {
fprintf(stderr, "Could not access KVM kernel module\n");
return -1;
}
//check api version
// ### 2. 获取kvm版本号,从而使应用层知道相关接口在内核是否支持
if (ioctl(kvm_state->fd, KVM_GET_API_VERSION, 0) != KVM_API_VERSION) {
fprintf(stderr, "kvm version not supported\n");
return -1;
}
//create vm
do {
// ### 3. 创建虚拟机,记录句柄
ret = ioctl(kvm_state->fd, KVM_CREATE_VM, 0);
} while (ret == -EINTR);
if (ret < 0) {
fprintf(stderr, "ioctl(KVM_CREATE_VM) failed: %d %s\n", -ret,
strerror(-ret));
return -1;
}
kvm_state->vmfd = ret;
}
// accel/kvm/kvm-all.c kvm_set_user_memory_region
static int kvm_set_user_memory_region(KVMSlot *slot) {
int ret = 0;
// ### 4. 为虚拟机映射内存,主要建立guest物理地址空间中的内存区域与qemu-kvm虚拟地址空间中的内存区域的映射,从而建立其从GPA到HVA的对应关系
struct kvm_userspace_memory_region mem;
mem.flags = slot->flags;
mem.slot = slot->slot;
mem.guest_phys_addr = slot->start_addr;
mem.memory_size = slot->memory_size;
mem.userspace_addr = (unsigned long)slot->ram;
ret = ioctl(kvm_state->vmfd, KVM_SET_USER_MEMORY_REGION, &mem);
return ret;
}
/********************************************************************/
/*cpus*/
/********************************************************************/
// cpus.c qemu_kvm_cpu_thread_fn
static void *qemu_kvm_cpu_thread_fn(void *arg)
{
int ret = 0;
struct CPUState *cpu = arg;
ret = kvm_init_vcpu(cpu);
if (ret < 0) {
fprintf(stderr, "kvm_init_vcpu failed: %s", strerror(-ret));
exit(1);
}
kvm_cpu_exec(cpu);
kvm_destroy_vcpu(cpu);
}
// cpus.c qemu_kvm_start_vcpu
void qemu_kvm_start_vcpu(struct CPUState *vcpu) {
pthread_t vcpu_thread;
// ### 9. 创建多个线程运行虚拟机,这里只有一个线程
if (pthread_create(&(vcpu_thread), (const pthread_attr_t *)NULL,
qemu_kvm_cpu_thread_fn, vcpu) != 0) {
fprintf(stderr, "can not create kvm cpu thread\n");
exit(1);
}
pthread_join(vcpu_thread, NULL);
}
// hw/i386/pc_piix.c DEFINE_I440FX_MACHINE -> pc_init1 ->
// hw/i386/pc.c pc_cpus_init -> pc_new_cpu ->
// target/i386/cpu.c x86_cpu_realizefn ->
// cpus.c qemu_init_vcpu
void qemu_init_vcpu(struct CPUState *cpu) {
qemu_kvm_start_vcpu(cpu);
}
/********************************************************************/
/*main*/
/********************************************************************/
// hw/core/loader.c rom_add_file
int rom_add_file(uint64_t ram_start, uint64_t ram_size, char *file) {
int ret = 0;
int fd = open(file, O_RDONLY);
if (fd == -1) {
fprintf(stderr, "Could not open option rom '%s'\n", file);
ret = -1;
goto err;
}
int datasize = lseek(fd, 0, SEEK_END);
if (datasize == -1) {
fprintf(stderr, "rom: file %-20s: get size error\n", file);
ret = -1;
goto err;
}
if (datasize > ram_size) {
fprintf(stderr, "rom: file %-20s: datasize=%d > ramsize=%zd)\n",
file, datasize, ram_size);
ret = -1;
goto err;
}
lseek(fd, 0, SEEK_SET);
// ### 5. 将虚拟机镜像映射到内存,相当于物理机boot的过程将镜像映射到内存
// ### file 是镜像文件,将文件内容读到内存的开始位置
int rc = read(fd, ram_start, datasize);
if (rc != datasize) {
fprintf(stderr, "rom: file %-20s: read error: rc=%d (expected %zd)\n",
file, rc, datasize);
ret = -1;
goto err;
}
err:
if (fd != -1)
close(fd);
return ret;
}
int mem_init(struct KVMSlot *slot, char *file) {
slot->ram = mmap(NULL, slot->memory_size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE,
-1, 0);
if ((void *)slot->ram == MAP_FAILED) {
fprintf(stderr, "mmap vm ram failed\n");
return -1;
}
//set vm's mem region
if (kvm_set_user_memory_region(slot) < 0) {
fprintf(stderr, "set user memory region failed\n");
return -1;
}
//load binary to vm's ram
if (rom_add_file((uint64_t)slot->ram, slot->memory_size, file) < 0) {
fprintf(stderr, "load rom file failed\n");
return -1;
}
}
int main(int argc, char **argv) {
kvm_state = malloc(sizeof(struct KVMState));
struct CPUState *vcpu = malloc(sizeof(struct CPUState));
struct KVMSlot *slot = malloc(sizeof(struct KVMSlot));
slot->memory_size = RAM_SIZE;
slot->start_addr = 0;
slot->slot = 0;
kvm_init();
mem_init(slot, argv[1]);
qemu_init_vcpu(vcpu);
munmap((void *)slot->ram, slot->memory_size);
close(kvm_state->vmfd);
close(kvm_state->fd);
free(slot);
free(vcpu);
free(kvm_state);
}
主要流程(细节见上面代码):
// ### 1. 获取到kvm句柄
kvm_state->fd = open("/dev/kvm", O_RDWR);
// ### 2. 获取kvm版本号,从而使应用层知道相关接口在内核是否支持
ioctl(kvm_state->fd, KVM_GET_API_VERSION, 0)
// ### 3. 创建虚拟机,记录句柄
ret = ioctl(kvm_state->fd, KVM_CREATE_VM, 0);
kvm_state->vmfd = ret;
// ### 4. 为虚拟机映射内存
struct kvm_userspace_memory_region mem;
ret = ioctl(kvm_state->vmfd, KVM_SET_USER_MEMORY_REGION, &mem);
// ### 5. 将虚拟机镜像映射到内存,相当于物理机boot的过程将镜像映射到内存
// ### file 是镜像文件,将文件内容读到内存的开始位置
int fd = open(file, O_RDONLY);
int rc = read(fd, ram_start, datasize);
// ### 6. 创建vcpe,并为vCPU分配内存空间。
cpu->kvm_fd = ioctl(kvm_state->vmfd, KVM_CREATE_VCPU, VCPU_ID);
// ### 获取kvm为vCPU分配的内存空间
mmap_size = ioctl(kvm_state->fd, KVM_GET_VCPU_MMAP_SIZE, 0);
// ### 将内存空间映射到用户态
cpu->kvm_run = mmap(NULL, mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED,
cpu->kvm_fd, 0);
// ### 7. 循环在vCPU上运行虚拟机,内存中已经有了vm的镜像,可以运行guest os
run_ret = ioctl(cpu->kvm_fd, KVM_RUN, 0);
内存虚拟化
kvm虚拟机实际运行于qemu-kvm的进程上下文中,因此,需要建立虚拟机的物理内存空间(GPA)与qemu-kvm进程的虚拟地址空间(HVA)的映射关系。
QEMU初始化时调用KVM接口告知KVM,虚拟机所需要的物理内存,通过mmap分配宿主机的虚拟内存空间作为虚拟机的物理内存,QEMU在更新内存布局时会持续调用KVM通知内核KVM模块虚拟机的内存分布。
在CPU支持EPT(拓展页表)后,CPU会自动完成虚拟机物理地址到宿主机物理地址的转换。虚拟机第一次访问内存的时候会陷入KVM,KVM逐渐建立起EPT页面。这样后续的虚拟机的虚拟CPU访问虚拟机虚拟内存地址时,会先被转换为虚拟机物理地址,接着查找EPT表,获取宿主机物理地址


参考资料:
https://cloud.tencent.com/developer/article/1087121
https://www.anquanke.com/post/id/221268
网友评论