RISC-V
使用虚拟地址进行索引,无论是设备的 RAM
还是物理内存都使用虚拟地址。RISC-V
的页表硬件通过将虚拟地址映射到物理地址上来连接这两种地址。
xv6
运行在 Sv-39 RISC-V
上,这意味着只有虚拟地址的 64 位中的低 39 位被用作编址,而高 25 位被保留。在 Sv39
的设置中,页表是一个存放了 $2^{27}=134217728$ 个页表条目 (Page Table Entry
) 的数组。其中每个 PTE
中的高 44 位存放着物理页面编号 (Physical Page Number
),低 12 位则是标志位。
页表设备首先使用 39 位虚拟地址中的高 27 位来索引到页表中以得到页表条目 PTE
。然后,从 PTE
中的高 44 位得到物理页面编号,再与虚拟地址中剩下的低 12 位偏移量组合得到 56 位的物理地址。也就是说,通过页表可以得到虚拟地址对应的实际物理内存中的页面,然后通过虚拟地址中页面的偏移量可以得到实际物理内存中具体的字节。
在 Sv39 RISC-V
中,64位的地址中有25位被保留。在 PTE
格式下,物理地址也有10位的空间可用于拓展(原本共有44+10=54位)。这些数字的选择是有原因的。首先,对于39位的虚拟地址,设计者认为对于运行在 RISC-V
上的应用来说,$2^{39}$字节=512GB的内存足以满足需求。而$2^{56}$的地址空间对于目前的 I/O
设备和 DRAM
芯片而言也是足够的。如果将来需要,设计者已经实现了使用48位虚拟地址的 Sv48
标准。
上图所示的是页表设备翻译虚拟地址的实际过程,一共有三步。页表是以一颗共有三层的树的形式存储在内存中的。树的根部是一个大小为 4096 字节的页面,存放了 512 个“PTE”,这些“PTE”用来索引至树的下一层。下一层同样是一个 4096 字节大小的页面,存放了 512 个“PTE”,而这些“PTE”则索引至树的最后一层。树的最后一层和其他层一样存放了 512 个“PTE”,这些“PTE”则直接索引至物理内存中的页面。虚拟地址中被用作索引的高 9 位用于在根节点页面中索引“PTE”,即目的“PTE”在根节点这一页面中的下标。同理,接下来的 9 位则是在第二层页面中的下标,再接下来的 9 位则是第三层页面中的下标。在“Sv48 RISC-V”中共有四层,虚拟地址中的第 39 − 47 位存放“PTE”在第四层的下标。
如果三个用于转换虚拟地址的 PTE
中的任何一个缺失,都会导致分页设备抛出 page-fault
异常并将其留给内核进行处理。
地址转换的三层结构使得 PTE
的记录相较于单层结构在空间上更为节省。在大多数情况下,虚拟地址不需要被映射,三层结构可以直接忽略多余的页面目录。例如,如果一个程序只用到了从下标 0 开始的几个页面,此时分页设备可以在第一层中取消为从 0 到 511 的下标分配页面目录。也就是说,从第二层开始只有一个页面目录被分配,一共省下了 511 × 512 个页面目录(第一层只有下标 0 指向下一个页面目录,其他 511 个下标均不指向下一个页面目录)。
尽管 CPU 执行读写指令时在硬件中使用了三层结构,这种做法的缺点就是 CPU 在转换地址时必须在内存中读取 3 次 PTE
。为了减少从物理内存中加载 PTE
的开销,CPU 会将 PTE
存放到一个被称作翻译旁路缓存 (Translation Look-aside Buffer
) 的地方中。
每个 PTE
都包含标志位来规定当前地址所对应的虚拟地址的使用约束。PTE_V
规定了当前 PTE
是否可用。如果这一位被设置为 0,那么指向这个页面的访问会导致一个异常(也就是不被允许)。PTE_R
规定了当前 PTE
是否可被读取;PTE_W
规定了当前 PTE
是否可以被写入;PTE_X
规定了当前 PTE
是否可能被 CPU 翻译为指令并执行;PTE_U
规定了当前 PTE
是否可以在用户态下被访问。如果这一位为 0,那么只有 CPU 处于核心态时才可以访问当前 PTE
。
为了告诉设备使用页表机制,内核必须将根页表页面的物理地址写入到 satp
寄存器中。每个 CPU 都有自己的 satp
寄存器,并用它指向的页表来转换后续指令所生成的地址。每个 CPU
都有自己的 satp
使得它们可以执行不同的指令,且每个指令都有由自己的页表所指向的私有虚拟地址空间。
通常情况下,CPU 会将所有物理地址映射到页表中,以便通过读写指令访问任意地址。由于页面目录存储在物理内存中,内核可以通过对相应地址执行写操作来更改页面目录中某个 PTE 的内容。
对于上述提到的几个名词:物理内存指的是DRAM
中的存储单元。每一个字节的物理内存都有一个地址,称为物理地址。所有指令使用的都是虚拟地址,经过页表设备转换成对应的物理地址后发送到DRAM
中进行读写操作。与物理地址和虚拟地址不同的是,虚拟内存并不是一个实际的对象,它指的是内核为了管理物理地址和虚拟地址所提供的一系列抽象和机制。
在上图中,左侧是 xv6
的内核地址空间。R
、W
和 X
分别表示读取、写入和执行权限。右侧是 xv6
所期望看到的物理地址空间。
xv6
为每个进程维护一个描述用户地址空间的页表和一个描述内核地址空间的页表。QEMU
模拟的计算机具有从地址 0x80000000
到至少 0x86400000
的内存,结束地址被声明为 PHYSTOP
。QEMU
还模拟了 I/O
设备,例如硬盘接口。QEMU
将设备接口作为位于物理地址空间 0x80000000
以下的内存映射控制寄存器开放给软件。内核可以通过对这些地址执行读写指令来与 I/O
设备进行交互。此时的读写指令与设备进行交互而不是与 RAM
交互。