内存虚拟化概述
对于非虚拟化的操作系统来说,物理内存需要有两个特性:1.物理地址从0开始 2.物理地址是连续的。
指令对于内存的访问是通过处理器来完成转发的,处理器将解码的请求发送到系统总线上,然后由芯片组来负责转发。处理器采用统一编址的方式将物理内存映射为一个地址空间,也就是物理地址空间。平时内存条插入主板的内存插槽上,内跟内存条都会被映射到物理地址空间中的某个位置。一般来说,每根内存插槽在物理地址空间的起始地址在主板制造时就可以确定下来。一旦内存插槽的起始地址固定下来,这根内存条上的每个字节的物理地址就会确定下来。于是,一根根内存条形成了一个完整的物理地址空间,且这个物理地址空间是从0开始。对于操作系统来说,也会假定物理内存是从0开始的。通常在加载内核时,启动加载程序(BootLoader)会通过对文件格式的分析,将相应的段复制到期望的内存,然后跳转到内核文件制定的入口点。系统需要保证在制定位置存在可用内容,而如果物理地址的不是从0开始的,BootLoader也会因为制定位置找不到内存而拒绝加载内核,即便将内核加载到内存中,也会因为内核代码在访问段是因为错误造成崩溃。
除此之外,大多数的操作系统会对内存的连续性存在一定程度的依赖性,比如:DMA。有一些IO设备会绕过处理器直接进行内存访问;某些情况下物理连续的页面可以简化程序的设计,带来性能上的提升;操作系统在处理器的帮助下,使用大页面映射的方式加速对某一块连续的内存的访问。因此,对于每个操作系统来说,内存的使用管理都必须要满足物理地址从0开始和连续这两个要求。
在虚拟化的环境中,物理内存要被多个客户机操作系统同时使用,但真实的物理内存只有一份,物理地址的起始地址0也只有一个,无法满足所有客户机地址从0开始的需求。在这种清空下,VMM需要做的就是“欺骗”客户机操作系统,让客户机操作系统在使用物理内存时,依然可以满足之前提及的两点要求。这个欺骗的过程,就叫做内存虚拟化。内存虚拟化的核心就是给引入了一层新的地址空间——客户机物理地址空间gpa。
两阶段地址置换的原理如图所示,详细的过程在讲述过虚拟地址,物理地址,MMU,TLB等内存管理的基本概念之后再详细展开。
虚拟化下二级地址转换原理介绍
内存虚拟化技术,核心的点在于需要“欺骗”客户机操作系统物理地址是从0开始且是连续的。于是内存虚拟化引入了一个过渡层技术——客户机物理地址空间。
对于VMM来说,掌管的是实际的物理内存,并且需要分配和掌管每个虚拟机的物理内存。对于客户机操作系统(guest)来说,所看到的是一个虚构的客户机物理地址空间(guest physical address,gpa
),在没有虚拟化的情况下,就是实际的物理地址。但是在开启虚拟化的情况下,gpa是不能直接被发送到系统总线上的,而是需要经过VMM负责将客户机物理地址转换成一个实际的物理地址(host physical address,hpa)后,再交给物理处理器来执行。对VMM来说,为了更有效地利用空闲的物理内存,尤其是系统在长时间运行之后产生的随便,VMM通常会以较小的颗粒(4KB)进行分配,这就会造成虚拟机实际使用的hpa并不是连续的,该过程依赖于VMM的内存分配算法。
内存虚拟化有两种技术来完成虚拟机的地址空间转换:
- 对于给定的一个虚拟机,维护客户机物理地址gpa和宿主机物理地址hpa的映射
- 截获虚拟机对客户机物理地址的访问,并根据记录的映射关系,将其转化为宿主机物理地址。
对于第一种方法,实现的方式为客户机操作系统操作客户页表维护了虚拟机中使用的虚拟地址到客户机物理地址的转换,也就是两阶段转换中的第一阶段gva->gpa
,在riscv中也称作VS-stage
阶段。gva代表客户机虚拟地址,gpa代表客户机物理地址,这样对于客户机来说就可以不经过修改正常完成虚拟地址到物理地址的转换,是完全虚拟化技术。VMM负责维护客户机物理地址到宿主机物理地址之间的动态映射关系,gpa->hpa
。这里hpa表示的是真实的物理地址,在riscv中也称作两阶段地址转换的第二阶段,G-stage
。虚拟机里一个进程使用的客户机虚拟地址要变成物理处理器可以执行的宿主机物理地址,需要经过两层转换。这种方法的实现要完成两次页表的转换,也就意味硬件中需要有两个MMU模块完成转换,这种需要硬件配合的地址转换方式,称作硬件辅助的完全虚拟化。
各大厂商相继推出了硬件辅助的内存虚拟化技术,存在两个MMU硬件模块来合作完成两阶段地址转换。影子页表的存在是早起硬件中只有一个mmu地址转换模块,综合了各种因素只能另外缔造出了一个影子页表来使用这个真正的mmu模块。但是这种方式实现比较负责,任何guest页表的动静都会触发vmm的page fault
,也需要为每一个进程的页表缔造一个影子页表,占据的内存空间大,实现的难度高。英特尔将硬件辅助的两阶段地址转换的技术称作EPT。对于硬件辅助的两阶段地址转换,gva->gpa
依然是由客户机guest决定的,而gpa->hpa
的。之所以叫nest嵌套的页表转换,是因为一个gva->hpa
的过程,内存的访问次数并不是简单的4+4=8次的寻址。
首先要普及的概念是,页表本身一定是物理地址。因为页表是帮助虚拟地址来寻找物理地址的手段,satp寄存器中存放的是最高级页表的地址,找到这个页表的地址之后再根据vpn[3]中寻找到index。页表的结构类似于数组,数组中index个元素存放的是页表项PTE,根据PTE中的PPN和flag可以得到下一级页表的页号,进行页偏移之后得到了下一级页表的地址,再去vpn[2]中寻找index,直到最后一级页表中的PPN,存放的就是最终的物理地址的页号。假设gPT和nPT都是四级页表,gpa->hpa
的过程分为gva->gpa
,gpa->hpa
两个过程。但是gva->gpa
这个本来四次寻址就能完成的过程,在虚拟化的环境中出现了问题。因为页表本身需要的是物理地址,但是guest层全程在虚拟地址中运行的。所以一次gva->hpa的转换,需要24次的内存访问。CPU首先会在guest中查找guest对应的vsatp寄存。
对于第二种方法从实现上来说更加复杂,且这种方式往往应用于没有硬件辅助的情况下,用软件实现的方式,也叫作影子页表。VMM的任务是跟踪客户页表,当其发生变化时触发一个page fault并构造一个有效的客户机虚拟地址到宿主机物理地址之间的映射关系,并添加到处理器所遍历的真实的页表中。
影子页表(sPT - shadow Page Table
)是一种比较传统的方式,因为一开始时候硬件只有一个mmu模块用来地址转换,而虚拟机模式下需要完成两次地址映射。影子页表保存的是gva->hpa的映射关系。大概流程为:当guest地址访问的时候,利用硬件mmu模块在影子页表sPT中进行寻址,如果页表中并没有gva->hpa的映射,就会在vmm中触发一个page fault去填充影子页表中的内容。TLB中存放的是影子页表中gva到hpa的映射。Guest自身的客户机页表被设为只读,并且只是运行在一个虚拟的MMU模块而非真实地MMU模块,当guest打算更改gva->gpa的映射关系是,触发一个page fault并对影子页表的内容进行填充完成gva->hpa的映射。随着guest的运行,sPT页表中逐步完成所有虚拟地址到host物理地址的映射。但是该方式存在两个比较大的问题:
实现比较复杂,需要为每一个guest VM中的每个进程都维护一个对应的sPT,增加了内存的开销。当gva->hpa的映射关系不存在的时候,VMM会截获非常多的page fault和trap,增加了cpu的负担。在某些场景下,影子页表机制会占据整个VMM开销的75%。
影子页表的结构:当客户机操作系统中原本的CR3寄存器(riscv为satp寄存器)存放的是客户机页表的地址,将该值低12位清零之后得到的高20位为客户机页表页(GFN,Guest Frame Number
),GFN是gpa,对应的hpa称作host物理页号(MFN, Machine Frame Number
)。VMM会在host物理地址中新分配一个物理页,得到影子页表宿主机页号(SMFN, Shadow Machine Frame Number
),VMM把这个页加载到CR3(satp)寄存器中。当该进程重新被调度执行时,CR3(satp)寄存器中的值就是SMFN。SMFN会和MFN通过哈希表的方式进行映射,即SMFN=hash(MFN, type), type通常指的是在影子页表中的第几级页表。
Riscv中两阶段地址转换的流程
一个操作系统运行在硬件层面上往往有三种Mode,user mode(用户态),supervisor mode(内核态),Machine mode(运行在硬件上的机器模式)
。在开启了虚拟化的时候,一套硬件设备可以运行多个操作系统。当V=1表示虚拟化模式开启,guest客户机则运行在虚拟化的VU和VS层上。
假如guest中运行了一个linux操作系统,使用了根据第二章中的原理,将虚拟机地址通过MMU或者page table转化成为了物理地址。如图:
但是这个转换出的地址是guest物理地址,而非真正的可以在硬件中寻址的物理地址。我们将这个过程称之为
(gva(Guest Virtual Address)->gpa(Guest Phyical Address))
,也就是两阶段地址转换过程中的第一阶段,也称VS-stage。在得到这个gpa之后,经过VMM/hypervisor的转换才得到最终的物理地址hpa(Host Phyical Address
)。gpa->hpa地址转换的过程是两阶段地址转换的第二个过程,也称G-stage。在riscv架构中,当V=1时,表示虚拟化和两阶段地址转换与保护开启。VS-stage第一阶段,vsatp寄存器保存的是原来satp的内容,即在guest模式下,进程对应的最高级页表的物理地址。在G-stage第二阶段时,hgatp寄存器保存着vcpu对应的最高级页表的地址(在每次vcpu进行切换的时候,都会对hgatp寄存器进行保护和恢复,vcpu就是hypervisor的进程)。vstap寄存器用法用法和stap寄存器类似,结构也类似。
Hgatp寄存器的用法和satp相似,但是并不相同。
当hgatp的MODE位为0时(bare),gpa的地址等于hpa的地址,也不会进行地址的保护。当MODE位为不同的位时,hypervisor层会选择相应的转换方式进行转换。
不管是Sv32x4还是Sv39x4,Sv48x4转换的方式和sv32页表寻址介绍的方式类似。对于Sv32x4来说,gpa也是就是一个虚拟地址。gva是32位,转换到物理地址gpa之后是33位。所以相比Sv32,Sv32x4在高位vpn[1]处多了两位。但转换的原理不变。
Sv39x4和Sv48x4类似,其gpa的格式如下: