内核重映射实现之一:页表
首先我们来看如何实现页表。
访问物理内存
简单起见,无论是初始映射还是重映射,无论是内核各段还是物理内存,我们都采用同样的偏移量进行映射,具体而言:va -> pa = va - 0xffffffff40000000 。
于是我们可以通过在内核中访问对应的虚拟内存来访问物理内存。相关常量定义在consts.rs中。
1. // src/consts.rs
3. pub const PHYSICAL_MEMORY_OFFSET: usize = 0xffffffff40000000;
5. // src/memory/mod.rs
7. // 将物理地址转化为对应的虚拟地址
8. pub fn access_pa_via_va(pa: usize) -> usize {
9. pa + PHYSICAL_MEMORY_OFFSET
10. }
在 riscv crate 和内核实现中,需要为页表机制提供了如下支持:
- 基于偏移量(也即线性映射)的 Sv39 三级页表
Rv39PageTable和页表映射操作PageTableImpl - 页表项
PageTableEntry和页项PageEntry - 页表项数组
PageTable - 页表项中的标志位
PageTableFlags
页表项和页项
首先来看一下页表项:
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);
5. impl PageEntry {
6. pub fn update(&mut self) {
7. unsafe { sfence_vma(0, self.1.start_address().as_usize()); }
8. }
9. // 一系列的标志位读写
10. pub fn accessed(&self) -> bool { self.0.flags().contains(EF::ACCESSED) }
11. pub fn clear_accessed(&mut self) { self.0.flags_mut().remove(EF::ACCESSED); }
12. ......
13. }
我们基于提供的类 PageTableEntry 自己封装了一个 PageEntry ,表示单个映射。里面分别保存了一个页表项 PageTableEntry 的可变引用,以及找到了这个页表项的虚拟页。但事实上,除了 update 函数之外,剩下的函数都是对 PageTableEntry 的简单包装,功能是读写页表项的目标物理页号以及标志位。
我们之前提到过,在修改页表之后我们需要通过屏障指令 sfence.vma 来刷新 TLB 。而这条指令后面可以接一个虚拟地址,这样在刷新的时候只关心与这个虚拟地址相关的部分,可能速度比起全部刷新要快一点。(实际上我们确实用了这种较快的刷新 TLB 方式,但并不是在这里使用,因此 update 根本没被调用过,这个类有些冗余了)
为 Rv39PageTable 提供物理页帧管理
在实现页表之前,我们回忆多级页表的修改会隐式的调用物理页帧分配与回收。比如在 Sv39 中,插入一对映射就可能新建一个二级页表和一个一级页表,而这需要分配两个物理页帧。因此,我们需要告诉 Rv39PageTable 如何进行物理页帧分配与回收。
1. // src/memory/paging.rs
3. // 事实上,我们需要一个实现了 FrameAllocator, FrameDeallocator trait的类
4. // 并为此分别实现 alloc, dealloc 函数
5. struct FrameAllocatorForPaging;
7. impl FrameAllocator for FrameAllocatorForPaging {
8. fn alloc(&mut self) -> Option<Frame> {
9. alloc_frame()
10. }
11. }
13. impl FrameDeallocator for FrameAllocatorForPaging {
14. fn dealloc(&mut self, frame: Frame) {
15. dealloc_frame(frame)
16. }
17. }
实现我们自己的页表 映射操作 PageTableImpl
于是我们可以利用 Rv39PageTable的实现我们自己的页表映射操作 PageTableImpl 。首先是声明及初始化:
1. // src/memory/paging.rs
3. pub struct PageTableImpl {
4. page_table: Rv39PageTable<'static>,
5. // 作为根的三级页表所在的物理页帧
6. root_frame: Frame,
7. // 在操作过程中临时使用
8. entry: Option<PageEntry>,
9. }
11. impl PageTableImpl {
12. // 新建一个空页表
13. pub fn new_bare() -> Self {
14. // 分配一个物理页帧并获取物理地址,作为根的三级页表就放在这个物理页帧中
15. let frame = alloc_frame().expect("alloc_frame failed!");
16. let paddr = frame.start_address().as_usize();
17. // 利用 access_pa_via_va 访问该物理页帧并进行页表初始化
18. let table = unsafe { &mut *(access_pa_via_va(paddr) as *mut PageTableEntryArray) };
19. table.zero();
21. PageTableImpl {
22. // 传入参数:三级页表的可变引用;
23. // 因为 Rv39PageTable 的思路也是将整块物理内存进行线性映射
24. // 所以我们传入物理内存的偏移量,即 va-pa,使它可以修改页表
25. page_table: Rv39PageTable::new(table, PHYSICAL_MEMORY_OFFSET),
26. // 三级页表所在物理页帧
27. root_frame: frame,
28. entry: None
29. }
30. }
31. }
然后是页表最重要的插入、删除映射的功能:
1. impl PageTableImpl {
2. ...
3. pub fn map(&mut self, va: usize, pa: usize) -> &mut PageEntry {
4. // 为一对虚拟页与物理页帧建立映射
6. // 这里的标志位被固定为 R|W|X,即同时允许读/写/执行
7. // 后面我们会根据段的权限不同进行修改
8. let flags = EF::VALID | EF::READABLE | EF::WRITABLE;
9. let page = Page::of_addr(VirtAddr::new(va));
10. let frame = Frame::of_addr(PhysAddr::new(pa));
11. self.page_table
12. // 利用 Rv39PageTable 的 map_to 接口
13. // 传入要建立映射的虚拟页、物理页帧、映射标志位、以及提供物理页帧管理
14. .map_to(page, frame, flags, &mut FrameAllocatorForPaging)
15. .unwrap()
16. // 得到 MapperFlush(Page)
17. // flush 做的事情就是跟上面一样的 sfence_vma
18. // 即刷新与这个虚拟页相关的 TLB
19. // 所以我们修改后要按时刷新 TLB
20. .flush();
21. self.get_entry(va).expect("fail to get an entry!")
22. }
23. pub fn unmap(&mut self, va: usize) {
24. // 删除一对映射
25. // 我们只需输入虚拟页,因为已经可以找到页表项了
26. let page = Page::of_addr(VirtAddr::new(va));
27. // 利用 Rv39PageTable 的 unmap 接口
28. // * 注意这里没有用到物理页帧管理,所以 Rv39PageTable 并不会回收内存?
29. let (_, flush) = self.page_table.unmap(page).unwrap();
30. // 同样注意按时刷新 TLB
31. flush.flush();
32. }
33. fn get_entry(&mut self, va: usize) -> Option<&mut PageEntry> {
34. // 获取虚拟页对应的页表项,以被我们封装起来的 PageEntry 的可变引用的形式
35. // 于是,我们拿到了页表项,可以进行修改了!
36. let page = Page::of_addr(VirtAddr::new(va));
37. // 调用 Rv39PageTable 的 ref_entry 接口
38. if let Ok(e) = self.page_table.ref_entry(page.clone()) {
39. let e = unsafe { &mut *(e as *mut PageTableEntry) };
40. // 把返回的 PageTableEntry 封装起来
41. self.entry = Some(PageEntry(e, page));
42. Some(self.entry.as_mut().unwrap())
43. }
44. else {
45. None
46. }
47. }
48. }
上面我们创建页表,并可以插入、删除映射了。但是它依然一动不动的放在内存中,如何将它用起来呢?我们可以通过修改 satp 寄存器的物理页号字段来设置作为根的三级页表所在的物理页帧,也就完成了页表的切换。
1. impl PageTableImpl {
2. ...
3. // 我们用 token 也就是 satp 的值来描述一个页表
4. // 返回自身的 token
5. pub fn token(&self) -> usize { self.root_frame.number() | (8 << 60) }
7. // 使用内联汇编将 satp 寄存器修改为传进来的 token
8. // 这个 token 对应的页表将粉墨登场...
9. unsafe fn set_token(token: usize) {
10. asm!("csrw satp, $0" :: "r"(token) :: "volatile");
11. }
13. // 查看 CPU 当前的 satp 值,就知道 CPU 目前在用哪个页表
14. fn active_token() -> usize { satp::read().bits() }
16. // 修改 satp 值切换页表后,过时的不止一个虚拟页
17. // 因此必须使用 sfence_vma_all 刷新整个 TLB
18. fn flush_tlb() { unsafe { sfence_vma_all(); } }
20. // 将 CPU 所用的页表切换为当前的实例
21. pub unsafe fn activate(&self) {
22. let old_token = Self::active_token();
23. let new_token = self.token();
24. println!("switch satp from {:#x} to {:#x}", old_token, new_token);
25. if new_token != old_token {
26. Self::set_token(new_token);
27. // 别忘了刷新 TLB!
28. Self::flush_tlb();
29. }
30. }
31. }
