简单起见,无论是初始映射还是重映射,无论是内核各段还是物理内存,我们都采用同样的偏移量进行映射,具体而言: 。

    于是我们可以通过在内核中访问对应的虚拟内存来访问物理内存。相关常量定义在consts.rs中。

    在 和内核实现中,需要为页表机制提供了如下支持:

    1. // riscv: src/paging/page_table.rs
    2. pub struct PageTableEntry(usize);
    3. impl PageTableEntry {
    4. pub fn is_unused(&self) -> bool { self.0 == 0 }
    5. pub fn set_unused(&mut self) { self.0 = 0; }
    6. ......
    7. }

    再来看一下页项:

    我们基于提供的类 PageTableEntry 自己封装了一个 PageEntry ,表示单个映射。里面分别保存了一个页表项 PageTableEntry 的可变引用,以及找到了这个页表项的虚拟页。但事实上,除了 update 函数之外,剩下的函数都是对 PageTableEntry 的简单包装,功能是读写页表项的目标物理页号以及标志位。

    我们之前提到过,在修改页表之后我们需要通过屏障指令 sfence.vma 来刷新 TLB 。而这条指令后面可以接一个虚拟地址,这样在刷新的时候只关心与这个虚拟地址相关的部分,可能速度比起全部刷新要快一点。(实际上我们确实用了这种较快的刷新 TLB 方式,但并不是在这里使用,因此 update 根本没被调用过,这个类有些冗余了)

    1. // src/memory/paging.rs
    2. // 事实上,我们需要一个实现了 FrameAllocator, FrameDeallocator trait的类
    3. // 并为此分别实现 alloc, dealloc 函数
    4. struct FrameAllocatorForPaging;
    5. impl FrameAllocator for FrameAllocatorForPaging {
    6. alloc_frame()
    7. }
    8. impl FrameDeallocator for FrameAllocatorForPaging {
    9. fn dealloc(&mut self, frame: Frame) {
    10. dealloc_frame(frame)
    11. }
    12. }

    于是我们可以利用 Rv39PageTable的实现我们自己的页表映射操作 PageTableImpl 。首先是声明及初始化:

    然后是页表最重要的插入、删除映射的功能:

    1. impl PageTableImpl {
    2. ...
    3. pub fn map(&mut self, va: usize, pa: usize) -> &mut PageEntry {
    4. // 为一对虚拟页与物理页帧建立映射
    5. // 这里的标志位被固定为 R|W|X,即同时允许读/写/执行
    6. // 后面我们会根据段的权限不同进行修改
    7. let flags = EF::VALID | EF::READABLE | EF::WRITABLE;
    8. let page = Page::of_addr(VirtAddr::new(va));
    9. let frame = Frame::of_addr(PhysAddr::new(pa));
    10. self.page_table
    11. // 利用 Rv39PageTable 的 map_to 接口
    12. // 传入要建立映射的虚拟页、物理页帧、映射标志位、以及提供物理页帧管理
    13. .map_to(page, frame, flags, &mut FrameAllocatorForPaging)
    14. .unwrap()
    15. // 得到 MapperFlush(Page)
    16. // flush 做的事情就是跟上面一样的 sfence_vma
    17. // 即刷新与这个虚拟页相关的 TLB
    18. .flush();
    19. }
    20. pub fn unmap(&mut self, va: usize) {
    21. // 删除一对映射
    22. // 我们只需输入虚拟页,因为已经可以找到页表项了
    23. let page = Page::of_addr(VirtAddr::new(va));
    24. // 利用 Rv39PageTable 的 unmap 接口
    25. // * 注意这里没有用到物理页帧管理,所以 Rv39PageTable 并不会回收内存?
    26. let (_, flush) = self.page_table.unmap(page).unwrap();
    27. // 同样注意按时刷新 TLB
    28. flush.flush();
    29. }
    30. fn get_entry(&mut self, va: usize) -> Option<&mut PageEntry> {
    31. // 获取虚拟页对应的页表项,以被我们封装起来的 PageEntry 的可变引用的形式
    32. // 于是,我们拿到了页表项,可以进行修改了!
    33. let page = Page::of_addr(VirtAddr::new(va));
    34. // 调用 Rv39PageTable 的 ref_entry 接口
    35. if let Ok(e) = self.page_table.ref_entry(page.clone()) {
    36. let e = unsafe { &mut *(e as *mut PageTableEntry) };
    37. // 把返回的 PageTableEntry 封装起来
    38. self.entry = Some(PageEntry(e, page));
    39. Some(self.entry.as_mut().unwrap())
    40. }
    41. else {
    42. None
    43. }
    44. }

    上面我们创建页表,并可以插入、删除映射了。但是它依然一动不动的放在内存中,如何将它用起来呢?我们可以通过修改 寄存器的物理页号字段来设置作为根的三级页表所在的物理页帧,也就完成了页表的切换。