Xen的虚拟化详解

最近在看Xen在2003年发表在sosp上的论文《Xen and the Art of Virtualization》,中途遇到一些不理解的技术点,在网络上查找相关资料,发现大多数人都只是介绍了一些Xen的历史和相关概念,对底层实现方案几乎没有涉及,少有的几篇博客虽然介绍了一些内容,但是我个人觉得很多地方过于笼统,而且互相之间内容十分类似。因此决定在深入学习之后总结出一篇博客,既能理清自己在学习过程中的思路,也能帮助后来者快速入门。

这篇博客不太适合对于虚拟化和操作系统方向的初学者,有些概念我不会详细介绍,默认读者已经掌握,当然即使是初学者想要通过本篇博客学习Xen,我觉得也是可以的,遇到不懂的地方可以自行Google(像para-virtualization和full-virtualization这类概念,我在前面已经介绍过了,这里就不提了)。

由于我的理解是基于以往研究其他系统的经验,对于Xen的底层源码我没有深入研究过,只是根据上面我提到的论文以及网络上的资料分析整理而成,难免有疏漏或者错误,还请读者不吝批评指正。

总体介绍

先抛开Xen的虚拟化方案,不妨设想一下,一个什么样的虚拟化方案才是最好的呢?由于我本人就是研究虚拟化和操作系统的,如果让我来回答这个问题,那我希望有以下几点:

  1. 性能应该很好,最好能和裸机时相差无几。
  2. 兼容性应该足够好,最好能够研究出一种虚拟机,guest os可以不加修改直接运行。
  3. 安全性和隔离性应该得到提升,因为加了一个vmm(virtual machine monitor,也可称为hypervisor),相当于加了一个隔离层。

如果上述三点是我们设计一个虚拟机的目标,那么我们首先要问,这三点能不能同时满足呢?其实,这三个要求本身就是相互制约的。首先,1和3之间的矛盾关系很明显,隔离性越好安全性就会越高,可是由于模块之间的隔离,它们互相调用时的开销也就会越大,因此性能必然有所影响,看过之前我介绍微内核的虚拟化方案的读者应该对此深有体会。接下来我们看看1和2,从道理上分析,我们知道,在一般情况下越是通用的东西在具体的应用场景上是不如专用的东西的。所以兼容性的提升在针对某一个具体guest os时的性能就不会好过针对某一个具体guest os情况设计的方案,反映在实现原理上就是para-virtualization和full-virtualization之间的矛盾。para-virtualization需要修改guest os的源码,这在一定程度上是一种开销,但是它可以显著提升性能,尤其在没有硬件支持的条件下,这种性能提升是显著的。

接下来我们来看xen的虚拟化方案,它需要修改guest os的源码,但是这种修改量是可接受的,如下图所示,展示了guest os的修改量(数据来自于xen的论文)。

Xen的虚拟化详解

所以说xen的缺点就是需要修改guest os的源码,但是它并不需要修改guest application的源码,事实上它对于应用程序提供的ABI(application binary interface,应用二进制接口)是兼容的,其实修改os并不可怕,而且上图也展示了,修改量其实不大,但是如果修改guest application就变得不可接受了,因为应用程序的数量非常之大,而且开发应用程序的人可不愿意做额外的修改,那么这种解决方案很可能就被淘汰了。

综上,xen采用了para-virtualization的方案同时兼顾了1和3,对于2,xen需要修改guest os的源码,不太理想,但是由于修改量可以接受,因此也还可以,xen是剑桥大学研究的,可以说在虚拟化领域是十分有代表性的,值得深入学习。(个人感觉)

Xen的虚拟化方法

在本文中,当我们提到xen的时候,其实是指vmm,也可以称之为hypervisor,因为xen的主要工作就是向guest os提供一个虚拟的运行环境。一般来讲,vmm的功能就是向guest os提供一个虚拟的硬件,具体来说,vmm一定要能够处理各种特权指令(supervisor instruction)并且能够虚拟化x86 MMU(其实就是能够处理页表的虚拟化)。这些问题当然可以解决,但是往往需要增加程序的复杂性和性能。这里我们举个例子(其实是xen的论文里举的例子):VMware的ESX Server,它通过重写hosted machine code,当与vmm发生交互时,插入trap指令。这就需要对整个guest os kernel作出修改,将所有的non-trap privileged都改成trap,这样这些指令就可以被捕获(caught)并且处理(handled)。ESX Server实现了影子结构(虚拟硬件的影子结构),每次对影子结构的更新操作都要执行trap,这会产生一个很大的开销。比方说,创建一个进程,需要执行fork()和exec()两个系统调用,这两个系统调用的实现还需要操作页表,这就需要很多额外的trap和与vmm的交互。

VMware还是基于full-virtualization的方法,xen让guest os可以同时访问real and virtual resource,比方说,guest os可以直接访问物理页表,这样可以提升性能。具体来说,xen通过vmm来管理底层硬件,向guest os提供一种硬件抽象,它类似但不等同于底层的物理硬件,这种方法就被称为para-virtualization。xen的设计原理主要为以下四条:

  1. 不需要修改ABI(application binary interface),否则应用程序开发者还要把应用程序移植到xen平台上。因此xen必须能够虚拟化现有的标准ABI所需的所有硬件feature。
  2. 需要能够支持多应用的操作系统,这需要允许guest os完成复杂的服务配置。
  3. 在硬件不支持虚拟化的情况下,采用para-virtualization获得高性能和隔离性。
  4. 即使在支持虚拟化的硬件架构上,对guest os完全隐藏资源虚拟化的影响也面临着正确性和性能的风险。

虚拟机接口(The Virtual Machine Interface)

接下来我们讨论xen向guest os提供的虚拟机抽象接口(virtual machine abstraction),以及guest os如何修改以便适应这些硬件接口。在此之前我们先区分几个概念,guest os、domain和hypervisor。guest os代表Xen需要虚拟化的操作系统,domain是guest os运行时所在的虚拟机,guest os和domain的区别类似于"程序"和"进程"之间的关系,两者一个是静态的,另一个是动态的。我们称Xen为hypervisor,因为Xen相比于guest os的supervisor code,运行在一个更高的特权级。

对于x86的硬件接口,我们将其分为三个主要部分:内存管理、CPU的运行和I/O外设。

Xen的虚拟化详解

如上图所示,接下来我们分别进行讨论

内存管理

对内存进行虚拟化是这三部分中最难的,无论是对于hypervisor的设计还是对于guest os的修改(在关于NOVA那篇博客中我已经提到,在没有硬件支持的条件下,虚拟化内存是十分困难的,因为硬件不感知多层次的页表,需要手工操作,典型的方法是影子页表)。x86硬件不支持由软件管理的TLB,每次TLB miss处理器都会自动通过硬件执行一次page-table walk,在这种情况下,想要实现最优的性能,最好的办法是设法让所有地址转换过程中需要访问到的页表都可以由硬件直接访问到。除此以外,由于TLB不是tagged的,不能标识对应的guest os,所以每次地址空间转换都需要执行TLB flush。由于这些限制,Xen在内存管理方面采取两点措施:

  1. 由guest os直接负责申请和管理硬件的页表,尽量绕过Xen来确保安全性和隔离性。
  2. Xen存在于每一个guest os的高64M空间(一个段的空间),这样确保进入和离开hypervisor的时候不需要额外的TLB flush操作。

当guest os想要申请一个页面的时候,比方说它创建一个进程时,它从自己管理的物理页表区域里申请并初始化一个页,并且向Xen进行注册。此时,guest os必须放弃对页表的直接

写权限,接下来更新页表的操作都需要经过Xen的验证。这些限制可以用多种方式实现,比如只允许guest os去map它的page table,但是不允许更改它的page table。guest os可以批量发起更新请求,减少进入和退出hypervisor所带来的开销。每一个地址空间的高64MB是不允许guest os访问或者重新映射的(remap)。这个地址区域x86的ABI是用不到的,所以它不会破坏应用程序的兼容性。

段的虚拟化采用类似的方式,通过Xen去验证之后才去更新硬件的段描述符表(segment descriptor table),对于x86的段描述符,主要有两条限制:

  1. 它们必须比Xen的特权级低;
  2. 它们不能访问Xen保留的地址空间端口。

CPU

在native环境下,我们一般认为os应该运行在系统的最高特权级上,加入了hypervisor以后,为了保证guest os的错误不会影响hypervisor(进而影响其他的guest os),guest os必须被修改从而运行在低一级的特权级上。如果处理器只支持两个特权级,那么guest os就需要和guest application运行在相同特权级上,这时需要引入虚拟特权级来区分guest os和guest application,具体的做法其实类似于操作系统的常规做法,把os和应用程序分别放在不同的地址,通过hypervisor传递控制信息,这些控制信息包括修改当前的虚拟特权级和切换当前的地址空间。如果处理器支持地址空间的tags,还可以避免高开销的TLB flush操作。

由于x86架构支持4种特权级,ring0-ring3,因此比较容易做到虚拟化特权级。只有ring0可以执行特权指令,而ring1和ring2一般都是不使用的,我们可以修改guest os的源码,让它运行在ring1,这样guest os不能直接执行特权指令,同时可以区分guest os和guest application。guest os的特权指令的执行需要通过Xen验证并由其仿真执行,比如申请一个页表或者让出处理器当前执行权限。当guest os试图直接执行敏感指令,处理器可能什么也不做(silently)或者产生一个异常(fault),因为只有Xen运行在特权模式(ring0)。异常包括内存访问错误(page falut)和软件trap,在x86架构上都可以直接进行虚拟化。Xen中有一个表(类似于中断向量表),根据每种异常类型跳转到对应的handler去处理。这里面比较特殊的是page fault的处理方法,因为guest os不能访问CR2寄存器(保存faulting address),因此每次发生page falut都需要通过Xen把CR2寄存器的内容拷贝到guest os的地址空间(栈空间)。另一种影响性能的异常是system call,为了提高性能,我们允许每个guest os在Xen中注册一个fast exception handler,不需要通过ring0就可以被处理器直接访问,这个handler在被插入到Xen的中断向量表之前就需要被验证,Xen会检查这个handler的代码段是不是只能在ring0中执行。除此以外,其他问题都很好解决,比如handler对应的代码段不在内存中,那么当Xen执行iret指令想要跳转到handler时会产生额外的page fault,Xen会另行处理(按照正常的page fault进行处理)。

Device I/O

对于full-virtualization来说,需要仿真具体外设,而Xen采用para-virtualization,只需要完成一些外设抽象,我们因此只需设计接口能够满足高效性并且满足我们的安全性和隔离性要求,从这个角度来说,I/O数据通过Xen与每一个domain进行传输,这种数据传输通过共享内存和异步buffer来实现。这提供了系统间通信的一种高效的机制,同时允许Xen去有效的验证(比方说,检验一个buffer是否在一个domain的内存区域中)。

接下来一个重要的部分是外设中断,类似于真实的物理中断,Xen提供一种相对轻量级的时间传递机制,用于向domain传递异步notification(这个notification类似于interrupt)。这种notification的具体做法是:更新一个等待事件的bitmap区域进而调用一个guest os特定的事件处理程序。这种回调能够由guest os延迟,去避免notification造成额外的开销。

系统架构

首先看一张Xen虚拟机的总体架构图。

Xen的虚拟化详解

上一篇:Csharp 简单操作Word模板文件


下一篇:BZOJ 3252: 攻略(思路题)