8.3.5 内存的虚拟化

现在我们已经知道了如何虚拟化处理器。但是一个计算机系统不止是一个处理器。它还有内存和I/O设备。它们也需要虚拟化。让我们来看看它们是如何实现的。

几乎全部的现代操作系统都支持虚拟内存,即从虚拟地址空间到物理地址空间的页面映射。这个映射由(多级)页表所定义。通过操作系统设置处理器中的控制寄存器,使之指向顶级页表,从而动态设置页面映射。虚拟化技术使得内存管理更加复杂。

例如,一台虚拟机正在运行,其中的客户操作系统希望将它的虚拟页面7、4、3分别映射到物理页面10、11、12。它建立包含这种映射关系的页表,加载指向顶级页表的硬件寄存器。这条指令是敏感指令。在支持VT技术的处理器上,将会引起陷入;在VMware管理程序上,它将会调用VMware例程;在准虚拟化的客户操作系统中,它将会调用管理程序调用。简单地讲,我们假设它陷入到了I型管理程序中,但实际上在上述三种情况下,问题都是相同的。

广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元

那么管理程序会怎么做呢?一种解决办法是把物理页面10、11、12分配给这台虚拟机,然后建立真实的页表使之分别映射到该虚拟机的虚拟页面7、4、3,随后使用这些页面。到目前为止还没有问题。现在,假设第二台虚拟机启动,希望把它的虚拟页面4、5、6分别映射到物理页面10、11、12,并加载指向页表的控制寄存器。管理程序捕捉到了这次陷入,但是它会做什么呢?它不能进行这次映射,因为物理页面10、11、12正在使用。它可以找到其他空闲页面,比如说20、21、22并使用它们,但是在此之前,它需要创建一个新的页表完成虚拟页面4、5、6到物理页面20、21、22的映射。如果还有其他的虚拟机启动,继续请求使用物理页面10、11、12,管理程序也必须为它创建一个映射。总之,管理程序必须为每一台虚拟机创建一个影子页表(shadow page table),用以实现该虚拟机使用的虚拟页面到管理程序分配给它的物理页面之间的映射。

但更糟糕的是,每次客户操作系统改变它的页表,管理程序必须相应地改变其影子页表。例如,如果客户操作系统将虚拟页面7重新映射到它所认为的物理页面200(不再是物理页面10了)。管理程序必须了解这种改变。问题是客户操作系统只需要写内存就可以完成这种改变。由于不需要执行敏感指令,管理程序根本就不知道这种改变,所以就不会更新它的由实际硬件使用的影子页表。

一种可能的(也很笨拙的)解决方式是,管理程序监视客户虚拟内存中保存顶级页表的内存页。只要客户操作系统试图加载指向该内存页的硬件寄存器,管理程序就能获得相应的信息,因为这条加载指令是敏感指令,它会引发陷入。这时,管理程序建立一个影子页表,把顶级页表和顶级页表所指向的二级页表设置成只读。接下来客户操作系统只要试图修改它们就会发生缺页异常,然后把控制交给管理程序,由管理程序来分析指令序列,了解客户操作系统到底要执行什么样的操作,并据此更新影子页表。这种方法并不好,但它在理论上是可行的。

在这方面,将来的VT技术可以通过硬件实现两级映射从而提供一些帮助。硬件首先把虚拟页面映射成客户操作系统所认为的“物理页面”,然后再把它(硬件仍然认为它是虚拟页面)映射到物理地址空间,这样做不会引起陷入。通过这种方式,页表不必再被标记成只读,而管理程序只需要提供从客户的虚拟空间到物理空间的映射。当虚拟机切换时,管理程序改变相应的映射,这与普通操作系统中进程切换时系统所做的改变是相同的。

在准虚拟化的操作系统中,情况是不同的。这时,准虚拟化的客户操作系统知道当它结束的时候需要更改进程页表,此时它需要通知管理程序。所以,它首先彻底改变页表,然后调用管理程序例程来通知管理程序使用新的页表。这样,当且仅当全部的内容被更新的时候才会进行一次管理例程调用,而不必每次更新页表的时候都引发一次保护故障,很明显,效率会高很多。