[rCore学习笔记 032] 管理SV39多级页表
上一节:硬件
本节:软件实现
物理页帧管理
内核->空闲内存->物理页帧->分配->存放
可用物理页的分配和回收
Link文件中关于操作系统bin文件的内存设计
内容os\src\linker-qemu.ld:
OUTPUT_ARCH(riscv)
ENTRY(_start)
BASE_ADDRESS = 0x80200000;
SECTIONS
{
. = BASE_ADDRESS;
skernel = .;
stext = .;
.text : {
*(.text.entry)
. = ALIGN(4K);
strampoline = .;
*(.text.trampoline);
. = ALIGN(4K);
*(.text .text.*)
}
. = ALIGN(4K);
etext = .;
srodata = .;
.rodata : {
*(.rodata .rodata.*)
*(.srodata .srodata.*)
}
. = ALIGN(4K);
erodata = .;
sdata = .;
.data : {
*(.data .data.*)
*(.sdata .sdata.*)
}
. = ALIGN(4K);
edata = .;
sbss_with_stack = .;
.bss : {
*(.bss.stack)
sbss = .;
*(.bss .bss.*)
*(.sbss .sbss.*)
}
. = ALIGN(4K);
ebss = .;
ekernel = .;
/DISCARD/ : {
*(.eh_frame)
}
}

可以看到我们可以分配的内从从ekernel开始,而从哪里结束取决于我们的物理设备,这里因为学习的是K210,那么选择的实际上是8MiB的内存,也就是从0x80000000到0x80800000.
这里在子模块os\src\boards\qemu.rs里有:
// os/src/config.rs
pub const MEMORY_END: usize = 0x80800000;
那么我们在这里就是要实现一个页帧内存分配器来管理这段内存,首先我们先将初始地址和结束地址转换为页号:

实现代码:
// os\src\mm\frame_allocator.rs
pub struct StackFrameAllocator {
current: usize,
end: usize,
recycled: Vec<usize>,
}
impl StackFrameAllocator {
pub fn init(&mut self, l: PhysPageNum, r: PhysPageNum) {
self.current = l.0;
self.end = r.0;
}
}
impl FrameAllocator for StackFrameAllocator {
fn new() -> Self {
Self {
current: 0,
end: 0,
recycled: Vec::new(),
}
}
fn alloc(&mut self) -> Option<PhysPageNum> {
if let Some(ppn) = self.recycled.pop() {
Some(ppn.into())
} else if self.current == self.end {
None
} else {
self.current += 1;
Some((self.current - 1).into())
}
}
fn dealloc(&mut self, ppn: PhysPageNum) {
let ppn = ppn.0;
// validity check
if ppn >= self.current || self.recycled.iter().any(|&v| v == ppn) {
panic!("Frame ppn={:#x} has not been allocated!", ppn);
}
// recycle
self.recycled.push(ppn);
}
}
这里注意
Vec的成功使用少不了前一部分的堆内存分配器.

为了保证每个物理页帧在被创建的时候清空页帧内容,在被销毁的时候能够自动回收.这里借用了RAII的思想.
RAII(Resource Acquisition Is Initialization) 资源和对象的声明周期一致的一种思想.
创建的时候初始化,Drop的时候进行回调
使用DropTrait来进行管理
为PhysPageNum创建一个包裹Tracker意为追踪器:
/// manage a frame which has the same lifecycle as the tracker
pub struct FrameTracker {
pub ppn: PhysPageNum,
}
impl FrameTracker {
pub fn new(ppn: PhysPageNum) -> Self {
// page cleaning
let bytes_array = ppn.get_bytes_array();
for i in bytes_array {
*i = 0;
}
Self { ppn }
}
}
impl Debug for FrameTracker {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_fmt(format_args!("FrameTracker:PPN={:#x}", self.ppn.0))
}
}
impl Drop for FrameTracker {
fn drop(&mut self) {
frame_dealloc(self.ppn);
}
}
这样在它的生命周期结束的时候就Drop掉,同样让这个页帧也回收.
因此,我们对外的程序接口应该返回的是FrameTracker而不是PhysPageNum.
type FrameAllocatorImpl = StackFrameAllocator;
lazy_static! {
/// frame allocator instance through lazy_static!
pub static ref FRAME_ALLOCATOR: UPSafeCell<FrameAllocatorImpl> =
unsafe { UPSafeCell::new(FrameAllocatorImpl::new()) };
}
/// initiate the frame allocator using `ekernel` and `MEMORY_END`
pub fn init_frame_allocator() {
unsafe extern "C" {
safe fn ekernel();
}
FRAME_ALLOCATOR.exclusive_access().init(
PhysAddr::from(ekernel as usize).ceil(),
PhysAddr::from(MEMORY_END).floor(),
);
}
/// allocate a frame
pub fn frame_alloc() -> Option<FrameTracker> {
FRAME_ALLOCATOR
.exclusive_access()
.alloc()
.map(FrameTracker::new)
}
/// deallocate a frame
fn frame_dealloc(ppn: PhysPageNum) {
FRAME_ALLOCATOR.exclusive_access().dealloc(ppn);
}
可以看到,这里创建了一个FRAME_ALLOCATOR单例,并且用UPSafeCell保证其线程安全,实现了init,alloc和dealloc的接口.
UPSafeCell是在RefCell的基础上,对获取一个非mut也做了限制,也就是读写都强制使用mut,以达到单核心线程安全.
多级页表管理
回望上一节,页表每个节点恰好存储在一个物理页帧中
它的位置可以用一个物理页号来表示
如下图所示,其实可以通过PNN访问某个页,以:
- 页表项的形式读出
- 以
u8的形式读出 - 拓展一下思维,其实可以以任何大小的形式读出

// os\src\mm\address.rs
impl PhysPageNum {
pub fn get_pte_array(&self) -> &'static mut [PageTableEntry] {
let pa: PhysAddr = (*self).into();
unsafe { core::slice::from_raw_parts_mut(pa.0 as *mut PageTableEntry, 512) }
}
pub fn get_bytes_array(&self) -> &'static mut [u8] {
let pa: PhysAddr = (*self).into();
unsafe { core::slice::from_raw_parts_mut(pa.0 as *mut u8, 4096) }
}
pub fn get_mut<T>(&self) -> &'static mut T {
let pa: PhysAddr = (*self).into();
unsafe { (pa.0 as *mut T).as_mut().unwrap() }
}
}
这里要注意,要想直接访问物理地址,必须是使用
M特权级.
但是我们使用的是S和U特权级,因此,需要一个恒等映射来解决这个问题.
这里恒等映射,其实是存在一个小问题的因为我们物理地址正常是56位,但是虚拟地址是39位,理论上会出问题.
但是我们的内存从[0x8000_0000,0x8080_0000)恰好弥补了这一点,一共就涉及32位嘛.
如下图所示,恒等映射就是把PPN和VPN用下一节提到的map映射起来,可以看到8MiB的映射需要约16KiB的页表项(忽略一级和二级页表损耗), 这里总结下来就是原本大小除以512 就是页表项的损耗.

注意这个
L1 3Bit,只能有000,001,010,011四种状态,要注意是左闭右开的区间,4*4KiB=16KiB.
建立和拆除虚实地址映射关系
实际上就是建立: 虚拟地址 -> 物理地址的接口和映射.

实现方法:
// os\src\mm\page_table.rs
fn find_pte_create(&mut self, vpn: VirtPageNum) -> Option<&mut PageTableEntry> {
let idxs = vpn.indexes();
let mut ppn = self.root_ppn;
let mut result: Option<&mut PageTableEntry> = None;
for (i, idx) in idxs.iter().enumerate() {
let pte = &mut ppn.get_pte_array()[*idx];
if i == 2 {
result = Some(pte);
break;
}
if !pte.is_valid() {
let frame = frame_alloc().unwrap();
*pte = PageTableEntry::new(frame.ppn, PTEFlags::V);
self.frames.push(frame);
}
ppn = pte.ppn();
}
result
}
对应的VPN转PPN的方法:
// os\src\mm\page_table.rs
pub fn map(&mut self, vpn: VirtPageNum, ppn: PhysPageNum, flags: PTEFlags) {
let pte = self.find_pte_create(vpn).unwrap();
assert!(!pte.is_valid(), "vpn {:?} is mapped before mapping", vpn);
*pte = PageTableEntry::new(ppn, flags | PTEFlags::V);
}
同样地,如果只进行查找不进行创建,可以得到如下两个方法:
// os\src\mm\page_table.rs
fn find_pte(&self, vpn: VirtPageNum) -> Option<&mut PageTableEntry> {
let idxs = vpn.indexes();
let mut ppn = self.root_ppn;
let mut result: Option<&mut PageTableEntry> = None;
for (i, idx) in idxs.iter().enumerate() {
let pte = &mut ppn.get_pte_array()[*idx];
if i == 2 {
result = Some(pte);
break;
}
if !pte.is_valid() {
return None;
}
ppn = pte.ppn();
}
result
}
pub fn unmap(&mut self, vpn: VirtPageNum) {
let pte = self.find_pte(vpn).unwrap();
assert!(pte.is_valid(), "vpn {:?} is invalid before unmapping", vpn);
*pte = PageTableEntry::empty();
}
总结
如果把kernel当作一个软件来看,基本的内存分配目前是如下图所示的:


浙公网安备 33010602011771号