忌才砟 发表于 2025-6-11 15:29:53

[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);
    }
}这里要注意,要想直接访问物理地址,必须是使用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.
建立和拆除虚实地址映射关系

实际上就是建立: 虚拟地址 -> 物理地址的接口和映射.

实现方法:
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);
}对应的VPN转PPN的方法:
// os\src\mm\address.rs
impl PhysPageNum {
    pub fn get_pte_array(&self) -> &'static mut {
      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 {
      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() }
    }
}同样地,如果只进行查找不进行创建,可以得到如下两个方法:
// 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
    }总结

如果把kernel当作一个软件来看,基本的内存分配目前是如下图所示的:


来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
页: [1]
查看完整版本: [rCore学习笔记 032] 管理SV39多级页表