现代计算机系统是一个庞大的整体,整个系统的复杂性是不言而喻的。因而,计算机系统被分成自下而上的多个层次。每一个层次都向上一层次呈现一个抽象,并且每一层只需知道下一层抽象的接口,不需要了解其内部运作机制。本质上,虚拟化就是由位于下一层的软件模块,通过向上一层软件模块提供一个与它原先所期待的运行环境完全一致的接口的方法,抽象出一个虚拟的软件或硬件接口,使得上层软件可以直接运行在虚拟的环境上。
首先介绍虚拟化中两个重要的名词:宿主和客户。在虚拟化中,物理资源通常有一个定语,称为宿主(Host),而虚拟出来的资源通常有一个定语,称为客户(Guest)。根据资源的不同,这两个名词的后面可以接不同的名词。例如,如果是将一个物理计算机虚拟为一个或多个虚拟计算机,则这个物理计算机通常被称为宿主机(Host Machine),而其上运行的虚拟机被称为客户机(Guest Machine)。宿主机上如果运行有操作系统,则该操作系统通常被称为宿主机操作系统(Host OS),而虚拟机中运行的操作系统被称为客户机操作系统(Guest OS)。
系统虚拟化是指将一个物理计算机系统虚拟化为一个或多个虚拟计算机系统。每个虚拟计算机系统(简称为虚拟机)都拥有自己的虚拟硬件(如CPU、内存和设备等),来提供一个独立的虚拟机执行环境。通过虚拟化层的模拟,虚拟机中的操作系统认为自己仍然独占系统硬件在运行。每个虚拟机中的操作系统可以完全不同,并且它们的执行环境是完全独立的。这个虚拟化层被称为虚拟机监控器(Virtual Machine Monitor,VMM),如图2-1所示。
图2-1 系统虚拟化
从本质上来说,虚拟计算机系统和物理计算机系统可以是两个完全不同的ISA(指令集架构)的系统。例如,可以在一个x86物理计算机上运行一个安腾的虚拟计算机。但是,不同的ISA使得虚拟机的每一条指令都需要在物理机上模拟执行,从而造成性能上的极大降低。当然,相同体系结构的系统虚拟化通常会使VMM实现起来比较简单,也有比较好的性能。虚拟机的大部分指令可以在处理器上直接运行,只有那些需要虚拟化的指令才会由VMM进行处理。
1974年,杰拉尔德·J.波佩克(Gerald J. Popek)和罗伯特·P.戈德堡(Robert P. Goldberg)提出可以将虚拟机看作物理机的一种高效隔离的复制。上面的定义蕴涵了三层含义(同质、高效和资源受控),这也是虚拟机所具有的三个典型特征。
●同质,是指虚拟机的运行环境和物理机的运行环境在本质上需要是相同的,但是在表现上可以有一些差异。例如,虚拟机所看到的处理器个数可以和物理机上实际的处理器个数不同,处理器主频也可以与物理机不同,但是物理机上的处理器和虚拟机中的处理器必须是同一种类型。
●高效,是指虚拟机中运行的软件需要具备在物理机上直接运行的性能。为了做到这一点,软件在虚拟机中运行时,大多数的指令要直接在硬件上执行,只有少量指令需要经过VMM处理或模拟。
●资源受控,是指VMM需要对系统资源有完全控制能力和管理权限,包括资源的分配、监控和回收。
传统的虚拟化技术一般是通过“陷入再模拟”的方式实现的,而这种方式依赖于处理器的支持。也就是说,处理器本身是否是一个可虚拟化架构决定了虚拟化实现的方式,那么什么是可虚拟化架构(Virtualizable Architecture)呢?
一般来说,虚拟环境由三个部分组成,即硬件、VMM和虚拟机,如图2-2所示。在没有虚拟化的情况下,操作系统直接运行在硬件上,管理着底层物理硬件,这就构成了一个完整的计算机系统,也就是所谓的物理机。在虚拟环境里,虚拟机监控器抢占了操作系统的位置,变成了真实物理硬件的管理者,同时向上层的软件呈现出虚拟的硬件平台,“欺骗”着上层的操作系统。而此时操作系统运行在虚拟平台上,仍然管理着它认为是“物理硬件”的虚拟硬件,不知道下面发生了什么,这就是图2-2中的虚拟机。
图2-2 虚拟环境的组成
或许硬件体系结构会有所限制,或许VMM的实现方式会有所不同,但如果虚拟机不具备2.1.1节介绍的三个典型特征,那么这个虚拟机是失败的,VMM的“骗术”是不高明的。
虚拟机的三个典型特征也决定了不是任何系统都是可虚拟化的。给定一个系统,其对应的体系结构是否可虚拟化,要看能否在该系统上虚拟化出具备上述三个典型特征的虚拟机。
为了进一步研究可虚拟化的条件,先从指令开始介绍。大多数的现代计算机体系结构都有两个或两个以上的特权级,用来分隔系统软件和应用软件。系统中有一些操作和管理关键系统资源的指令会被定为特权指令(Privileged Instruction),这些指令只有在最高特权级上才能够正确执行。如果在非最高特权级上运行特权指令,会引发一个异常,处理器会陷入最高特权级,交由系统软件来处理。在不同的运行级上,不仅指令的执行效果是不同的,而且也并不是每个特权指令都会引发异常。假如一个x86平台的用户违反了规范,在用户态修改EFLAGS寄存器的中断开关位,这一修改将不会产生任何效果,也不会引起异常陷入,而是会被硬件直接忽略掉。
在虚拟化世界里,还有一类指令,被称为敏感指令(Sensitive Instruction),简言之就是操作特权资源的指令,包括:修改虚拟机的运行模式或者物理机的状态;读写敏感的寄存器或内存,例如时钟或者中断寄存器;访问存储保护系统、内存系统或地址重定位系统以及所有的I/O指令。
显而易见,所有的特权指令都是敏感指令,然而并不是所有的敏感指令都是特权指令。
VMM为了完全控制系统资源,它不允许直接执行虚拟机上操作系统(即客户机操作系统)的敏感指令。也就是说,敏感指令必须在VMM的监控审查下进行,或者经由VMM来完成。如果一个系统上所有敏感指令都是特权指令,则能用一个很简单的方法来实现虚拟环境:将VMM运行在系统的最高特权级上,而将客户机操作系统运行在非最高特权级上,当客户机操作系统因执行敏感指令(此时,也就是特权指令)而陷入VMM时,VMM模拟执行引起异常的敏感指令,这种方法被称为陷入再模拟。
总而言之,判断一个架构是否可虚拟化,其核心就在于该架构对敏感指令的支持。如果在某个架构上所有敏感指令都是特权指令,则它是可虚拟化架构;否则,如果它无法支持在所有的敏感指令上触发异常,则它不是一个可虚拟化架构,我们称其存在“虚拟化漏洞”。
我们已经知道,通过陷入再模拟敏感指令的执行来实现虚拟机的方法是有前提条件的:所有的敏感指令必须都是特权指令。否则,要么系统的控制信息会被虚拟机修改或访问,要么VMM会遗漏需要模拟的操作,影响虚拟化的正确性。如果一个架构存在敏感指令不属于特权指令,那么它就存在虚拟化漏洞。有些计算机架构是存在虚拟化漏洞的,就是说它们不能很高效地支持系统虚拟化。
虽然虚拟化漏洞有可能存在,但是可以用一些方法来填补或避免这些漏洞。最简单、最直接的方法是,如果所有虚拟化都通过模拟来实现,例如解释执行,即取一条指令,模拟出这条指令执行的效果,继续取下一条指令,那么就不存在所谓陷入不陷入的问题,从而避免了虚拟化漏洞。这种方法不但适用于模拟与物理机具有相同体系结构的虚拟机,而且能模拟不同体系结构的虚拟机。虽然这种方法保证了所有指令(包括敏感指令)执行受到VMM的监督审查,但是它不会对每条指令区别对待,其最大的缺点是性能太差,是不符合虚拟机“高效”特点的,导致其性能下降为原来的十分之一甚至几十分之一。
既要填补虚拟化漏洞,又要保证虚拟化的性能,只能采取一些辅助的手段。可以直接在硬件层面填补虚拟化漏洞,也可以通过软件的方法避免虚拟机中使用无法陷入的敏感指令。这些方法都不仅保证了敏感指令的执行受到VMM的监督审查,而且保证了非敏感指令可以不经过VMM而直接执行,从而相比解释执行来说,性能得到了极大的提高。
从以上对可虚拟化架构的分析不难看出,某些处理器在设计之初并没有充分考虑虚拟化的需求,不具备一个完备的可虚拟化架构。如何填补这些架构上的缺陷直接促成了三种主要虚拟化方式的产生,即基于软件的完全虚拟化(Full Virtualization)、硬件辅助虚拟化(Hardware Assisted Virtualization)和半虚拟化(Paravirtualization)。不论采取何种虚拟化方式,VMM对物理资源的虚拟可以归结为三种主要类型:处理器虚拟化、内存虚拟化和I/O虚拟化。
1.处理器虚拟化
处理器虚拟化是VMM中最核心的部分,因为访问内存或者I/O的指令本身就是敏感指令,所以内存虚拟化与I/O虚拟化都依赖于处理器虚拟化的正确实现。
VMM运行在最高特权级,可以控制物理处理器上的所有关键资源;而客户机操作系统运行在非最高特权级,所以其敏感指令会陷入VMM中通过软件的方式进行模拟。处理器虚拟化的关键在于正确模拟指令的行为。当VMM接管物理处理器后,客户机操作系统试图访问关键资源的指令就成为敏感指令,VMM会通过各种手段保证这些敏感指令的执行能够触发异常,从而陷入VMM进行模拟,VMM会通过准确模拟物理处理器的行为,而将其访问定位到与物理寄存器对应的虚拟的寄存器上。虚拟寄存器往往实现为VMM内存中的数据结构。VMM同时负责虚拟处理器的调度和切换,以保证在给定时间内,每个虚拟处理器上的当前进程可以在物理处理器上运行一段时间。但凡切换必然涉及保留现场,这个现场就是上下文状态。VMM只有保存和恢复好上下文,才能让虚拟机看起来好像从未被中断过。既然谈及虚拟处理器,那么什么是虚拟处理器呢?虚拟处理器的本质是其需要模拟完成的一组功能集合。虚拟处理器的功能可以由物理处理器和VMM共同完成。对于非敏感指令,物理处理器直接解码处理其请求,并将相关的效果直接反映到物理寄存器上;而对于敏感指令,VMM负责陷入再模拟,从程序的角度来看就是一组数据结构与相关处理代码的集合。数据结构用于存储虚拟寄存器的内容,而相关处理代码负责按照物理处理器的行为将结果反映到虚拟寄存器上。至此,我们基本了解了处理器虚拟化,其宗旨就是让虚拟机里执行的敏感指令陷入下来之后能被VMM模拟,而不要直接作用于真实硬件上。
当然,模拟的前提是能够陷入,VMM陷入利用了处理器的保护机制,并利用中断和异常来完成,它有以下几种方式。
●基于处理器保护机制触发的异常,处理器会在执行敏感指令之前,检查其执行条件是否满足,一旦特权级别、运行模式或内存映射(Memory Mapping)关系等条件不满足,VMM得到陷入然后进行处理。
●虚拟机主动触发异常,也就是通常所说的陷阱。当条件满足时,处理器会在触发陷阱的指令执行完毕后,再抛出一个异常。虚拟机可以通过陷阱指令来主动请求陷入VMM中去。
●异步中断,包括处理器内部的中断源和外部的设备中断源。这些中断源可以是周期性产生中断的时间源,也可以是根据设备状态产生中断的大多数外设。一旦中断信号到达处理器,处理器会强行中断当前指令,然后跳到VMM注册的中断服务程序。
中断和异常机制是处理器提供给系统程序的重要功能,异常保证了系统程序对处理器关键资源的绝对控制,而中断提供了系统程序与外设之间更有效的一种交互方式。所以,VMM在实现处理器虚拟化时,必须正确模拟中断与异常的行为。
VMM对于异常的虚拟化需要完全遵照物理处理器对于各种异常条件的定义,再根据虚拟处理器当时的内容来判断是否需要模拟出一个虚拟的异常,并将其注入虚拟环境中。VMM通常会在硬件异常处理程序和指令模拟代码中进行异常虚拟化的检查。无论是哪一条路径,VMM需要区分两种原因:一是虚拟机自身对运行环境和上下文的设置违背了指令正确执行的条件;二是虚拟机运行在非最高特权级别,由于虚拟化的原因触发的异常。
虚拟中断的触发来自虚拟设备的模拟程序。当设备模拟器发现虚拟设备状态满足中断产生的条件时,会将这个虚拟中断通知给中断控制器的模拟程序。然后,VMM会在特定的时候检测虚拟中断控制器的状态,来决定是否模拟一个中断的注入。这里的虚拟中断源包括:处理器内部中断源的模拟、外部虚拟设备的模拟、直接分配给虚拟机使用的真实设备的中断和自定义的中断类型。
不管怎样,当VMM决定向虚拟机注入一个中断或异常时,它需要严格模拟物理处理器的行为来改变客户指令流的路径,而且要包括一些必需的上下文保护与恢复。VMM需要首先判断当前虚拟机的执行环境是否允许接受中断或异常的注入,假如客户机操作系统正好禁止了中断的发生,这时VMM就只能把中断事件先暂时缓存起来,直到某个时刻客户机操作系统重新允许了中断的发生,VMM才立即切入来模拟一个中断的注入。当中断事件不能被及时注入时,VMM还要进一步考虑中断合并、中断取消、同类型中断大量阻塞的情况以及正确性与效率等方面的因素。
总起来说,中断/异常的虚拟化由中断/异常源的定义、中断/异常源与VMM处理器虚拟化模块间的交互机制以及最终模拟注入的过程所组成。虚拟机与VMM通过中断和异常发生的交互机制如图2-3所示。
在CPU虚拟化方面,Intel VT提供了面向x86的英特尔虚拟化技术(Intel Virtualization Technology for x86,简称Intel VT-x)。Intel VT中的VT-x技术扩展了传统的IA32处理器架构,为IA32架构的处理器虚拟化提供了硬件支持。VT-x架构的基本思想如图2-4所示。
图2-3 虚拟机与VMM通过中断和异常发生的交互机制
图2-4 VT-x架构的基本思想
首先,VT-x引入了两种操作模式,统称为VMX操作模式。
●根操作模式(VMX Root Operation):VMM运行所处的模式,以下简称为根模式。
●非根操作模式(VMX Non-Root Operation):客户机运行所处的模式,以下简称为非根模式。
这两种操作模式与IA32特权级0~特权级3是正交的,即每种操作模式下都有相应的特权级0~特权级3。
VT-x中,非根模式下敏感指令引起的“陷入”被称为VM Exit。VM Exit发生时,CPU自动从非根模式切换为根模式。相应地,VT-x也定义了VM Entry,该操作由VMM发起,通常是调度某个客户机运行,此时CPU从根模式切换为非根模式。
其次,为了更好地支持CPU虚拟化,VT-x引入了虚拟机控制结构(Virtual-Machine Control Structure,VMCS)。VMCS保存虚拟CPU需要的相关状态,例如CPU在根模式和非根模式下的特权寄存器的值。VMCS主要供CPU使用,CPU在发生VM Exit和VM Entry时都会自动查询和更新VMCS。VMM可以通过指令来配置VMCS,进而影响CPU的行为。
最后,VT-x还引入了一组新的指令,VMLAUNCH/VMRESUME用于发起VM-Entry,VMREAD/VMWRITE用于配置VMCS等。
在进行CPU虚拟化时,使用vCPU描述符来描述虚拟CPU(Virtual CPU),它本质上是一个结构体,通常由以下几部分构成。
●vCPU标识信息:用于标识vCPU的一些属性,例如vCPU的ID号、vCPU属于哪个客户机等。
●虚拟寄存器信息:虚拟的寄存器资源,在使用Intel VT-x的情况下,这些内容包含在VMCS中,例如客户机状态域保存的内容。
●vCPU状态信息:类似于进程的状态信息,标识该vCPU当前所处的状态,例如睡眠、运行等,主要供调度器使用。
●额外寄存器/部件信息:主要指未包含在VMCS中的一些寄存器或CPU部件,例如浮点寄存器和虚拟的LAPIC等。
●其他信息:用于VMM进行优化或存储额外信息的字段,例如存放该vCPU私有数据的指针等。
由此可见,在Intel VT-x情况下,vCPU可以划分成两个部分:一个是以VMCS为主由硬件使用和更新的部分,主要指虚拟寄存器;另一个是除VMCS之外,由VMM使用和更新的部分,主要指VMCS以外的部分。图2-5展示了vCPU的构成。
当VMM创建客户机时,首先要为客户机创建vCPU,整个客户机的运行实际上是VMM调度不同的vCPU运行。
图2-5 vCPU的构成
创建vCPU实际上是创建vCPU描述符,由于本质上vCPU描述符是一个结构体,因此创建vCPU描述符简单来说就是分配相应大小的内存。
vCPU描述符被创建之后,需要初始化才能使用。物理CPU在上电之后,硬件会自动将CPU初始化为特定的状态。vCPU的初始化也是一个类似的过程,将vCPU描述符的各个部分置成可用的状态。通常初始化包含如下内容。
●分配vCPU标识:首先要标识该vCPU属于哪个客户机,再为该vCPU分配一个在客户机范围内唯一的标识。
●初始化虚拟寄存器组:主要指初始化VMCS相关域。这些寄存器的初始化值通常是根据物理CPU上电后各寄存器的值设定的。
●初始化vCPU状态信息:设置vCPU在被调度前需要配置的必要标志,具体情况依据调度器的实现决定。
●初始化额外部件:将未被VMCS包含的虚拟寄存器初始化为物理CPU上电后的值,并配置虚拟LAPIC等部件。
●初始化其他信息:根据VMM的实现初始化vCPU的私有数据。
vCPU被创建并初始化之后,就可以通过调度程序被调度运行。调度程序会根据一定的策略算法来选择vCPU,然后将vCPU切换到物理CPU上运行。前面提到,在Intel VT-x的支持下,vCPU的上下文可以分为两部分。故上下文的切换也分为硬件自动切换(VMCS部分)和VMM软件切换(非VMCS部分)两个部分。
图2-6描述了VT-x支持的CPU上下文切换的过程,可以归纳为下列几个步骤。
●VMM保存自己的上下文,主要是保存VMCS不保存的寄存器,即宿主机状态域以外的部分。
●VMM将保存在vCPU中由软件切换的上下文加载到物理CPU中。
●VMM执行VMRESUME/VMLAUNCH指令,触发VM-Entry,此时CPU自动将vCPU上下文中VMCS部分加载到物理CPU,CPU切换到非根模式。
图2-6 VM-Entry中的上下文切换
此时,物理CPU已经处于客户机的运行环境,rip/eip也指向了客户机的指令,这样vCPU就被成功调度并运行了。
当然,vCPU作为调度单位不可能永远运行,vCPU退出在VT-x中表现为发生VM-Exit。对vCPU退出的处理是VMM进行CPU虚拟化的核心,例如模拟各种特权指令。
图2-7描述了VMM处理vCPU退出的典型流程,可以归纳为下列几个步骤。
●发生VM Exit,CPU自动进行一部分上下文的切换。
●当CPU切换到根模式开始执行VM Exit的处理函数后,进行另一部分上下文的切换。
图2-7 VM Exit的处理
图2-7列举了一些较为典型的vCPU退出的原因。总起来说,vCPU退出的原因大体上有三类:访问了特权资源、引发了异常或发生了中断。
VMM在处理完vCPU的退出后,会负责将vCPU投入再运行。从VT-x的角度来看,需要额外考虑以下几点。
●如果vCPU继续在相同的物理CPU上运行,可以用VMRESUME来实现VM-Entry。VMRESUME比VMLAUNCH更轻量级,执行效率更高。
●如果由于某种原因(如负载均衡),vCPU被调度程序迁移到了另一个物理CPU上,那么VMM需要将vCPU对应的VMCS迁移到另一个物理CPU,这通常可以由一个IPI中断实现;迁移完成后,在重新绑定的物理CPU上执行VMLAUNCH发起VM-Entry。
由此可见,整个虚拟化的内容就是在VMM→客户机→VMM→……中完成的。这里再细化一下,客户机的顺利运行就是在vCPU运行→vCPU退出→vCPU再运行→……的过程中完成的。
2.内存虚拟化
在介绍完处理器虚拟化的基本原理之后,接着来看一下内存虚拟化。首先操作系统对物理内存有两个主要认识:物理地址从0开始和内存地址连续性。而内存虚拟化的产生主要源于VMM与客户机操作系统对物理内存的认识存在冲突,造成了物理内存的真正拥有者——VMM必须对客户机操作系统所访问的内存进行一定程度上的虚拟化。VMM的任务就是模拟使得虚拟出来的内存仍然符合客户机操作系统对内存的假定和认识。
因此,在虚拟环境里,内存虚拟化面临的问题是:物理内存要被多个客户机操作系统同时使用,但物理内存只有一份,物理起始地址0也只有一个,无法同时满足所有客户机操作系统内存从0开始的要求;由于使用内存分区方式,把物理内存分给多个客户机操作系统使用,客户机操作系统的内存连续性要求虽能得到解决,但是内存的使用效率非常低。在面临这些问题的情况下,VMM所要做的就是“欺骗”客户机操作系统,以满足客户机操作系统对内存的上述两点要求,这种欺骗过程就是内存虚拟化。
内存虚拟化的核心在于引入一层新的地址空间——客户机物理地址空间。
在图2-8中,VMM负责管理和分配每个虚拟机的物理内存,客户机操作系统所看到的是一个虚构的客户机物理地址空间,其指令目标地址也是一个客户机物理地址。这样的地址在无虚拟化的情况下,其实就是实际物理地址。但是,在有虚拟化的情况下,这样的地址是不能被直接发送到系统总线上去的,需要VMM负责将客户机物理地址转换成一个实际物理地址后,再交由物理处理器来执行。
图2-8 内存虚拟化示意图
值得一提的是,为了更有效地利用空闲的物理内存,尤其是系统长期运行后产生的碎片,VMM通常会以比较小的粒度(如4KB)进行分配,这就导致给定一个虚拟机的物理内存实际上是不连续的,其具体位置完全取决于VMM的内存分配算法。
由于引入了客户机物理地址空间,内存虚拟化就主要处理以下两个方面的问题。
●给定一个虚拟机,维护客户机物理地址到宿主机物理地址之间的映射关系。
●截获虚拟机对客户机物理地址的访问,并根据所记录的映射关系将其转换成宿主机物理地址。
第一个问题相对比较简单,因为只是一个数据结构的映射问题。在实现过程中,客户机操作系统采用客户页表维护该虚拟机里进程所使用的虚拟地址到客户机物理地址的动态映射关系,可以用一个简单的公式表示:GPA= f 1 (GVA)。其中,GVA是客户机虚拟地址(Guest Virtual Address),GPA是客户机物理地址(Guest Physical Address)。而VMM负责维护客户机物理地址到宿主机物理地址之间的动态映射关系,可以用一个简单的公式表示:HPA= f 2 (GPA)。这里,HPA是宿主机物理地址(Host Physical Address)。虚拟机里一个进程所使用的客户机虚拟地址要变成物理处理器可以执行的宿主机物理地址,需要经过两层转换,即HPA= f 2 ( f 1 (GVA))。
VMM内存虚拟化任务就是跟踪客户页表,当其发生变化时,及时地切入,构造一个有效的客户机虚拟地址到宿主机物理地址间的映射关系,并将其添加到物理处理器所遍历的真实页表上。
第二个问题从实现上来说更加复杂、更具有挑战性,也是衡量一个虚拟机的性能最重要的因素。另外,地址转换一定要发生在物理处理器处理目标指令之前,否则一旦客户机物理地址被直接发送到系统总线上,就会对整个虚拟环境,包括其他虚拟机以及VMM自身,造成严重的破坏和巨大的漏洞。
一个最简单的方法就是设法让虚拟机对客户机物理地址空间的每一次访问都触发异常,然后由VMM来查询地址转换表模拟其访问。这种方法的完备性和正确性没有任何问题,但是性能上绝对是最差的。为了解决这个问题,VT-x提供了扩展页表(Extended Page Table,EPT)技术,直接在硬件上支持GVA→GPA→HPA的两次地址转换,大大降低了内存虚拟化的难度,也进一步提高了内存虚拟化的性能。
3.I/O虚拟化
现实中的外设资源也是有限的,为了满足多个客户机操作系统的需求,VMM必须通过I/O虚拟化的方式来复用有限的外设资源。VMM截获客户操作系统对设备的访问请求,然后通过软件的方式来模拟真实设备。模拟软件本身作为物理驱动程序众多客户端中的一个,有效地实现了物理资源的复用。由于从处理器的角度看,外设是通过一组I/O资源(端口I/O或者内存映射I/O)来进行访问的,因此设备相关的虚拟化又被称为I/O虚拟化。基于设备类型的多样化,以及不同VMM所构建的虚拟环境上的差异,I/O虚拟化的方式和特点纷繁复杂,不一而足。
I/O端口、内存映射I/O(Memory-Mapped I/O,MMIO)与中断模块组成了一个典型外设呈现给软件的基本资源。I/O虚拟化并不需要完整地虚拟化出所有外设的所有接口,怎样做完全取决于设备与VMM的策略以及客户机操作系统的需求。其中主要涉及虚拟芯片组、虚拟PCI总线布局、虚拟系统设备(如PIC、IO-APIC、PIT和RTC等)以及虚拟基本输入输出设备(如显卡、网卡和硬盘等)。VMM提供某种方式,让客户机操作系统发现虚拟设备以加载相关的驱动程序,然后VMM设法截获客户机操作系统对虚拟设备的访问并进行模拟,这样完成I/O虚拟化。虚拟化完毕后,只要客户机操作系统中有驱动程序遵守该虚拟设备的接口定义,它就可以被客户机操作系统使用。
至于虚拟化方式,虚拟设备可以与物理设备具有完全相同的接口定义,从而允许客户机操作系统中的原有驱动程序无须修改就能驱动这个虚拟设备。在这种情况下,物理设备具备哪些资源,设备模拟器需要呈现出同样的资源。另一种方式是把客户机操作系统中特定的驱动程序进行简化,并将客户机操作系统中的驱动程序称为前端(Front-End,FE)设备驱动,而将VMM中的驱动程序称为后端(Back-End,BE)设备驱动。简化就是前端程序将来自其他模块的请求通过客户机之间的特殊通信机制直接发送给后端程序,而后端程序在处理完请求后再发回通知给前者。这种方式是基于请求/事务的,能在很大程度上减少上下文切换的频率,提供更大的优化空间,可以把这种方式看作上一种方式的衍生。还有一种方式,如果直接将物理设备分配给某个客户机操作系统,由客户机操作系统直接访问目标设备,VMM不需要为这种方式提供模拟,客户机操作系统中原有的驱动程序也可以无缝地操作目标设备,这种I/O虚拟化的方式从性能上来说是最优的。目前与此相关的技术有Intel VT-d和PCI组织制定的SR-IOV等。
本节主要针对x86架构阐述了虚拟化技术的基本原理,至于其他指令集架构,如Arm6 等,限于篇幅,在此不再赘述,感兴趣的读者可以参考相关资料。
前面提及了三种主要的虚拟化方式,下面来简要介绍三种不同方式的原理。
1.基于软件的完全虚拟化
我们知道,所有的虚拟化形式都可以用模拟技术来实现。在模拟技术中,最简单最直接的模拟技术是解释执行,即取一条指令出来,模拟出这条指令执行的效果,再继续取下一条指令,周而复始。由于是一条一条地取指令而不会漏过每一条指令,在某种程度上即每条指令都“陷入”了,所以解决了陷入再模拟的问题,进而避免了虚拟化漏洞。这种方法通常可以被用于不同体系结构的虚拟化中,也就是在一种硬件体系结构上模拟出另一种不同硬件体系结构的运行环境。而在同一种体系结构的模拟中,情况会变得更容易,因为大多数指令是不需要被模拟执行而直接放在真实的硬件上执行的,于是一条指令一条指令地解释执行在这里就没有必要了。可以采用改进的代码扫描与修补(scan-and-patch)技术和二进制代码翻译技术来完成CPU虚拟化,基于影子页表来完成内存虚拟化,以及借助设备模型并对其软件接口进行拦截和模拟来完成I/O虚拟化,从而尽可能地提高虚拟化的性能。
2.硬件辅助虚拟化
硬件辅助虚拟化技术,顾名思义,就是在CPU、芯片组及I/O设备等硬件中加入专门针对虚拟化技术的支持,使系统软件可以更容易、更高效地实现虚拟化功能。只要为硬件本身加入足够的虚拟化功能,就可以截获操作系统对敏感指令的执行或者对敏感资源的访问,从而通过异常的方式报告给VMM,这样就解决了虚拟化的问题。Intel的VT-x技术就是这一方向的代表。VT-x技术在处理器上引入了一个新的执行模式,用于运行虚拟机。当虚拟机运行在这个特殊模式中时,它仍然面对的是一套完整的处理器寄存器集合和执行环境,只是任何特权操作都会被处理器截获并报告给VMM。VMM本身运行在正常模式下,在接收到处理器的报告后,通过对目标指令的解码,找到对应的虚拟化模块进行模拟,并把最终的效果反映在特殊模式下的环境中。硬件虚拟化是一种完备的虚拟化方法,因为内存和外设的访问本身也由指令来承载,对处理器指令级别的截获就意味着VMM可以模拟一个与真实主机完全相同的环境。在这个环境中,任何操作系统只要能够在现实中的等同主机上运行,就可以在这个虚拟机环境中无缝地运行。
3.半虚拟化
在计算机硬件不能改变的情况下,VMM提供给虚拟机的硬件抽象是可以修改的。这里所说的硬件抽象,是指硬件平台掩去了内部的具体实现,暴露给软件的抽象接口。半虚拟化(Paravirtualization)技术的主要思想就是通过修改暴露给虚拟机的硬件抽象以及上层操作系统,使操作系统与VMM配合工作,避开虚拟化漏洞,从而实现系统虚拟化。半虚拟化技术修改了硬件抽象,操作系统也需要做相应的修改,这些修改主要包括指令集、外部中断、内存空间及内存管理方式、I/O设备驱动、时钟等。事实上,不同的半虚拟化系统为了实现不同的目的,对于硬件抽象层和操作系统的修改都是不同的。当然,虚拟硬件抽象与实际硬件的差别越大,客户机操作系统和应用程序需要做的修改也越多。从操作系统的角度来看,半虚拟化的硬件抽象是一种与x86架构有所不同的体系结构,因此,对操作系统的修改就是对这种体系结构进行移植的过程。修改后的操作系统能够意识到虚拟环境的存在。
通过1.2节的介绍,相信读者已经对VMM模型的分类有了初步的认识,这里进一步对各模型中模块的功能进行深入剖析。我们知道,根据虚拟化技术实现结构的不同,通常将虚拟机监控器分为以下两类。
1.原生型虚拟机监控器(Native Hypervisor)
在原生型虚拟机监控器中,VMM可以被看作一个完备的操作系统,不过和传统操作系统不同的是,VMM是为虚拟化而设计的,因此还具备虚拟化功能。从架构上来看,首先,所有的物理资源(如处理器、内存和I/O设备等)都归VMM所有,因此,VMM承担着管理物理资源的责任;其次,VMM需要向上提供虚拟机,用于运行客户机操作系统,因此,VMM还负责虚拟环境的创建和管理。此类型通常被称为一型虚拟机监控器(Type-1 Hypervisor),又被称为裸机虚拟机监控器(Bare-metal Hypervisor)。
图2-9展示了此类模型中较为典型的监控模型(Hypervisor Model)架构,其中处理器管理代码(Processor,P)负责物理处理器的管理和虚拟化,内存管理代码(Memory,M)负责物理内存的管理和虚拟化,设备模型(Device Model,DM)负责I/O设备的虚拟化,设备驱动(Device Driver,DR)则负责I/O设备的驱动,即物理设备的管理。VMM直接管理所有的物理资源,包括处理器、内存和I/O设备,因此,设备驱动是VMM的一部分。此外,处理器管理代码、内存管理代码和设备模型也是VMM的一部分。典型的产品为VMware ESXi 。
图2-9 监控模型架构
对于上述模型来说,设备驱动开发的工作量是一个很大的挑战。一种改进实现是,VMM会主动让出大部分I/O设备的控制权,把它们交由一个运行在特权虚拟机中的特权操作系统来控制。相应地,VMM虚拟化的职责也被分担。处理器和内存的虚拟化依然由VMM来完成,而I/O的虚拟化则由VMM和特权操作系统共同合作来完成。包含特权系统的原生型虚拟机监控器架构,由于其本质上仍属于一型虚拟机监控器,同时兼具后面即将介绍的另一类模型的某些特征,因此是两种模式的汇合体,我们权且将其称为混合模型。
图2-10展示了混合模型(Hybrid Model)的架构。在这一模型中,I/O设备由特权操作系统控制,因此,设备驱动模块位于特权操作系统内核空间中。其他物理资源的管理和虚拟化由VMM完成,因此,处理器管理代码和内存管理代码处在VMM中。I/O设备虚拟化由VMM和特权操作系统共同完成,因此,设备模型模块位于特权操作系统用户空间中,并且通过相应的通信机制与VMM合作。典型的产品有Microsoft Hyper-V8 、Xen9 。
2.宿主型虚拟机监控器(Hosted Hypervisor)
与原生型虚拟机监控器不同,在宿主模型中,物理资源由宿主机操作系统管理。宿主机操作系统是传统操作系统,如Windows、Linux等,这些传统操作系统并不是为虚拟化而设计的,因此本身并不具备虚拟化功能,实际的虚拟化功能由VMM来提供。VMM通常是宿主机操作系统独立的内核模块,有些实现中还包括用户态进程,如负责I/O虚拟化的用户态设备模型。VMM通过调用宿主机操作系统的服务来获得资源,实现处理器、内存和I/O设备的虚拟化。VMM创建虚拟机之后,通常将虚拟机作为宿主机操作系统的一个进程参与调度。此类型通常被称为二型虚拟机监控器(Type-2 Hypervisor)。
图2-10 混合模型架构
图2-11展示了宿主模型(Host-based Model)的架构。由于宿主机操作系统控制所有的物理资源,包括I/O设备,因此,设备驱动位于宿主机操作系统中。VMM则被拆分为两部分协同运作:内核态VMM和用户态VMM。内核态VMM包含了处理器虚拟化模块和内存虚拟化模块。设备模型实际上也是VMM的一部分,在图中位于用户态VMM中,在具体实现时,既可以将设备模型放在用户态,也可以将其放在内核态。典型的产品有VMware Workstation、VMware Fusion、KVM 、Intel HAXM 。
图2-11 宿主模型架构