简单起见,无论是初始映射还是重映射,无论是内核各段还是物理内存,我们都采用同样的偏移量进行映射,具体而言: 。
于是我们可以通过在内核中访问对应的虚拟内存来访问物理内存。相关常量定义在consts.rs中。
在 和内核实现中,需要为页表机制提供了如下支持:
- 基于偏移量(也即线性映射)的 Sv39 三级页表
Rv39PageTable
和 - 页表项
PageTableEntry
和 - 页表项数组
PageTable
// riscv: src/paging/page_table.rs
pub struct PageTableEntry(usize);
impl PageTableEntry {
pub fn is_unused(&self) -> bool { self.0 == 0 }
pub fn set_unused(&mut self) { self.0 = 0; }
......
}
再来看一下页项:
我们基于提供的类 PageTableEntry
自己封装了一个 PageEntry
,表示单个映射。里面分别保存了一个页表项 PageTableEntry
的可变引用,以及找到了这个页表项的虚拟页。但事实上,除了 update
函数之外,剩下的函数都是对 PageTableEntry
的简单包装,功能是读写页表项的目标物理页号以及标志位。
我们之前提到过,在修改页表之后我们需要通过屏障指令 sfence.vma
来刷新 TLB
。而这条指令后面可以接一个虚拟地址,这样在刷新的时候只关心与这个虚拟地址相关的部分,可能速度比起全部刷新要快一点。(实际上我们确实用了这种较快的刷新 TLB 方式,但并不是在这里使用,因此 update
根本没被调用过,这个类有些冗余了)
// src/memory/paging.rs
// 事实上,我们需要一个实现了 FrameAllocator, FrameDeallocator trait的类
// 并为此分别实现 alloc, dealloc 函数
struct FrameAllocatorForPaging;
impl FrameAllocator for FrameAllocatorForPaging {
alloc_frame()
}
impl FrameDeallocator for FrameAllocatorForPaging {
fn dealloc(&mut self, frame: Frame) {
dealloc_frame(frame)
}
}
于是我们可以利用 Rv39PageTable
的实现我们自己的页表映射操作 PageTableImpl
。首先是声明及初始化:
然后是页表最重要的插入、删除映射的功能:
impl PageTableImpl {
...
pub fn map(&mut self, va: usize, pa: usize) -> &mut PageEntry {
// 为一对虚拟页与物理页帧建立映射
// 这里的标志位被固定为 R|W|X,即同时允许读/写/执行
// 后面我们会根据段的权限不同进行修改
let flags = EF::VALID | EF::READABLE | EF::WRITABLE;
let page = Page::of_addr(VirtAddr::new(va));
let frame = Frame::of_addr(PhysAddr::new(pa));
self.page_table
// 利用 Rv39PageTable 的 map_to 接口
// 传入要建立映射的虚拟页、物理页帧、映射标志位、以及提供物理页帧管理
.map_to(page, frame, flags, &mut FrameAllocatorForPaging)
.unwrap()
// 得到 MapperFlush(Page)
// flush 做的事情就是跟上面一样的 sfence_vma
// 即刷新与这个虚拟页相关的 TLB
.flush();
}
pub fn unmap(&mut self, va: usize) {
// 删除一对映射
// 我们只需输入虚拟页,因为已经可以找到页表项了
let page = Page::of_addr(VirtAddr::new(va));
// 利用 Rv39PageTable 的 unmap 接口
// * 注意这里没有用到物理页帧管理,所以 Rv39PageTable 并不会回收内存?
let (_, flush) = self.page_table.unmap(page).unwrap();
// 同样注意按时刷新 TLB
flush.flush();
}
fn get_entry(&mut self, va: usize) -> Option<&mut PageEntry> {
// 获取虚拟页对应的页表项,以被我们封装起来的 PageEntry 的可变引用的形式
// 于是,我们拿到了页表项,可以进行修改了!
let page = Page::of_addr(VirtAddr::new(va));
// 调用 Rv39PageTable 的 ref_entry 接口
if let Ok(e) = self.page_table.ref_entry(page.clone()) {
let e = unsafe { &mut *(e as *mut PageTableEntry) };
// 把返回的 PageTableEntry 封装起来
self.entry = Some(PageEntry(e, page));
Some(self.entry.as_mut().unwrap())
}
else {
None
}
}
上面我们创建页表,并可以插入、删除映射了。但是它依然一动不动的放在内存中,如何将它用起来呢?我们可以通过修改 寄存器的物理页号字段来设置作为根的三级页表所在的物理页帧,也就完成了页表的切换。