从零开始写 OS (8) —— 创建页表

上一篇

本章代码对应 commit :1ebb1756d2d747756a3c6e2bd9b4cf6bebb4a112

概要

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

// in memory/mod.rs

mod paging;

pub fn init(dtb: usize) {
    ...
    remap_kernel(dtb);
}

fn remap_kernel(dtb: usize) {
    println!("remaping");
    let offset = KERNEL_OFFSET as usize - MEMORY_OFFSET as usize;
    use crate::memory::paging::{ InactivePageTable, MemoryAttr };
    let mut pg_table = InactivePageTable::new(offset);
    pg_table.set(stext as usize, etext as usize, MemoryAttr::new().set_readonly().set_execute());
    pg_table.set(sdata as usize, edata as usize, MemoryAttr::new().set_WR());
    pg_table.set(srodata as usize, erodata as usize, MemoryAttr::new().set_readonly());
    pg_table.set(sbss as usize, ebss as usize, MemoryAttr::new().set_WR());
    pg_table.set(bootstack as usize, bootstacktop as usize, MemoryAttr::new().set_WR());
    pg_table.set(dtb, dtb + MAX_DTB_SIZE, MemoryAttr::new().set_WR());
    unsafe {
        pg_table.activate();
    }
}

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

// in memort/paging.rs

pub struct MemoryAttr(u32);

impl MemoryAttr {
    pub fn new() -> MemoryAttr {
        MemoryAttr(1)
    }

    pub fn set_readonly(mut self) -> MemoryAttr {
        self.0 = self.0 | 2; // 1 << 1
        self
    }

    pub fn set_execute(mut self) -> MemoryAttr {
        self.0 = self.0 | 8; // 1 << 3
        self
    }

    pub fn set_WR(mut self) -> MemoryAttr {
        self.0 = self.0 | 2 | 4;
        self
    }
}
由于我们创建的页表需要是有效(valid)的,所以 new 函数中使用 1 进行初始化

页表结构体

pub struct InactivePageTable {
    root_table: Frame,
    PDEs: [Option<Frame>; 1024],
    offset: usize,
}

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

impl InactivePageTable {
    pub fn new(_offset: usize) -> InactivePageTable {
        if let Some(_root_table) = alloc_frame() {
            return InactivePageTable {
                root_table: _root_table,
                PDEs: [None; 1024],
                offset: _offset,
            }
        } else {
            panic!("oom");
        }
    }
}

首先我们给根页表分配一个页面大小的物理内存,以后可以作为长度为 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 为一级页表项。

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

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

fn get_PDX(addr: usize) -> usize {
    addr >> 22
}

fn get_PTX(addr: usize) -> usize {
    (addr >> 12) & 0x3ff
}

页面重映射

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

impl InactivePageTable {
    fn pgtable_paddr(&mut self) -> usize {
        self.root_table.start_address().as_usize()
    }

    fn pgtable_vaddr(&mut self) -> usize {
        self.pgtable_paddr() + self.offset
    }

    pub fn set(&mut self, start: usize, end: usize, attr: MemoryAttr) {
        unsafe {
            let mut vaddr = start & !0xfff; // 4K 对齐
            let pg_table = &mut *(self.pgtable_vaddr() as *mut [u32; 1024]);
            while vaddr < end {
                // 1-1. 通过页目录和 VPN[1] 找到所需页目录项
                let PDX = get_PDX(vaddr);
                let PDE = pg_table[PDX];
                // 1-2. 若不存在则创建
                if PDE == 0 {
                    self.PDEs[PDX] = alloc_frame();
                    let PDE_PPN = self.PDEs[PDX].unwrap().start_address().as_usize() >> 12;
                    pg_table[PDX] = (PDE_PPN << 10) as u32 | 0x1; // pointer to next level of page table
                }
                // 2. 页目录项包含了叶结点页表(简称页表)的起始地址,通过页目录项找到页表
                let pg_table_paddr = (pg_table[PDX] & (!0x3ff)) << 2;
                // 3. 通过页表和 VPN[0] 找到所需页表项
                // 4. 设置页表项包含的页面的起始物理地址和相关属性
                let pg_table_2 = &mut *((pg_table_paddr as usize + self.offset) as *mut [u32; 1024]);
                pg_table_2[get_PTX(vaddr)] = ((vaddr - self.offset) >> 2) as u32 | attr.0;
                vaddr += (1 << 12);
            }
        }
    }
}

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

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

use riscv::asm::sfence_vma_all;

impl InactivePageTable {
    unsafe fn set_root_table(root_table: usize) { // 设置satp
        asm!("csrw satp, $0" :: "r"(root_table) :: "volatile");
    }

    unsafe fn flush_tlb() {
        sfence_vma_all();
    }

    pub unsafe fn activate(&mut self) {
        Self::set_root_table((self.pgtable_paddr() >> 12) | (1 << 31));
        Self::flush_tlb();
    }
}

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

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

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

预告

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

下一篇

编辑于 2019-05-07 18:56