3. 内存虚拟化

1. 背景概述和内存虚拟化概述

首先从一个操作系统的角度, 介绍其对物理内存存在的两个主要基本认识:

  • 物理地址从0开始
  • 内存地址连续性(至少在一些大的粒度上连续)

内存虚拟化的产生, 主要源于VMM与客户机操作系统!!!对物理内存的认识上存在冲突, 造成物理内存的真正拥有者 ---- VMM, 必须对客户OS所访问的内存进行一定程度的虚拟化.

所以VMM满足客户机操作系统对内存的这两点要求, 这个欺骗过程, 就是内存虚拟化.

不仅仅如此, 实际上, 内存虚拟化做到了:

  • 既满足了客户机操作系统对于内存和地址空间特定认识,
  • 也可以更好在虚拟机之间虚拟机与VMM之间进行隔离, 防止某个虚拟机内部的活动影响到其它的虚拟机甚至是VMM本身, 从而造成安全上的漏洞.

下面详细说明下.

2. 没有虚拟化的环境

先分析没有虚拟化的环境. 这种环境, 任何一个OS都认为自己完全控制处理器, 相应的就完全拥有了内存的所有权. 所以OS总是按照一台物理计算机上内存的属性和特征对其进行管理.

详细见第二章内存

2.1. 指令对内存的访问

指令对于内存的访问都是通过处理器来转发的, 首先处理器会将解码后的信息请求发送到系统总线上, 然后由芯片组来负责进一步转发.

为了唯一标识, 处理器使用统一编址方式物理内存映射成一个地址空间, 即所谓的物理地址空间.

平时, 我们将一根根内存条插到主板上的内存插槽中, 每根内存条都需要被映射到物理地址空间中某个位置!!!.

一般来说, 每根内存插槽!!!物理地址空间的起始地址!!!可以在主板制造时就固定!!!下来, 也可以通过某种方式由BIOS加电后自动设置!!!. 一旦**内存插槽的起始地址被固定!!!下来, 这根内存条上每个字节的物理地址就相应确定!!!**下来了.

总的来说, 一根根内存条形成了一个连续的物理地址空间, 而 这个物理地址空间一定是从0开始!!! 的.

例如4个内存插槽的主板, 每个插槽插上256MB的内存条, 如果4根插槽的起始地址分别固定为0x00000000、0x10000000、0x20000000和0x30000000, 那么在它们上面的物理内存就被映射为0x00000000 --- 0x0FFFFFFF、0x10000000 --- 0x1FFFFFFF、0x20000000 --- 0x2FFFFFFF、0x30000000 --- 0x3FFFFFFF这4段. 总的来说, 这四根内存条组成了该系统1GB的内存, 而且这1GB内存是从0开始的连续空间, 4根内存条上每个字节都会对应到唯一的物理地址. 处理器访问任何一个字节就是通过请求一个物理地址, 芯片组收到处理器发出的内存访问请求后, 会检测内核维护的物理地址空间的分配表, 当发现目标地址落在0x00000000 --- 0x3FFFFFFF范围内时, 处理器就会进一步将请求转发给内存控制器.

2.2. 操作系统对内存的认知

没有虚拟化的环境中, 操作系统也会假定物理内存是从物理地址0开始的.

2.2.1. Linux内核可执行文件

x86处理器上的Linux为例. 在x86上, Linux内核可执行文件头定义了每个段的大小!!!、期望在物理地址空间!!!中被加载的位置1MB!!!, 以及加载后执行第一条指令的地址!!! 等, 这些信息在编译链接阶段就确定下来了.

由于加载的位置是1MB, 那么对于后面代码, 其访问的段!!! 都是基于1MB这个起始地址!!! 的, 这也是在编译链接阶段就确定下来了的.

通常, 在加载内核时, 启动加载程序(Boot Loader!!! )就会通过对该文件格式的分析, 将相应的段!!!复制到期望的位置!!!, 然后跳转到内核文件指定的入口点!!!. 而系统所做的, 必须保证该指定位置存在可用内存.

如果物理地址空间不是从0开始的, Boot Loader将会因指定位置找不到可用内存而拒绝加载内核, 及时加载内核到内存中, 由于内核代码在访问段时也会自身产生错误而造成整个系统的崩溃.

2.2.2. DMA操作

除此之外, 现实操作系统基本上对内存连续性存在一定程度的依赖性, 如DMA. DMA的目的就是允许设备绕过处理器来直接访问物理内存, 从而保证了I/O处理的高效. 目前绝大多数设备都支持DMA功能, 只是在实现上对驱动程序提出了不同的要求. 如图.

3. 内存虚拟化

现实中设备在DMA的逻辑上要更复杂. 这是个简化模型.

左边的设备使用最直接的方式, 即驱动程序提供DMA的目标内存地址0x100000以及大小1MB, 然后设备顺序访问从0x100000到0x200000的内存. 很容易看到, 当一个内存页面大小小于1MB时, 就需要请求几个在物理上连续的内存页面, 以满足设备顺序访问内存的需求.

而右边设备使用了一种更加灵活的方式, 叫做"分散-聚合"(Scatter-Gather), 它允许驱动程序一次提供多个物理上不连续的内存段, 设备通过相关信息来离散地访问这些不连续的目标内存.

实际设备中, 这两种模式很普遍, 而且即使后一种模式, 设备允许的离散块数目是有限的, 为支持更大的DMA区域, 驱动程序仍然会在每一个离散块中分配多个连续的内存页面, 这就意味着驱动必须从OS中分配到足够多连续的空闲内存页来满足DMA的要求.

2.2.3. 操作系统对内存的认知小结

总之, 在没有虚拟化情况下, 操作系统在对内存的使用与管理已经达成以下两点认识.

⓵ 内存都是从物理地址0开始的.

⓶ 内存都是连续的, 或者说至少在一些大的粒度(如256MB)上连续.

3. 虚拟化

3.1. 内存虚拟化的目的

从上面操作系统对物理内存的假定和认知, 在虚拟化环境里, VMM的任务就是模拟使得模拟出来的内存仍然符合客户机操作系统内存的假定和认识.

因此, 在虚拟化环境中, 内存虚拟化的目的有两个.

  • 提供给虚拟机一个从零地址开始的连续物理内存空间.
  • 在各虚拟机之间有效隔离、调度和共享内存资源.

3.2. 内存虚拟化面临的问题

因此, 内存虚拟化面临的问题是:

  • 物理内存要被多个客户机操作系统同时使用, 但物理内存只有一份, 物理起始地址0也显然只有一个, 无法同时满足所有客户机操作系统内存从0开始的要求;

  • 由于使用内存分区方式!!!, 把物理内存分给多个客户机操作系统使用, 客户机操作系统的内存连续性要求!!! 虽然能得到解决, 但内存的使用效率非常不灵活.

3.3. 内存虚拟化实现

在面临这些问题的情况下,内存虚拟化的核心, 在于引入了一层新的地址空间 --- 客户机物理地址空间.

图3-5中, VMM!!! 负责管理和分配每个虚拟机的物理内存!!!, 客户机OS!!! 看到的是一个虚构的客户机物理地址空间!!!, 其指令目标地址!!! 也是一个客户机物理地址!!!.

虚拟化环境下, 这样的地址是不能被直接发送到系统总线上, 需要VMM!!! 负责将客户机物理地址!!! 首先转换成一个实际物理地址!!! 后, 再交给物理处理器来执行.

3. 内存虚拟化

值得一提的是, 为更有效利用空闲的物理内存, 尤其是系统长期运行后产生的碎片, VMM通常会以比较小的粒度(如4KB)进行分配, 这就会造成了给定一个虚拟机的物理内存实际上是不连续的问题, 其具体位置完全取决于VMM的内存分配算法.

3.3.1. 内存虚拟化主要工作

由于引入了客户机物理地址空间, 内存虚拟化就主要处理以下两个方面的问题.

⓵ 给定一个虚拟机, 维护客户机物理地址宿主机物理地址之间的映射关系!!!.

截获!!! 虚拟机对客户机物理地址的访问, 并根据所记录的映射关系, 将其转换宿主机物理地址.

第一个问题相对比较简单, 因为这只是一个数据结构的映射问题. 在实现过程中, 客户机OS采用客户页表维护了该虚拟机里进程所使用的虚拟地址到客户机物理地址的动态映射关系; VMM负责维护客户机物理地址到宿主机物理地址之间的动态映射关系.

第二个问题相对复杂, 也是衡量一个虚拟机的性能最重要的方面. 再者, 地址转换一定要发生在物理处理器处理目标指令之前, 否则一旦客户机物理地址被直接发送到系统总线上, 会造成严重的破坏.

一个最简单的办法, 设法让虚拟机对客户机物理地址空间的每一次访问都触发异常, 然后由VMM来查询地址转换表模拟其访问. 这种方法完备性和正确性没问题, 但性能是最差的, 其它方法见后续.

3.3.2. 系统的安全隔离

内存虚拟化还实现了整个系统安全隔离, 包括虚拟机之间, 虚拟机与VMM之间.

(1) VMM通过处理器硬件功能使得客户机操作系统完全运行在不同的地址空间, 或通过段限制使客户机OS所能"看见"的空间大小, 以保证VMM自身的安全性, 从而防止虚拟机触及VMM自身的运行状态.

(2) VMM通过特殊权限验证机制使客户机OS局限在给定的地址空间里, 以保证一个虚拟机只能访问分配给它的内存页.

(3) VMM通过硬件技术, 防止虚拟机利用设备可以通过DMA方式绕过处理器而直接访问目标内存的特点, 恶意访问设备的DMA目标寄存器, 进而通过设备越权访问所有物理内存.