虚拟机创建完成后,QEMU便可以通过前述虚拟机文件描述符发起系统调用,请求KVM创建vCPU,完整的创建流程如图2-12所示。
在QEMU/KVM中,每个vCPU对应宿主机操作系统中的一个线程,由QEMU创建,其执行函数为qemu_kvm_cpu_thread_fn,该函数将调用kvm_init_vcpu函数进而调用kvm_get_vcpu函数发起KVM_CREATE_VCPU ioctl,其代码如下。同虚拟机创建一样,KVM会为每个vCPU创建一个vCPU文件描述符返回给QEMU,然后QEMU发起KVM_GET_VCPU_MMAP_SIZE查看QEMU与KVM共享内存空间的大小。其中共享内存空间的第一个页将被映射到KVM vCPU结构体struct kvm_vcpu的run成员,该成员将保存一些VM-Exit相关信息,如VM-Exit的原因以及部分虚拟机寄存器状态等,便于QEMU处理VM-Exit。kvm_init_vcpu函数然后调用mmap函数将其映射到QEMU的虚拟地址空间中。
图2-12 QEMU/KVM vCPU创建的函数调用流程
注:①QEMU进行ioctl系统调用。
qemu-4.1.1/accel/kvm/kvm_all.c
QEMU发起KVM_CREATE_VCPU ioctl后将陷入KVM模块进行处理,处理函数为kvm_vm_ioctl_create_vcpu。kvm_vm_ioctl_create_vcpu函数首先调用kvm_arch_vcpu_create函数创建vCPU对应的结构体,然后调用create_vcpu_fd函数进而调用anon_inode_getfd为vCPU创建对应的文件描述符,对应的file_operations为kvm_vcpu_fops。kvm_vcpu_fops定义了vCPU文件描述符对应的ioctl系统调用的处理函数,该函数为kvm_vcpu_ioctl。主要代码如下。
linux-4.19.0/virt/kvm/kvm_main.c
值得注意的是,kvm_arch_vcpu_create函数除了创建vCPU对应的结构体以外,还完成了vCPU相应的VMCS初始化工作。前面提到每个vCPU都有一个VMCS与其对应,使用时需要执行VMPTRLD指令将其与物理CPU绑定。kvm_arch_vcpu_create函数通过前述kvm_x86_ops的vcpu_create成员调用vmx.c中的vmx_create_vcpu函数。vmx_create_vcpu函数调用alloc_loaded_vmcs函数进而调用alloc_vmcs_cpu函数为vCPU分配VMCS结构,然后调用vmx_vcpu_load函数进而调用vmcs_load函数执行VMPTRLD指令将VMCS与当前CPU绑定,相关代码如下。
linux-4.19.0/arch/x86/kvm/vmx.c
绑定之后,vmx_create_vcpu函数调用vmx_vcpu_setup函数通过若干vmcs_write16/vmcs_write32/vmcs_write64函数进而调用__vmcs_writel函数设置VMCS中相关域的值,如前所述,vmcs_writexx函数最终会执行VMWRITE指令写VMCS,相关代码如下。
linux-4.19.0/arch/x86/kvm/vmx.c
vmcs_setup_config函数主要设置VMCS中控制域的值,客户机状态域和宿主机状态域的初始化工作则由kvm_vm_ioctl_create_vcpu函数调用kvm_arch_vcpu_setup函数进而调用kvm_vcpu_reset函数完成。kvm_vcpu_reset函数通过kvm_x86_ops的vcpu_reset成员调用vmx.c中的vmx_vcpu_reset函数完成,部分代码如下。
linux-4.19.0/arch/x86/kvm/vmx.c
根据上述代码,KVM会调用vmcs_writel函数将VMCS中客户机的代码段寄存器(GUEST_CS_SELECTOR)设置为0xf000,将代码段基地址(GUEST_CS_BASE)设置为0xffff0000。而kvm_rip_write函数则会将KVM模拟的虚拟RIP(Return Instruction Pointer,返回指令指针)寄存器设为0xfff0,当后续调用vmx_vcpu_run函数运行vCPU时,该函数会将模拟的RIP寄存器值写入VMCS中,部分代码如下。
linux-4.19.0/arch/x86/kvm/vmx.c
这与Intel x86架构硬件要求相符。在Intel x86架构下,计算机加电后会将CS(Code Segment,代码段)寄存器设置为0xf000,RIP寄存器设置为0xfff0,而CS寄存器中隐含的代码段基地址则设置为0xffff0000,这样当程序启动时,执行的第一条指令位于0xfffffff0处(CS_BASE+RIP) 。值得注意的是,这里KVM仅仅是对VMCS中的客户机状态域进行初始化,QEMU仍可以通过KVM API设置VMCS客户机状态域。QEMU在调用kvm_cpu_exec函数运行虚拟机代码前,首先调用kvm_arch_put_registers函数,该函数将会调用kvm_getput_regs函数和kvm_put_sregs函数,通过前述vCPU设备文件描述符发起KVM_SET_REGS和KVM_SET_SREGS ioctl,分别设置vCPU通用寄存器和段寄存器的值,KVM会进而将QEMU传入的寄存器值写入VMCS中。具体寄存器的值则由x86_cpu_reset函数指定,相关代码如下。
qemu-4.1.1/target/i386/cpu.c
qemu-4.1.1/target/i386/kvm.c
根据上述代码,QEMU传入的CS和RIP寄存器的值与默认VMCS相应域的设置相同:CS=0xf000,RIP=0xfff0,CS_BASE=0xffff0000。这里通过一个小实验验证上述代码的有效性。QEMU提供了-s和-S选项允许GDB远程连接调试内核,其中-s选项使得QEMU等待来自1234端口的TCP连接,-S选项则使得QEMU阻塞客户机执行,直到远程连接的GDB(Linux下常用的程序调试器)允许它继续执行,这允许用户方便地在GDB中查看虚拟机运行过程中物理寄存器的状态。可以打开两个终端,终端1(Terminal 1)启动QEMU等待GDB连接,终端2(Terminal 2)则运行GDB通过1234端口远程连接至QEMU,命令如下。
Terminal 1
Terminal 2
根据上述实验,vCPU启动后,CS寄存器和RIP寄存器的值与QEMU设置的一致,说明虚拟机启动时硬件会将VMCS中相应域加载到寄存器中。而为了兼容早期8086架构,程序启动后,0xffff0和0xfffffff0内存处指令一致,都是跳转至BIOS进行初始化,加载bootloader。在0xfffffff0处设置断点,运行虚拟机后发现断点被触发;而删去断点后,虚拟机正常启动,说明程序的入口位于0xfffffff0处。改写后的x86_cpu_reset函数代码如下。
qemu-4.1.1/target/i386/cpu.c
如上所述,将CS寄存器设置为0x1234,将RIP寄存器设置为0x5678,重新编译QEMU并运行上述代码,实验结果如下。
Terminal 3
从GDB输出可以发现上述寄存器设置生效,感兴趣的读者可以自行修改QEMU target/i386/cpu.c中的x86_cpu_reset函数,重复上述实验。以上便是QEMU/KVM vCPU创建与初始化流程,与虚拟机创建类似,vCPU创建的起点实际上位于TYPE_X86_CPU类型的QOM对象的realize_fn成员中。该成员在CPU对应的QOM对象初始化时被设置为x86_cpu_realizefn函数,该函数调用qemu_init_vcpu函数创建了vCPU线程,线程执行函数为前述的qemu_kvm_cpu_thread_fn。