如何计算虚拟化vcpu_虚拟化技术之QEMU与KVM交互过程详解
上一期,我们详细介绍了QEMU-KVM的交互通道IOCTL,这一“里应外合”的交互方式有如下好处:1、策略与机制分离,加速的机制由KVM负责,而如何调用加速的机制由Qemu负责;2、可以由Qemu设置多种内存模型,如UMA、NUMA等等;3、方便Qemu对特殊内存的管理(如MMIO);4、内存的分配、回收、换出等都可以采用Linux原有的机制,不需要为KVM单独开发;5、兼容其他加速...
上一期,我们详细介绍了QEMU-KVM的交互通道IOCTL,这一“里应外合”的交互方式有如下好处:
1、策略与机制分离,加速的机制由KVM负责,而如何调用加速的机制由Qemu负责;
2、可以由Qemu设置多种内存模型,如UMA、NUMA等等;
3、方便Qemu对特殊内存的管理(如MMIO);
4、内存的分配、回收、换出等都可以采用Linux原有的机制,不需要为KVM单独开发;
5、兼容其他加速器模型(或者无加速器,单纯使用Qemu做模拟)。
但是,我们脑子里总是有一系列的问题,比如,到底这两者是怎么互动,才能创建、加载和运行一个虚拟机的。因为什么事情,只有落实到了代码层面,才能心里有底。这一期,我们将通过分析代码,详解一下。下面,先上几张图,对QEMU-KVM有个感性认识。

图一

图二

图三

图四
上面几张图,从不同的侧面反映了QEMU-KVM的体系结构,侧重点不同。
有没有看晕?我还没醉。下面,总结一下qemu-kvm软件的架构特点:
1、Kvm本身只提供两个内核模块。Kvm实现了vcpu和内存的管理;
2、Qemu控制逻辑,负责创建虚拟机,创建vcpu等。
在详细介绍,kvm提供了三种通过不同的io_ctl接口来控制的概念:
1、struct kvm:代表kvm模块本身,用来管理kvm版本信息,创建一个vm;
2、struct vm:代表一个虚拟机。通过vm的io_ctl接口,可以为虚拟机创建vcpu,设置内存区间,创建中断控制芯片,分配中断等等;
3、struct vcpu:代表一个vcpu。通过vcpu的io_ctl接口,可以启动或者暂停vcpu,设置vcpu的寄存器,为vcpu注入中断等等。
首先,定义一个简单地虚拟机需运行代码:
mov $0x3f8, %dxadd %bl, %aladd $'0', %alout %al, (%dx)mov $'\n', %alout %al, (%dx)hlt
这段代码比较简单,也就是先将al和bl寄存器的值相加(初始默认值均为2),结果转换后,输出至0x3f8端口,最后停机。然后,我们通过gcc和objdump将上述二进制代码转换为机器码,内容如下:
constuint8_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 */};
需要指出的是,运行这段代码需要CPU"unrestricted guest"特性支持。
下面,简略叙述一下QEMU、KVM的交互过程。
先定义并初始化几个变量:
/* 向KVM注册用户态内存空间,也即向虚拟机添加“物理内存” */ /* 注意该“物理内存”是qemu进程向host申请的用户态内存 */struct kvm_userspace_memory_region region = { .slot = 0, .guest_phys_addr = 0x1000, .memory_size = 0x1000, .userspace_addr = (uint64_t)mem,}; /* 通用寄存器信息初始化,此处可以看到a、b寄存器值初始化为2 */struct kvm_regs regs = { .rip = 0x1000, .rax = 2, .rbx = 2, .rflags = 0x2,}; /* cs段寄存器信息初始化 */sregs.cs.base = 0;sregs.cs.selector = 0;
运行过程如下:
void main(){ /* 打开kvm控制的总设备文件/dev/kvm */ kvm = open("/dev/kvm", O_RDWR | O_CLOEXEC); /* 检查API版本信息,检测ret值 */ 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); /* 获取页对齐且初始化为0的一个内存页 */ mem = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0); /* 将前述二进制代码拷贝至该页内 */ memcpy(mem, code, sizeof(code)); /* 将二进制页赋予虚拟机 */ /* 此时,虚拟机将其当做物理内存,且只有一个slot */ ioctl(vmfd, KVM_SET_USER_MEMORY_REGION, ®ion); /* 创建VCPU,且每个VCPU关联一个struct kvm_run结构体 */ vcpufd = ioctl(vmfd, KVM_CREATE_VCPU, (unsigned long)0); /* 计算需要kernel和用户空间共享的struct kvm_run结构体大小 */ mmap_size = ioctl(kvm, KVM_GET_VCPU_MMAP_SIZE, NULL); /* 执行共享struct kvm_run结构体*/ run = mmap(NULL, mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED, vcpufd, 0); /* 初始化虚拟机的VCPU寄存器信息,准备运行 */ ioctl(vcpufd, KVM_GET_SREGS, &sregs); ioctl(vcpufd, KVM_SET_REGS, ®s); /* 运行 */ while (1) { /* 进入运行 */ ioctl(vcpufd, KVM_RUN, NULL); /* 退出处理 */ 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:printf("error\n"); break; } } }
上面已经简略地注释了各行代码。我们大体上搞明白了创建、加载、运行虚拟机的基本流程。
但还有很多问题没说呢?比如,struct kvm_run结构体为什么要共享、同步?那一页slot内存是怎么给虚拟机的?到底CPU的VT技术是怎么支持KVM的?一系列的问号啊!!!
下期精彩继续!!!
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)