7. 创建页表

本章代码对应 commit :f34aba219f9a336d0bad562f217d62d8bf2e38a6

概要

bbl 中创建的页表其实并不完善,所以本章我们将自己动手建立页表(remap kernel)。这个过程分为以下几步:

  1. 获取需要重新映射的内存范围(虚拟地址)。

  2. 设置页面属性。

  3. 设置页表,将虚拟地址映射至目标物理地址。

重新映射范围

首先我们需要获取需要重新映射内存的虚拟地址范围:

// in memory/mod.rs

extern "C" {
    // text
    fn stext();
    fn etext();
    // data
    fn sdata();
    fn edata();
    // read only
    fn srodata();
    fn erodata();
    // bss
    fn sbss();
    fn ebss();
    // kernel
    fn start();
    fn end();
    // boot
    fn bootstack();
    fn bootstacktop();
}

这些函数赋值由 boot/linker.ld 完成,这里将他们作为 usize 使用。

设置页面属性

首先,创建文件 memory/paging.rs 。然后修改 memory/mod.rs

这里涉及了两个尚未创建的结构体,首先我们来实现 MemoryAttr 。在 页表简介 中我们介绍过 页表项/页目录项 的结构,我们只需要根据其结构设置相关属性即可:

pte_riscv32

由于我们创建的页表需要是有效(valid)的,所以 new 函数中使用 1 进行初始化

页表结构体

该结构体包含了根页表的物理地址,根页表的页目录项。由于我们采用线性映射,所以我们还需要保存线性映射的 offset 。

首先我们给根页表分配一个页面大小的物理内存,以后可以作为长度为 1024 的 u32 数组使用。每个 u32 就是一个页目录项。 PDEs 为页目录项指向的页表的物理地址,用 None 初始化。

地址虚实转换

页面重新映射的过程与虚拟地址通过页表转换为物理地址的过程相似,我们先通过图示看看虚拟地址转换为物理地址的过程:

  1. 通过 satp 获取根页表(页目录)的基址(satp.PPN),VPN[1]给出了二级页号,因此处理器会读取位于地址 satp.PPN * PAGE_SIZE + VPN[1] * PTE_SIZE 的页目录项(pde)。

  2. 该 pde 包含一级页表的物理地址,VPN[0]给出了一级页号,因此处理器读取位于地址 pde.PPN * PAGE_SIZE + VPN[0] * PTE_SIZE 的页表项(pte)。

  3. pte 的 PPN 字段和页内偏移(原始虚拟地址的低 12 位)组成了最终的物理地址:pte.PPN * PAGE_SIZE + page_offset

在 riscv32 中,PAGE_SIZE = 4096,PTE_SIZE = sizeof(u32) = 4

该过程可由下图表示。本文使用页表项(PTE)和页目录项(PDE)来区分一级页表和二级页表项,但是实际上他们的结构是相同的,所以图中将二者均写为 PTE 。左边的 PTE 为二级页表项,右边的 PTE 为一级页表项。

VA2PA_riscv32

注意,物理地址长度为 34 ,而虚拟地址长度为 32

为了获取 VPN[1] 和 VPN[0] 编写以下函数:

页面重映射

地址虚实转换过程编写页面的重新映射:

映射的过程已写于注释中,这里不再赘述。

最后,让我们来完成启用页表的函数:

这里我们通过将新页表的物理地址写入 satp 寄存器,达到切换页表的目的。tlb 可以理解为页表的缓存,用以加快虚拟地址转换为物理地址的速度。所以在切换页表之后需要通过 flush_tlb 清空缓存。

执行 make run ,屏幕正常而规律的输出着 100 ticks! 。虽然看起来与之前并无不同,但这依然是一件令人感到兴奋的事情。

没出 bug 难道不是一件值得兴奋的实现吗。。。

预告

多线程并发技术使得计算机能够“同时”执行多个线程,从而提升整体性能。下一章我们将实现内核线程的创建,并完成线程的切换。

Last updated

Was this helpful?