在上期,德国哲学家马克斯韦伯指出,通过在大型分布式计算系统中,引入中间管理层Hypervisor,并将CPU的内核视为计算资源,进行资源池化,建设科层制的分布式计算,是解决海量处理器并发计算问题的唯一可行的方法。在Hypervisor之下,每台虚拟机内部也有一个中间管理层Guest OS,管理虚拟机内部的计算、存储和网络资源,调度虚拟机内的计算任务,如下图所示:
马克斯韦伯还认为,为了让中间管理层更好地执行管理职责,需要赋予中间管理层一些特权,而为了避免特权的滥用,又需要一些措施对特权进行限制和监督,也就是所谓的“把特权关进笼子”。
在虚拟化系统中,最需要关进笼子的特权持有者,就是虚拟机的Guest OS。
让我们翻开厚厚的《Intel 64 and IA 32 Architectures Software Developer 's Manual》的Volume 3A: System Programming Guide, Part1,在第二章《System Architecture Overview》中,提到了,Intel的处理器在上电复位后,进入了实地址模式(Real Address Mode)。在实地址模式下处理器可寻址1088K的RAM地址,以及64K的IO空间,任意进程或可执行程序都可以随意读写这些地址空间,没有任何安全性可言。(这也是DOS操作系统下计算机病毒泛滥的根本原因)
因此,Intel在286时代引入了保护模式(Protected Mode),并在386时代将保护模式标准化。在保护模式下处理器被分为4个特权环,分别为Ring3,Ring2,Ring1和Ring0。Ring0可以访问整个系统的所有资源,而Ring3受到最严格的限制。以Linux系统为例,内核指令运行在Ring0下,而应用程序运行在Ring3。当处理器从低特权级向高特权级切换的时候,需要经过“调用门”。
那么,我们遇到了一个问题:
虚拟机上也有操作系统,操作系统的内核应当工作在ring0。那么,虚拟机上操作系统工作在ring0时,实际上就可以访问整个计算机系统内的任何资源,包括宿主机! 这种情况被称为“虚拟机逃逸”,属于虚拟化系统中不可接受的现象。
科学家和工程师们为了防止虚拟机逃逸的发生,需要一种机制将虚拟机上ring0的特权关进笼子。
首先,人们想到的是,改造虚拟机上操作系统运行的模式。如果我们让虚拟机内的Guest OS运行在ring1,宿主机的操作系统运行在ring 0,会怎么样呢?
当Guest OS运行在Ring1时,可以执行绝大部分操作系统内核应当完成的任务,只有遇到所谓的特权指令时,会触发CPU的陷阱(Trap),如下图所示:
当GuestOS的内核在ring1中,执行只有ring0才可以执行的特权指令时,触发了x86的trap,导致处理器暂时停下当前执行的指令,将CS:EIP指向的地址保存到堆栈,并转入trap处理例程。trap处理例程是由宿主机操作系统中的VMM提供的,会对特权指令进行适当的处理,再跳转回ring1中的guest OS内核。
然而,这种实现有一个漏洞——
在x86中,有一批指令被称为“敏感指令”。它不属于特权指令,但和特权指令类似,有可能在非Ring0中,修改系统中一些关键的标志位,或操纵真实的物理外设,或被处理器忽略,而且并不会触发trap。
让我们举一个栗子:
熟悉x86保护模式的同学可能知道,在中断处理时,首先需要使用指令pushf将FLAGS寄存器的内容压到栈中,然后将栈顶的IF清零,在中断处理结束后使用popf指令从栈中恢复FLAGS寄存器,让处理器的FLAGS寄存器恢复到中断产生之前。但是,如果处理器运行在ring1下,x86的CPU并不会抛出异常,而只是默默地忽略指令popf。在这种情况下,IF标志位没有被清零,系统将无法正确处理后续的中断。
如何让虚拟机在ring1下正确执行这些敏感指令,从而修补这一漏洞呢?
著名开源虚拟化项目Xen给出的答案是:对虚拟机操作系统内核进行改造。
在Xen中,虚拟机的操作系统运行于ring1,但操作系统代码是进行过改造的,所有应当在ring0中运行的特权指令被替换为hypervisor call,调用Xen提供的内核处理例程进行操作,如下图所示:
由于这种虚拟化实现方式只实现了半截,另外半截需要在宿主机的内核中执行,因此被称为半虚拟化(para-virtualiazation)。半虚拟化的实现方式要修改操作系统内核代码,因此,如果操作系统是Windows或Unix一类非开源操作系统,将无法在Xen为代表的半虚拟化平台上运行,而即使是Linux这样的开源操作系统,其配套的驱动程序也需要进行修改后才能使用。
同时,业内也出现了以VMWare为代表的另一个流派。这个流派的观点是,不修改GuestOS的系统内核,而是让Hypervisor在后台跟踪捕捉GuestOS的指令。如果发现敏感指令,则用其他指令替换之,通过在运行时以代码块为单元动态修改二进制代码的方式,来让这些敏感指令的执行得到期望的结果,如下图所示:
这种方式被称为“二进制翻译”。
显然,二进制翻译实现的虚拟化,虽然无需改动操作系统内核代码,但由于需要在运行时做指令翻译,会导致虚拟机性能显著低于物理机。
这也就是在谈论“虚拟化”这一话题的时候,经常被提到的虚拟化性能损耗。
如何解决这一问题呢?
请看下期分解。