共计 3435 个字符,预计需要花费 9 分钟才能阅读完成。
QEMU和KVM的关系
我们已经知道QEMU是作为用户态软件模拟IO,而KVM负责完成CPU、内存等虚拟化,为什么常常把KVM和QEMU联系起来,因为KVM诞生之初就是使用的QEMU作为用户态的设备模拟软件,其实Xen也可以使用QEMU作为用户态组件来实现设备的模拟。
KVM在内核中通过/dev/kvm给用户态的软件,提供了一系列的接口,例如用户态创建、配置、启动虚拟机等,在设备模拟的部分,KVM创立之初就重用了QEMU的设备模拟的部分,从本质上来讲,QEMU和KVM是完全可以不用相互依赖的。举个例子,来展示QEMU和KVM之间的关系,这个例子包括精简版内核,这个内核功能简单,作用只是向I/O端口写入数据;第二部分可以看作是一个精简版的QEMU,它的功能也很简单,就是使用上述内核创建虚拟机,打印出精简内核的端口数据。
精简内核代码
精简内核代码如下:作用是将”SCU HELLO”字符串输出到I/O端口0xf1上,然后调用hlt指令使CPU进入停机状态,直到有中断或者复位信号发生才会继续执行。
test.S 代码如下:
start:
mov $0x53,%al
outb %al,$Oxf1
mov $0x43,%al
outb %al,$Oxf1
mov $0x55,%al
outb %al,$0xf1
mov $0x20,%al
outb %al,$Oxf1
mov $0x48,%al
outb %al,$Oxf1
mov $0x45, %al
outb %al,$Oxf1
mov $0x4c, %al
outb %al,$Oxf1
mov $0x4c, %al
outb %al,$Oxf1
mov $0x4f, %al
outb %al,$Oxf1
mov $0x0a, %al
outb %al,$Oxf1
hlt
说明:
mov $0x48,%al
outb %al,$Oxf1
这是两条x86汇编语言指令,它们的作用是将十六进制数0x48输出到I/O端口0xf1上。具体来说,第一条指令mov $0x48,%al
的作用已经在上一个问题中解释过了,它将立即数0x48移动到AL寄存器中。第二条指令outb %al,$0xf1
则是将AL寄存器中的值输出到I/O端口0xf1上。outb是x86汇编语言中的一个输出指令,它的作用是将一个字节(8位)的数据输出到指定的I/O端口。
使用如下命令进行编译:
as -32 test.S -o test.o
objcopy -O binary test.o test.bin
第一个命令as -32 test.S -o test.o
是使用GNU汇编器(as)将汇编代码文件test.S
汇编成目标文件(object file)test.o
。其中,-32
选项指定生成32位目标文件,-o
选项指定输出文件名为test.o
。
第二个命令objcopy -O binary test.o test.bin
是使用GNU二进制文件操作工具(objcopy)将目标文件test.o
转换为二进制文件test.bin
。其中,-O binary
选项指定输出文件格式为二进制文件,test.o
为输入文件名,test.bin
为输出文件名。
精简版QEMU代码
qemu.c代码如下:
#include <stddef.h>
#include <stdio.h>
#include <linux/kvm.h>
#include <fcntl.h>
#include <sys/mman.h>
int main()
{
struct kvm_sregs sregs;
int ret;
//通过打开/edv/kvm获取系统中KVM子系统的文件描述符
int kvmfd = open ("/dev/kvm",O_RDWR);
//保持应用层与内核统一,获取KVM版本,应用层可以知晓KVM的支持情况
ioctl(kvmfd, KVM_GET_API_VERSION,NULL);
// 创建虚拟机,返回一个代表虚拟机的文件描述符
int vmfd = ioctl(kvmfd, KVM_CREATE_VM,0);
// 给虚拟机分配物理内存,虚拟机的物理内存是QEMU的位于进程地址空间,这段内存大小为0x1000 4KB
unsigned char *ram = mmap(NULL,0x1000,PROT_READ | PROT_WRITE,MAP_SHARED | MAP_ANONYMOUS,-1,0);
//将精简内核读入内存
int kfd = open("test.bin",O_RDONLY);
read(kfd,ram,4096);
struct kvm_userspace_memory_region mem = {
//slot用来表示不同的内存空间
.slot = 0,
//表示这段空间在虚拟机物理内存空间的地址
.guest_phys_addr = 0,
//表示这段物理空间的大小
.memory_size = 0x1000,
//表示这段物理空间对应宿主机上的虚拟机地址 也就是GPA->HVA
.userspace_addr = (unsigned long) ram,
};
//为虚拟机分配内存,一个内存条
ret = ioctl(vmfd, KVM_SET_USER_MEMORY_REGION,&mem);
//创建vCPU
int vcpufd = ioctl(vmfd,KVM_CREATE_VCPU,0);
//每一个vCPU都有一个struct kvm_run的结构,用来在用户态(qemu)和内核态(kvm)之间共享数据。
//用户态需要将这段空间映射到用户空间
int mmap_size = ioctl(kvmfd, KVM_GET_VCPU_MMAP_SIZE, NULL);
struct kvm_run *run = mmap(NULL,mmap_size, PROT_READ |PROT_WRITE,MAP_SHARED,vcpufd, 0);
//设置VCPU相关的寄存器,sregs存放的如段寄存器、控制寄存器等特殊寄存器。
ret = ioctl(vcpufd, KVM_GET_SREGS, &sregs);
//设置代码段其实地址为0,
sregs.cs.base = 0;
sregs.cs.selector = 0;
ret = ioctl(vcpufd,KVM_SET_SREGS, &sregs);
//设置通用寄存器,如指令指针寄存器为0,这样就直接从精简内核第一行代码开始执行
struct kvm_regs regs ={
.rip =0,
};
ret = ioctl(vcpufd,KVM_SET_REGS, &regs);
while (1)
{
//将VCPU调度到物理CPU上运行
ret = ioctl(vcpufd, KVM_RUN, NULL);
if( ret == -1)
{
printf("exit unknown\n");
return -1;
}
//在VCPU运行过程中遇到敏感指令会退出,KVM不能处理会交给用户态的Qemu处理,此时ioctl系统调用就会返回,并且将一些信息保存到kvm_run结构中
//这样用户程序就可以分析虚拟机退出的原因,然后根据原因进行处理。
switch (run->exit_reason){
//本例中,精简内核执行hlt会产生KVM_EXIT_HLT推出事件。
case KVM_EXIT_HLT:
puts("KVM_EXIT_HLT");
return 0;
case KVM_EXIT_IO:
putchar(*(((char *)run)+run->io.data_offset));
break;
case KVM_EXIT_FAIL_ENTRY:
puts("entry error");
return -1;
default:
puts("other error");
printf("exit_reason: %dln", run->exit_reason);
return -1;
}
}
}
编译执行:
root@test-ubuntu-no-vgpu:~# gcc qemu.c -o light-qemu
root@test-ubuntu-no-vgpu:~# ./light-qemu
SCU HELLO
KVM_EXIT_HLT
总结
由上述代码可以看出,KVM通过一组ioctl系统调用,向用户空间暴露了接口,这些接口用于创建虚拟机、设置内存、创建VCPU,调度运行等。
参考文献
- QEMU/KVM源码解析与应用-李强