x86架构CPU虚拟化GeraldJ.Popek和RobertP.Goldberg在1974年发表了论文《FormalRequirementsforVirtualizable[A1][A2]ThirdGenerationArchitectures”,提出了一种虚拟化有三种虚拟化的条件:(1)等价性,即VMM需要为宿主机上的虚拟机模拟一个与物理机本质上一致的环境。在此环境中运行的虚拟机与在物理机上运行是一样的,只是由于资源竞争或VMM干预,虚拟环境中的性能可能会略有差异,例如虚拟机的I/O,网络等由于宿主机限速或多个虚拟机共享资源,所以速度可能会比独享物理机慢。(2)效率,即虚拟机指令执行的性能与在物理机上运行相比没有明显损失。该标准要求虚拟机中的大部分指令直接在物理CPU上运行,无需VMM干预。比如我们在x86架构上通过Qemu运行的ARM系统,并不是虚拟化,而是仿真(Emulator)。(3)资源控制,即VMM完全控制系统资源。VMM控制和协调宿主机的资源给各个虚拟机,但是宿主机的资源不能被虚拟机控制。TrapandEmulateModel为了满足GeraldJ提出的虚拟化的三个条件。Popek和RobertP.Goldberg提出的典型解决方案是TrapandEmulate模型。一般来说,处理器可以分为两种操作模式:系统模式和用户模式。相应地,CPU指令也分为特权指令和非特权指令。特权指令只能在系统模式下运行。如果特权指令在用户模式下运行,它将触发处理器异常。操作系统将内核运行在系统模式下,因为内核需要管理系统资源和运行特权指令,而普通的用户程序运行在用户模式下。在虚拟化场景下,虚拟机的用户程序仍然运行在用户态,但是虚拟机的内核运行在用户态。这种方法称为环压缩。这样,虚拟机中的非特权指令直接运行在处理器上,满足了Popek和Goldberg提出的虚拟化标准的高效要求,即大部分指令直接运行在处理器上,无需VMM干涉。但是,当虚拟机执行特权指令时,由于特权指令是在用户态执行的,会触发处理器异常,从而落入VMM,VMM会代理虚拟机完成访问系统资源,这称为仿真。这样也满足了Popek和Goldberg提出的虚拟化标准中VMM控制系统资源的要求,虚拟机不会修改宿主机的资源,因为它可以直接运行特权指令,从而破坏环境的主机。x86架构虚拟化的障碍GeraldJ.Popek和RobertP.Goldberg指出,那些修改系统资源或在不同模式下表现不同的指令都是敏感指令。在虚拟化场景中,VMM需要监控这些敏感指令。支持虚拟化的架构的敏感指令都是特权指令,即当这些敏感指令在非特权级别执行时,CPU会抛出异常,进入VMM的异常处理函数,从而达到目的控制VM对敏感资源的访问。然而,x86架构恰恰不能满足GeraldJ.Popek和RobertP.Goldberg定义的标准,并不是所有的敏感指令都是特权指令,一些敏感指令在非特权模式下执行时不会抛出异常。此时VMM无法拦截或处理VM的行为。以修改FLAGS寄存器中的IF(中断标志)为例,我们首先使用指令pushfd将寄存器FLAGS的内容压入栈,然后将栈顶的IF清0,最后使用popf指令从堆栈中恢复FLAGS寄存器。如果虚拟机内核运行在ring1,x86CPU不会抛出异常,只是默默忽略popfd指令,所以关闭虚拟机IF的目的并没有生效。有人提出了一种半虚拟化的方法,即修改Guest代码,但这不符合虚拟化的透明准则。后来人们又提出了二进制翻译的方法,包括静态翻译和动态翻译。静态翻译是在运行前扫描整个可执行文件,翻译敏感指令,重新组成一个新文件。静态翻译有其局限性,必须提前处理,而且有些指令有副作用,只在运行时发生,无法静态处理。因此,动态翻译应运而生,即在运行时以代码块为单位对二进制代码进行动态修改。很多VMM都用到了动态翻译,优化效果很好。VMX扩展虽然程序员从软件层面采用了多种方案来解决x86架构的虚拟化问题,但是软件层的方案除了额外的开销之外,也给VMM的实现带来了巨大的复杂性。因此,英特尔试图从硬件层面解决这个问题。Intel不会将那些非特权敏感指令修改为特权指令,因为并不是所有的特权指令都需要Trap和Emulate。让我们举一个典型的例子。每当操作系统内核切换进程时,都会切换cr3寄存器,使其指向当前运行进程的页表。当使用影子页表从GVA映射到HPA时,需要捕获每一次设置Guest的cr3寄存器的操作,VMM模块使其指向影子页表。当启用硬件级别的EPT支持时,cr3仍然指向Guest进程页表,不需要捕获Guest设置cr3寄存器的操作。也就是说,写cr3寄存器虽然是特权指令,但不需要落入VMM。Intel开发了VT技术来支持虚拟化,为CPU添加虚拟机扩展(Virtual-MachineExtensions,简称VMX)。一旦启用CPU的VMX支持,CPU将提供2种运行模式:VMXRootMode和VMXnon-RootMode,每种模式都支持ring0~ring3。VMM运行在VMXRootMode下,VMXRootMode除了支持VMX外,与普通模式没有本质区别。VM运行在VMXnon-RootMode,Guest不需要使用RingCompression方式。Guest内核可以直接运行在VMXnon-RootMode的ring0上,如图1所示。图1VMX运行模式VMXRootMode下的VMM可以通过执行CPU提供的虚拟化命令VMLaunch切换到VMXnon-RootMode.因为这个过程相当于进入Guest[3],所以通常称为VM进入。当Guest内部执行敏感指令,比如某些I/O操作时,会触发CPU陷入陷阱,从VMXnon-RootMode切换到VMXRootMode。这个过程相当于退出了VM,所以也叫VMexit。.然后VMM会模拟Guest的操作。相对于RingCompression方式,即Guest内核也运行在用户态(ring1~ring3),支持VMX扩展的CPU[4]:(1)当运行在Guest模式时,系统调用Guest用户空间的直接落入Guest模式的内核空间,而不是落入Host模式的内核空间。(2)对于外部中断,由于VMM需要控制系统的资源,Guest模式的CPU收到外部中断,触发CPU从Guest模式退出到Host模式,Host内核处理外部中断.处理完中断后,再次切换到Guest模式。为了提高I/O效率,Intel支持外设透传模式。在这种模式下,Guest不需要生成VM出口。“设备虚拟化”一章将讨论这种特殊方法。(3)并不是所有的特权指令都会导致Guest模式下的CPU退出VM,只有在运行敏感指令时才会导致CPU从Guest模式掉到Host模式,因为有些特权指令不需要VMM处理。正如一个CPU可以同时运行多个任务,每个任务都有自己的上下文,调度器在调度时切换上下文,这样同一个CPU就可以同时运行多个任务。在VMX扩展下,同一个物理CPU“一人多用”,分时运行Host和Guest,并根据需要在不同模式间切换。因此,不同的模式也需要保存自己的上下文。为此VMX设计了一个数据结构来保存上下文:VMCS。每个来宾都有一个VMCS实例。当物理CPU加载不同的VMCS时,会运行不同的Guest,如图2所示。图2多个Guest之间的切换VMCS主要存储两类数据,一类是status,包括Host和Guest,另一类是to控制Guest运行时的行为。(1)Guest-statearea,即保存虚拟机状态的区域。当VM退出时,Guest的状态保存在该区域;当VM进入时,这些状态将被加载到CPU中。这些是硬件级别的自动行为,VMM不需要编码干预。(2)Host-statearea,保存宿主机状态的区域。当发生VM退出时,CPU会自动将这些状态从VMCS加载到物理CPU;当有VM进入时,CPU会自动将状态保存到这个区域。(3)VM-退出信息字段。当虚拟机出现VM退出时,VMM需要知道VM退出的原因,才能对症下药,进行相应的模拟操作。为此,CPU会自动将Guest退出的原因保存在该区域,以供VMM使用。(4)VM-执行控制字段。该区域的各个字段控制了虚拟机运行时的一些行为,比如设置运行时Guest访问cr3时是否触发VM退出;控制VM进入和退出行为的VM-entry控制字段和VM-exitcontrol字段。具体的我们就不一一列举了,有需要的读者可以参考Intel的手册。在创建VCPU时,KVM模块会为每个VCPU申请一个VMCS,每次CPU准备切换到Guest模式时,都会设置自己的VMCS指针指向要切换的Guest对应的VMCS实例在:commit6aa8b732ca01c3d7a54e93f4d701b8aabbe60fb7[PATCH]kvm:userspaceinterfacelinux。git/drivers/kvm/vmx.cstaticstructkvm_vcpu*vmx_vcpu_load(structkvm_vcpu*vcpu){u64phys_addr=__pa(vcpu->vmcs);intcpu;cpu=get_cpu();…if(per_cpu(current_vmcs,cpu)!=vcpu->vmcs){...per_cpu(current_vmcs,cpu)=vcpu->vmcs;asmvolatile(ASM_VMX_VMPTRLD_RAX";setna%0":"=g"(错误):"a"(&phys_addr),"m"(phys_addr):"cc");…}…}并不是所有状态都由CPU自动保存和恢复,我们还需要考虑效率。以cr2寄存器为例。大多数时候,从Guest退出到Host重新进入Guest这段时间,Host不会改变cr2寄存器的值,写入cr2的开销不小。如果每次创建VM条目时都更新cr2,除了浪费CPU指令周期外,毫无意义。因此,把这些状态交给VMM,由软件自己来控制比较合适。VCPU生命周期对于每个虚拟处理器(VCPU),VMM使用一个线程来表示VCPU实体。Guest运行过程中,各个VCPU基本处于图1-3所示的状态。图3VCPU生命周期用户空间就绪后,VCPU所在线程向内核中的KVM模块发送ioctl请求KVM_RUN,通知内核中的KVM模块用户空间的操作已经完成,guest可以切换成Guest模式运行。进入内核模式后,KVM模块会调用CPU提供的虚拟化指令进入Guest模式。如果是第一次运行Guest,使用VMLaunch命令,否则使用VMResume命令。在这个切换过程中,首先CPU的状态,也就是Host的状态,会被保存在VMCS中存放Host状态的区域,没有的状态CPU自动保存的是KVM自己保存的。然后,将存储在VMCS中的Guest的状态加载到物理CPU中,没有被CPU自动恢复的状态由KVM自己恢复。物理CPU切换到Guest模式并运行Guest指令。当执行Guest命令,遇到敏感命令时,CPU会从Guest模式切换到Host模式的ring0,进入Host内核的KVM模块。在这个切换过程中,首先会将CPU的状态,也就是Guest的状态,保存到VMCS中存放Guest状态的区域,然后再将Host的状态保存到VMCS将加载到物理CPU。同样,CPU不自动保存的状态,由KVM模块自己保存。内核态的KVM模块从VMCS中读取虚拟机退出的原因,并尝试在内核中进行处理。如果可以在内核中处理,那么虚拟机就不需要切换到Host模式的用户态。处理完后可以直接切换回Guest。此出口也称为轻量级虚拟机出口。如果内核态的KVM模块无法处理虚拟机的退出,那么VCPU会再次进行上下文切换,从Host的内核态切换到Host的用户态,用户空间部分VMM将处理它。VMM用户空间处理完成后,再次发起切换到Guest模式的指令。这个过程在虚拟机的整个运行过程中不断重复。下面是切入切出内核空间的代码:commit6aa8b732ca01c3d7a54e93f4d701b8aabbe60fb7[PATCH]kvm:userspaceinterfacelinux.git/drivers/kvm/vmx.cstaticintvmx_vcpu_run(structkvm_vcpu*vcpu,…){sulds_el,u1_ed_need;…tfs_run/*Enterguestmode*/"jnelaunched\n\t"ASM_VMX_VMLAUNCH"\n\t""jmpkvm_vmx_return\n\t""已启动:"ASM_VMX_VMRESUME"\n\t"".globlkvm_vmx_return\n\t""kvm_vmx_return:"/*Saveguestregisters,loadhostregisters,keepflags*/…if(kvm_handle_exit(kvm_run,vcpu)){…gotoagain;}}return0;}从Guest退出时,KVM模块首先调用函数kvm_handle_exit尝试在内核空间HandleGuest出口。函数kvm_handle_exit有一个协议。如果在内核空间可以成功处理虚拟机退出,或者因为外部中断等其他干扰导致虚拟机退出等,则不需要切换到Host的用户空间,则返回1;否则返回0,说明KVM需要帮助进程的用户空间部分处理虚拟机的退出,比如模拟设备需要KVM用户空间处理外设请求。如果内核空间成功处理了虚拟机的退出,函数kvm_handle_exit返回1,我们看到上面的代码又直接跳转到了标号处,然后程序流又会切入Guest。这种类型的虚拟机出口称为轻量级虚拟机出口。如果函数kvm_handle_exit返回0,则函数vmx_vcpu_run执行结束,CPU从内核空间返回到用户空间。以kvmtool为例,相关代码片段如下:代码来看,kvmtool发起进入Guest的代码是在一个无限for循环中。从KVM内核空间回到用户空间后,kvmtool在用户空间处理Guest请求,比如调用模拟设备处理I/O请求。处理完Guest的请求后,重新进入下一轮for循环,kvmtool再次请求KVM模块切换到Guest。王百胜,高级技术专家,先后就职于中科院软件所、红旗Linux、百度,现任百度首席架构师。曾在操作系统、虚拟化技术、分布式系统、云计算、自动驾驶等相关领域工作多年,具有丰富的实践经验。畅销书《深度探索Linux操作系统》的作者(2013年出版)。谢广军博士计算机专业,毕业于南开大学计算机系。IT行业多年工作经验的资深技术专家。现任百度智能云副总经理,负责云计算相关产品的研发工作。多年从事操作系统、虚拟化技术、分布式系统、大数据、云计算等相关领域的研发,具有丰富的实践经验。本文内容节选自《深度探索Linux虚拟化技术》,已获得机械工业出版社华章公司授权。本文转载自微信公众号“Linux代码阅读领域”,可通过以下二维码关注。转载本文,请联系Linux代码阅读领域。
