在 x86 平台上实现虚拟化的问题引言 虚拟化,即在一个物理机上同时运行多个操作系统实例的技术, 自 20 世纪 60 年代开始在 IBM 的大型主机等专有平台上获得了重要的应用。 自 80 年代以来,基于 X86 处理器的 PC 技术开始高速发展并变得无处不在。 然而由于 PC 的低廉价格,人们似乎淡忘虚拟化的存在意义。进入 21 世纪,随着数据中心技术的发展和 VMware 等厂商的推动,虚拟化开始进入 PC 领域,直到今天在有着广泛应用的 PC 平台上施用虚拟化技术已经成为了趋势性的事件。在 IBM 的大型主机上,虚拟化主要目的是用来让多个操作系统共享单个主机的资源,以最大化资源的利用率。 然而在今天,由于 PC 的无处不在,虚拟化的价值已经远不仅在于最大化单台物理机的资源利用率。 相比较于传统的裸机操作系统,虚拟化能带来很多新的机会和优势。 在笔者看来,虚拟化最少在如下方面好处明显 :
应用服务的整合和安全隔离。
将多个物理机上运行的服务应用整合到一个物理机上,提高物理资源的利用率,极大的节省机房空间,提供灵活的电源管理,满足绿色经济的要求; 同时为在小的空间内部署复杂的计算或网络环境提供可能 ( 如 IP 测试床 )。 另一方面,原本运行在相同物理机上的多个应用可利用虚拟化方式部署进行隔离,提高应用的安全性。
快速动态部署能力
快速及动态的部署使应用服务的高可用性变的更简单, 使部署大规模的计算工作变的容易; 提供商在动态部署的基础上,实行精确的按使用计费或按需计费,使计算资源租用作为一种商业模式变得更有吸引力,而计算资源租用模式本身可利用对资源的灵活分配,统一管理来节省对总体资源的消耗。 削减操作系统和应用软件对硬件的依赖性
使大量无需或者难以升级的旧的应用系统得以在新的硬件平台上继续运行, 减少重新投资软件的必要。 建立复杂的软件测试环境
复杂的软件测试环境通常要求多个不同级别的处理器硬件平台,并搭配不通的操作系统和编译器版本,对于一个中小型用户来说,利用物理硬件来满足这个要求通常是很困难的,然而虚拟化技术让这个要求变得容易实现,只需要虚拟化软件创建多个不同的虚拟机,仿真出不通的处理器特征,然后再安装各自的软件环境即可。
为保持 x86 平台继续在数据中心服务器,计算工作站,个人电脑等市场的无处不在地位,Intel 等 x86 厂商必然会想方设法适应虚拟化所带来的变化和要求, 在 x86 上如何优化虚拟化技术的实现,提高虚拟化的性能是他们需要考虑的重大问题。
在 x86 上运行虚拟化的具体问题 虚拟化技术的一个重要要求之一就是通过虚拟机监控软件 (VMM) 运行的操作系统 ( 我们称之为客操作系统 ) 在运行时,其运行效果应该和直接在裸机上运行操作系统是一致的,即客操作系统不应该感觉到虚拟化技术的存在。 IBM 的主机采用 “trap-and-emulate” 的方法来实现 CPU 的虚拟化,即一般的指令直接运行,那些可能改变主机全局行为的“敏感指令”则会被截取,交由 VMM 通过仿真来完成其功能。 “trap-and-emulate” 被认为是实现 CPU 虚拟化的最好方法。 作者 Popek 和 GoldBerg 在 1974 年的一篇文章提出了 “classic virtualization” 的概念,该概念认为能够比较完好的实现 “trap-and-emulate”的硬件平台才是 “classically virtualizable” 的平台,即如果该平台上的 VMM 如果能够比较容易的捕获敏感指令,我们才认为该平台是典型的比较易于虚拟化的平台。 X86 平台由于其迅速的无处不在的统治力,在最初的设计上并没太多考虑虚拟化的要求,根据 GoldBerg 的标准,传统的 x86 属于非 “classically virtualizable”的平台。 Scott Robin 在 2000 年的文章以 Intel Pentium 为例详细介绍了 x86 平台在支持虚拟化方面存在的问题。GoldBerg 的标准并不排除用其他方法解决“trap-and-emulate”的问题, 如 X86 上的虚拟化提供商 VMware 和 XenSource 分别采用“Binary Translation” 和“Para-virtualization”来解决敏感指令问题。其中“Binary Translation”技术提前发现敏感的指令并通过插入断点来截获之,交由 VMM 来解释执行。 “Para-virtualization” 方法则直接修改客操作系统代码,修改其特权级,并将敏感指令改为 Trap Call 直接通知 VMM 来处理。 这两种在软件上处心积虑的方法会导致软件实现的复杂性,限制了 VMM 性能的提升空间,“Para-virtualization” 更是没法施用在 Windows 等私有操作系统上。
在说明 X86 平台对虚拟化的支持能力之前,我们有必要解释一下特权级的概念。 x86 硬件支持 4 个特权级 (Ring),一般内核运行在 Ring 0, 用户应用运行在 Ring 3, 更小的 Ring 有比更高的 Ring 能访问更多的系统全局资源,即更高的特权。 有些指令只能在 Ring 0 才能正确执行,如 LGDT、LMSW 指令,我们称之为特权指令;另外有些指令可以在 Ring 3 正确执行,如 SGDT、 SMSW、PUSHF/POPF,我们称之为非特权指令。
在传统的 X86 平台上支持虚拟化上存在如下问题 : X86 指令集中存在 17 条敏感的非特权指令
“非特权指令”表明这些指令可以在 x86 的 ring 3 执行, 而 “敏感性” 说明 VMM 是不可以轻易让客操作系统执行这些指令的。 这 17 条指令在客操作系统上的执行或者会导致系统全局状态的破坏,如 POPF 指令,或者会导致客操作系统逻辑上的问题,如 SMSW 等读系统状态或控制寄存器的指令。 传统的 X86 没法捕获这些敏感的非特权指令 “Ring deprivileging” 带来的问题
除了那 17 条敏感的非特权指令,其他敏感的指令都是敏感的特权指令。 在 x86 虚拟化环境,VMM 需要对系统资源进行统一的控制,所以其必然要占据最高的特权级,即 Ring 0, 所以为了捕获特权指令,在传统 x86 上一个直接可行的方法是 “Ring deprivileging”, 如将客操作系统内核的特权级从 Ring 0 改为 Ring 1 或 Ring 3, 即 “消除” 客内核的特权,以低于 VMM 所在的 Ring 0, 从而让 VMM 捕获敏感的特权指令。 然而,采用 “Ring deprivileging” 又会带来如下问题 :
“Ring aliasing”。 该问题是指客操作系统可通过读取 cs,ss 段寄存器的值而知道其自身已经不处于 Ring 0, 这一结果理论上可以让客操作系统改变自己的行为,违背了虚拟化应该对客操作系统透明的原则。
“Ring compression”。 无论采用哪些特权级,传统 x86 都需通过分段或分页的方法来实现地址空间间的访问控制。然而传统 x86 上所有 64 位的操作系统都没使用分段,另外 x86 上的分页不区分 Ring 0、Ring 1 和 Ring 2, 也就是说 x86 上没法通过分段或分页机制来阻止 Ring 1 上的 64 位客操作系统内核来访问 VMM 的地址空间。 所以,客内核被迫采用 Ring 3, 也就是和客操作系统上的应用相同的运行级,这当然就会导致新的问题。
“Adverse Impact on Guest system calls”。 该问题和用 sysenter/sysexit 指令实现的系统调用机制有关。Sysenter 不同于 “int 0x80”, 其不需要经过异常表的控制而明确切换到 Ring 0, 所以 sysenter 是 x86 平台上性能更高的系统调用实现方式。 但在虚拟化情况下 sysenter/sysexit 带来的问题是, sysenter 让客操作系统上的应用进入 Ring 0 而不是客内核的 Ring 1, sysexit 在 Ring 1 上执行直接导致 fault, 解决这一问题的方法只能是让 VMM 来仿真 sysenter/sysexit, 或者让 VMM 向客操作系统屏蔽掉虚拟 CPU 的 sysenter 能力,无论哪种做法,都会导致客操作系统上应用性能下降。