内核重映射实现之一:页表

首先我们来看如何实现页表。

访问物理内存

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

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

  1. // src/consts.rs
  2. pub const PHYSICAL_MEMORY_OFFSET: usize = 0xffffffff40000000;
  3. // src/memory/mod.rs
  4. // 将物理地址转化为对应的虚拟地址
  5. pub fn access_pa_via_va(pa: usize) -> usize {
  6. pa + PHYSICAL_MEMORY_OFFSET
  7. }

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

页表项和页项

首先来看一下页表项:

  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. }

再来看一下页项:

  1. // src/memory/paging.rs
  2. ......
  3. pub struct PageEntry(&'static mut PageTableEntry, Page);
  4. impl PageEntry {
  5. pub fn update(&mut self) {
  6. unsafe { sfence_vma(0, self.1.start_address().as_usize()); }
  7. }
  8. // 一系列的标志位读写
  9. pub fn accessed(&self) -> bool { self.0.flags().contains(EF::ACCESSED) }
  10. pub fn clear_accessed(&mut self) { self.0.flags_mut().remove(EF::ACCESSED); }
  11. ......
  12. }

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

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

为 Rv39PageTable 提供物理页帧管理

在实现页表之前,我们回忆多级页表的修改会隐式的调用物理页帧分配与回收。比如在 Sv39 中,插入一对映射就可能新建一个二级页表和一个一级页表,而这需要分配两个物理页帧。因此,我们需要告诉 Rv39PageTable 如何进行物理页帧分配与回收。

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

实现我们自己的页表 映射操作 PageTableImpl

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

  1. // src/memory/paging.rs
  2. pub struct PageTableImpl {
  3. page_table: Rv39PageTable<'static>,
  4. // 作为根的三级页表所在的物理页帧
  5. root_frame: Frame,
  6. // 在操作过程中临时使用
  7. entry: Option<PageEntry>,
  8. }
  9. impl PageTableImpl {
  10. // 新建一个空页表
  11. pub fn new_bare() -> Self {
  12. // 分配一个物理页帧并获取物理地址,作为根的三级页表就放在这个物理页帧中
  13. let frame = alloc_frame().expect("alloc_frame failed!");
  14. let paddr = frame.start_address().as_usize();
  15. // 利用 access_pa_via_va 访问该物理页帧并进行页表初始化
  16. let table = unsafe { &mut *(access_pa_via_va(paddr) as *mut PageTableEntryArray) };
  17. table.zero();
  18. PageTableImpl {
  19. // 传入参数:三级页表的可变引用;
  20. // 因为 Rv39PageTable 的思路也是将整块物理内存进行线性映射
  21. // 所以我们传入物理内存的偏移量,即 va-pa,使它可以修改页表
  22. page_table: Rv39PageTable::new(table, PHYSICAL_MEMORY_OFFSET),
  23. // 三级页表所在物理页帧
  24. root_frame: frame,
  25. entry: None
  26. }
  27. }
  28. }

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

  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. // 所以我们修改后要按时刷新 TLB
  19. .flush();
  20. self.get_entry(va).expect("fail to get an entry!")
  21. }
  22. pub fn unmap(&mut self, va: usize) {
  23. // 删除一对映射
  24. // 我们只需输入虚拟页,因为已经可以找到页表项了
  25. let page = Page::of_addr(VirtAddr::new(va));
  26. // 利用 Rv39PageTable 的 unmap 接口
  27. // * 注意这里没有用到物理页帧管理,所以 Rv39PageTable 并不会回收内存?
  28. let (_, flush) = self.page_table.unmap(page).unwrap();
  29. // 同样注意按时刷新 TLB
  30. flush.flush();
  31. }
  32. fn get_entry(&mut self, va: usize) -> Option<&mut PageEntry> {
  33. // 获取虚拟页对应的页表项,以被我们封装起来的 PageEntry 的可变引用的形式
  34. // 于是,我们拿到了页表项,可以进行修改了!
  35. let page = Page::of_addr(VirtAddr::new(va));
  36. // 调用 Rv39PageTable 的 ref_entry 接口
  37. if let Ok(e) = self.page_table.ref_entry(page.clone()) {
  38. let e = unsafe { &mut *(e as *mut PageTableEntry) };
  39. // 把返回的 PageTableEntry 封装起来
  40. self.entry = Some(PageEntry(e, page));
  41. Some(self.entry.as_mut().unwrap())
  42. }
  43. else {
  44. None
  45. }
  46. }
  47. }

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

  1. impl PageTableImpl {
  2. ...
  3. // 我们用 token 也就是 satp 的值来描述一个页表
  4. // 返回自身的 token
  5. pub fn token(&self) -> usize { self.root_frame.number() | (8 << 60) }
  6. // 使用内联汇编将 satp 寄存器修改为传进来的 token
  7. // 这个 token 对应的页表将粉墨登场...
  8. unsafe fn set_token(token: usize) {
  9. asm!("csrw satp, $0" :: "r"(token) :: "volatile");
  10. }
  11. // 查看 CPU 当前的 satp 值,就知道 CPU 目前在用哪个页表
  12. fn active_token() -> usize { satp::read().bits() }
  13. // 修改 satp 值切换页表后,过时的不止一个虚拟页
  14. // 因此必须使用 sfence_vma_all 刷新整个 TLB
  15. fn flush_tlb() { unsafe { sfence_vma_all(); } }
  16. // 将 CPU 所用的页表切换为当前的实例
  17. pub unsafe fn activate(&self) {
  18. let old_token = Self::active_token();
  19. let new_token = self.token();
  20. println!("switch satp from {:#x} to {:#x}", old_token, new_token);
  21. if new_token != old_token {
  22. Self::set_token(new_token);
  23. // 别忘了刷新 TLB!
  24. Self::flush_tlb();
  25. }
  26. }
  27. }