- QEMU/KVM源码解析与应用
- 李强编著
- 1535字
- 2021-08-24 11:53:37
1.3 KVM API使用实例
前面提到KVM导出了一系列接口供用户态创建、配置、启动虚拟机,典型的用户态软件是QEMU。之所以将QEMU和KVM经常联系起来,是因为KVM创立之初重用了QEMU的设备模拟部分,本质上来说,QEMU和KVM可以不必相互依赖。本节将以一个非常简单的例子展示QEMU和KVM的关系。这个例子包括两个部分,第一部分是一个精简版内核,这个内核非常简单,它的任务仅仅是向I/O端口写入数据。第二部分可以看作是一个精简版的QEMU,它的任务也非常简单,就是将上述精简内核的端口数据打印出来。
精简的内核代码如下:向端口0xf1写入Hello字符串,然后调用hlt指令。
使用如下命令编译上述汇编代码。
精简版的QEMU代码如下。
编译并执行该文件。
可以看到这个名为light-qemu的精简版QEMU输出了精简版内核向端口写入的数据。下面对light-qemu程序进行简单介绍。
KVM通过一组ioctl向用户空间导出接口,这些接口能够用于虚拟机的创建、虚拟机内存的设置、虚拟机VCPU的创建与运行等。按照接口所使用的文件描述符(file descriptor,fd)不同,KVM的这组ioctl接口可以分为三类:
1)系统全局的ioctl,这类ioctl的作用对象是KVM模块本身,比如一些全局的配置项,创建虚拟机的ioctl也在此例。
2)虚拟机相关的ioctl,这类ioctl的作用对象是一台虚拟机,比如设置虚拟机的内存布局、创建虚拟机VCPU也在此例。
3)虚拟机VCPU相关的ioctl,这类ioctl的作用对象是一个虚拟机的VCPU,比如说开始虚拟机VCPU的运行。
在light-qemu中,首先通过打开“/dev/kvm”获取系统中KVM子系统的文件描述符kvmfd,为了保持应用层和内核的统一,可以通过ioctl(KVM_GET_API_VERSION)获取KVM的版本号,从而使应用层知道相关接口在内核是否有支持。
接着在kvmfd上面调用ioctl(KVM_CREATE_VM)创建一个虚拟机,该ioctl返回一个代表虚拟机的文件描述符vmfd。这代表了一个完整的虚拟机系统,可以通过vmfd控制虚拟机的内存、VCPU等。内存是一个计算机必不可少的组件,所以在创建了虚拟机之后,需要给虚拟机分配物理内存,虚拟机的物理内存对应QEMU的进程地址空间,这里使用mmap系统调用分配了一页虚拟机内存,并且将精简版的内核代码读入了这段空间中。之后用分配的虚拟内存地址初始化kvm_userspace_memory_region对象,然后调用ioctl(KVM_SET_USER_MEMORY_REGION),这就为虚拟机指定了一个内存条。其中slot用来表示不同的内存空间,guest_phys_addr表示这段空间在虚拟机物理内存空间的位置,memory_size表示这段物理空间的大小,userspace_addr则表示这段物理空间对应在宿主机上的虚拟机地址。
设置好虚拟机的内存后,light-qemu接着在vmfd上调用ioclt(KVM_CREATE_VCPU)来创建虚拟机VCPU。每一个VCPU都有一个struct kvm_run结构,用来在用户态(这里是light-qemu)和内核态(KVM)共享数据。用户态程序需要将这段空间映射到用户空间,为此首先调用ioctl(KVM_GET_VCPU_MMAP_SIZE)得到这个结构的大小,注意这里是对kvmfd调用ioctl,因为这个对KVM所有VCPU都是一样的。接着调用mmap,将kvm_run映射到了用户态空间。
为了让虚拟机VCPU运行起来,需要设置VCPU的相关寄存器,其中段寄存器和控制寄存器等特殊寄存器存放在kvm_sregs,通过ioctl(KVM_GET_SREGS)读取和修改,通用寄存器存放在结构kvm_regs中,通过ioctl(KVM_SET_REGS)读取和修改。最重要的是设置CS和IP寄存器,light-qemu都将其设置为0,由于代码加在了虚拟机物理地址0处,所以虚拟机VCPU运行的时候直接从地址0处取指令开始运行。
至此,一个简单的虚拟机和虚拟机VCPU、内存都已经准备完毕,寄存器也设置好了,这个时候可以让虚拟机运行起来了。通常在一个循环中对vcpufd调用ioctl(KVM_RUN)。内核在处理这个ioctl时会把VCPU调度到物理CPU上运行,VCPU在运行过程中遇到一些敏感指令时会退出,如果内核态的KVM不能处理就会交给应用层软件处理,此时ioctl系统调用返回,并且将一些信息保存到kvm_run,这样用户态程序就能够知道导致虚拟机退出的原因,然后根据原因进行相应的处理。在这个例子中,虚拟机内核向端口写数据会产生KVM_EXIT_IO的退出,表示虚拟机内部读写了端口,在输出了端口数据之后让虚拟机继续执行,执行到最后一个hlt指令时,会产生KVM_EXIT_HLT类型的退出,此时虚拟机运行结束。
当然,与精简的QEMU和精简的内核相比,实际的QEMU和实际的操作系统的内核复杂度都远远超过这个水平,但是其基本原理都是类似的。