核心问题:xv6如何实现虚拟地址转换的?即给定一个虚拟地址,xv6如何读写到真实的物理内存上。
为什么需要虚拟内存?
实现操作系统的隔离性,使各个进程之间互不干扰,自以为独占内存。
地址空间
一个进程的地址空间包含运行的程序的所有内存状态。
虚拟内存系统负责为程序提供一个巨大的、稀疏的、私有的地址空间的假象,其中保存了程序的所有指令和数据。操作系统在专门硬件的帮助下,通过每一个虚拟内存的索引,将其转换为物理地址,物理内存根据获得的物理地址但获取所需的信息。
操作系统会同时对许多进程执行此操作,并且确保程序之间互相不会受到影响,也不会影响操作系统。
虚拟地址和物理地址
RISC-V 指令(用户和内核)操作的是虚拟地址(用户看到的地址都是虚拟地址)。物理内存(RAM)是用物理地址来做索引的。
RISC-V的页表硬件(MMU)通过将每个虚拟地址映射到一个物理地址将这两种地址联系起来(地址映射)。映射的单位是页(Page),RISC-V中,一个Page是4KB(4096Bytes)。
虚拟地址格式
xv6中虚拟地址共39bit(硬件决定的,最高可以用64bit,xv6基于硬件平台是Sv39 RISC-V),其中
27bit => index (VPN)
12bit => offset
offset必须是12bit,因为对应了一个page的4096个字节。
物理地址格式
物理地址共56位(这是由硬件设计人员决定的,RISC-V的设计人员认为56bit的物理内存地址是个不错的选择…),其中
- 44bit => index (PPN)
- 12bit => offset
页表项与地址转换过程
硬件
页表是在硬件中通过处理器和内存管理单元(Memory Management Unit)实现。采用哪个页表由 SATP
寄存器指定(只有运行在kernel mode的代码可以更新这个寄存器)。每个 CPU 都有自己的 SATP
寄存器、MMU、TLB。
在这种Sv39配置中,一个RISC-V页表在逻辑上是一个由227(134,217,728)个页表项(Page Table Entry, PTE)组成的数组。
每个PTE包含一个44位的物理页号(Physical Page Number, PPN)和一些标志位,如下图所示。
分页硬件通过利用 VPN 在页表中索引找到一个 PTE ,取其 PPN,然后组合原虚拟地址后12位的偏移组成物理地址。
页表项
页表项(Page Table Entry, PTE)是页表的基本组成单位,每个PTE包含一个44位的物理页号(Physical Page Number, PPN)和一些标志位,如下图:
每个 PTE 都包含标志位,用于告诉分页硬件相关的虚拟地址被允许怎样使用。
PTE_V
表示 PTE 是否存在:如果没有设置,对该页的引用会引起异常(即不允许)。PTE_R
控制是否允许指令读取该页。PTE_W
控制是否允许指令向该页写入。PTE_X
控制 CPU 是否可以将页面的内容解释为指令并执行。PTE_U
控制是否允许用户态下的指令访问页面;如果不设置PTE_U
, 对应 PTE 只能在内核态下使用。
标志位和与分页硬件相关的数据结构定义如下:
|
|
多级页表
采用多级页表的动机:
根据上文所说,一个RISC-V页表在逻辑上是一个由 227(134,217,728)个页表项(Page Table Entry, PTE)组成的数组。而一个PTE占64bit(8Bytes)。
那么,这个数组将占227 * 8 = 230 字节 = 1 GB。这个所占内存数量是我们无法接受的。
另外一个例子是linux:
在32位的linux中,一个页大小也是4KB,占地址的12Bits。
页表一共需要记录 220 个PTE。一个PTE算作是完整的 32 位(4 字节),这样一个页表就需要 220 * 4 = 4MB 的空间。
每一个进程,都有属于自己独立的虚拟内存地址空间,这样每一个进程都需要这样一个页表。综合来看,这将造成极大的内存浪费。
因此采用多级页表的形式,xv6采用如下图所示三层页表结构,多级页表能够在大范围的虚拟地址没有被映射这种常见情况时忽略整个页表。
- 页表以三层树的形式存储在物理内存中。
- 结构:树的根部是一个 4 KB 的页表页,它包含 512 个(2^9) PTE,这些 PTE 包含树的下一级页表页的物理地址。每一页都包含 512 个 PTE,用于指向下一个页表的物理地址或最终转换结果的物理地址。
- 查找:分页硬件用 27 位中的高 9 位选择根页表页中的 PTE,用中间 9 位选择树中下一级页表页中的 PTE,用低 9 位选择最后的 PTE。
- 如果转换时所需的三个 PTE 中的任何一个不存在,分页硬件就会引发一个缺页异常(page-fault exception),让内核来处理这个异常。
主要注意的是:3级页表的查找都发生在硬件中,MMU是硬件的一部分而不是操作系统的一部分。
在xv6中,walk
函数模拟了MMU的三级页表找寻功能:
|
|
walkaddr
函数基于 walk
函数,可以根据一个页表及虚拟地址,返回其对应的物理地址:
|
|
TLB
采用TLB的动机:
访问内存是一种比较耗时的操作,尤其是在引入3级页表后,访问内存的次数会增多,因此考虑增加一层cache,从而减少地址转换时间。
每个 RISC-V CPU 都会在 Translation Look-aside Buffer(TLB) 中缓存PTE。当处理器第一次查找一个虚拟地址时,MMU通过3级page table得到最终的PPN,TLB会保存虚拟地址到物理地址的映射关系。这样下一次访问同一个虚拟地址时,处理器可以查看TLB,TLB会直接返回物理地址,而不需要通过访问页表得到结果。
需要注意的是:当 xv6 改变页表时,必须告诉 CPU 使相应的缓存 TLB 项无效。通过 sfence.vma
指令可以刷新当前 CPU 的 TLB。
xv6中,用户页表切换内核页表是,刷新TLB:
|
|
内核页表切换用户列表是,刷新TLB:
|
|
参考资料
[1] ostep-抽象-地址空间