1 内存分页管理
内容摘自大神Vamei的博客
内存
内存用内存地址(memory address)来为每个字节的数据顺序编号。内存地址说明了数据在内存中的位置。内存地址从0开始,每次增加1。内存提供的存储空间,除了能满足内核的运行需求,还通常能支持运行中的进程,存储进程的相关数据。
虚拟内存
应用程序进程并不直接访问物理内存,每个进程都有自己的一套虚拟内存地址,用来给自己的进程空间编号。进程空间的数据同样以字节为单位,依次增加。不同进程的虚拟内存地址相互独立,可以重复,但是虚拟地址对应的实际物理内存地址是不一样的,不同进程虚拟地址对内存物理地址的映射关系需要操作系统来负责。
内存分页
虚拟内存地址与物理内存地址的对应关系记录在一张表中,这张表加载在内存中。以多大的内存地址空间单位来进行地址映射,决定了这张表需要记录的内容多少。越小的内存粒度将产生越多转换记录,和越大的转换表格。
Linux采用了分页(paging)的方式来记录对应关系。所谓的分页,就是以更大尺寸的单位页(page)来管理内存。在Linux中,通常每页大小为4KB。内存分页,可以极大地减少所要记录的内存对应关系。
无论是虚拟页,还是物理页,一页之内的地址都是连续的。这样的话,一个虚拟页和一个物理页对应起来,页内的数据就可以按顺序一一对应。这意味着,虚拟内存地址和物理内存地址的末尾部分应该完全相同。大多数情况下,每一页有4096个字节。由于4096是2的12次方,所以地址最后12位的对应关系天然成立。我们把地址的这一部分称为偏移量(offset)。偏移量实际上表达了该字节在页内的位置。地址的前一部分则是页编号。操作系统只需要记录页编号的对应关系。
多级分页表
记录进程空间页和物理页的对应关系的表叫分页表(page table)。由于每个进程会有一套虚拟内存地址,那么每个进程都会有一个分页表。
如果把所有进程的分页表都记录到同一个线性列表中,需要给每一个虚拟页预留一条记录的位置。但对于任何一个应用进程,其进程空间真正用到的地址都相当有限,如果使用连续分页表,很多条目都没有真正用到。Linux中的分页表,采用了多层的数据结构,多层的分页表能够减少所需的空间。
页编号分成了两级。第一级对应了前8位页编号,用2个十六进制数字表示。第二级对应了后12位页编号,用3个十六进制编号。二级表记录有对应的物理页,即保存了真正的分页记录。二级表有很多张,每个二级表分页记录对应的虚拟地址前8位都相同。比如二级表0x00,里面记录的前8位都是0x00。翻译地址的过程要跨越两级。我们先取地址的前8位,在一级表中找到对应记录。该记录会告诉我们,目标二级表在内存中的位置。我们再在二级表中,通过虚拟地址的后12位,找到分页记录,从而最终找到物理地址。
多层分页表就好像把完整的电话号码分成区号。我们把同一地区的电话号码以及对应的人名记录同通一个小本子上。再用一个上级本子记录区号和各个小本子的对应关系。如果某个区号没有使用,那么我们只需要在上级本子上把该区号标记为空。同样,一级分页表中0x01记录为空,说明了以0x01开头的虚拟地址段没有使用,相应的二级表就不需要存在。正是通过这一手段,多层分页表占据的空间要比单层分页表少了很多。
多层分页表还有另一个优势。单层分页表必须存在于连续的内存空间。而多层分页表的二级表,可以散步于内存的不同位置。这样的话,操作系统就可以利用零碎空间来存储分页表。
TLB
TLB是translation lookaside buffer的简称,可翻译为“地址转换后援缓冲器”,也可简称为“快表”。简单地说,TLB就是页表的Cache,其中存储了当前最可能被访问到的页表项,其内容是部分页表项的一个副本。只有在TLB无法完成地址翻译任务时,才会到内存中查询页表,这样就减少了页表查询导致的处理器性能下降。
TLB中的项由两部分组成:标识和数据。标识中存放的是虚地址的一部分,而数据部分中存放物理页号、存储保护信息以及其他一些辅助信息。
EPT和影子列表
虚拟化系统中包括三层内存地址空间:虚拟机虚拟地址GVA、虚拟机物理地址GPA和物理机物理地址HPA。因此,原先由MMU完成的线性地址到物理地址的映射已经不能满足,必须由VMM接入来完成这三层地址的映射维护和转换。
为了实现上述映射和转换关系,主要有两种解决方案:软件解决方案—影子页表和硬件解决方案—Intel的EPT和AMD的RVI
影子列表
影子列表存储GVA-HPA的映射关系,Guest OS的页表内容保持不变,然后,VMM将影子页表写入MMU。
影子页表的维护将带来时间和空间上的较大开销。时间开销主要体现在Guest OS构造页表时不会主动通知VMM,VMM必须等到Guest OS发生缺页错误时(必须Guest OS要更新主页表),才会分析缺页原因再为其补全影子页表。而空间开销主要体现在VMM需要支持多台虚拟机同时运行,每台虚拟机的 Guest OS通常会为其上运行的每个进程创建一套页表系统,因此影子页表的空间开销会随着进程数量的增多而迅速增大。
为权衡时间开销和空间开销,现在一般采用影子页表缓存(Shadow Page Table Cache)技术,即VMM在内存中维护部分最近使用过的影子页表,只有当影子页表在缓存中找不到时,才构建一个新的影子页表。当前主要的虚拟化技术都采用了影子页表缓存技术。
EPT
Intel公司在Nehalem微架构CPU中推出扩展页表(Extended Page Table,EPT)技术;AMD公司在四核皓龙CPU中推出快速虚拟化索引(Rapid Virtualization Index,RVI)技术。
RVI与EPT尽管在具体实现细节上有所不同,但是在设计理念上却完全一致:通过在物理MMU中保存两个不同的页表,使得内存地址的两次映射都在硬件中完成,进而达到提高性能的目的。具体来说,MMU中管理管理了两个页表,第一个是GVA >>>GPA,由虚拟机决定;第二个是GPA>>>HPA,对虚拟机透明,由VMM决定。根据这两个映射页表,CPU中的page walker就可以生成最近访问过key-value键值对<GVA,HPA> ,并缓存在TLB中(类似影子页表缓存技术思路)。
另外,原来在影子页表中由VMM维持的GPA>>>HPA映射关系,则由一组新的数据结构扩展页表(Extended Page Table,也称为Nested Page Table)来保存。由于GPA >>>HPA的映射关系非常定,并在虚拟机创建或修改页表时无需更新,因此VMM在虚拟机更新页表的时候无需进行干涉。VMM也无需参与到虚拟机上下文切换,虚拟机可以自己修改GVA >>>GPA的页表。