跳转到内容

页表实现与核心函数

本节将详细解析页表项 (PageTableEntry) 的结构以及多级页表 (PageTable) 的管理逻辑,特别是涉及到映射建立与拆除的核心函数。

RISC-V SV39 架构规定,每个页表项 (PTE) 占据 8 字节(64 位)。

 63    54 53          10 9  8  7  6  5  4  3  2  1  0
┌──────┬────────────────┬─────┬──┬──┬──┬──┬──┬──┬──┬──┐
│ 保留 │      PPN       │ RSW │ D│ A│ G│ U│ X│ W│ R│ V│
│(10位)│     (44位)     │(2位)│  │  │  │  │  │  │  │  │
└──────┴────────────────┴─────┴──┴──┴──┴──┴──┴──┴──┴──┘

PTEFlags 定义了页表项的权限和状态:

标志位名称含义
VValid该页表项是否有效。如果 V=0,访问该页会触发 Page Fault。
RReadable是否可读。
WWritable是否可写。
XExecutable是否可执行(是否包含指令)。
UUser用户态 (U-mode) 是否可访问。若 U=0,仅 S-mode 可访问。
GGlobal全局映射(通常用于内核共享区域,本实验不深入讨论)。
AAccessed访问位。当该页被访问(读/写/执行)时,硬件会自动置位。
DDirty脏位。当该页被写入时,硬件会自动置位。
bitflags! {
    pub struct PTEFlags: u8 {
        const V = 1 << 0;
        const R = 1 << 1;
        const W = 1 << 2;
        const X = 1 << 3;
        const U = 1 << 4;
        const G = 1 << 5;
        const A = 1 << 6;
        const D = 1 << 7;
    }
}

PageTable 结构体负责管理根页表以及在映射过程中动态分配的中间页表帧:

pub struct PageTable {
    root_ppn: PhysPageNum,      // 根页表的物理页号
    frames: Vec<FrameTracker>,  // 所有的中间页表帧(用于自动回收)
}

我们使用 frames 向量持有所有中间页表节点的 FrameTracker。利用 Rust 的 RAII 机制,当 PageTable 被销毁时,这些物理帧会被自动释放,防止内存泄漏。


kernel/src/mm/page_table.rs 中,我们需要实现以下关键函数来管理虚拟内存映射。

fn find_pte_create(&mut self, vpn: VirtPageNum) -> Option<&mut PageTableEntry>

功能:根据给定的虚拟页号 (vpn),在三级页表中查找对应的叶子页表项。

  • 如果中间级页表不存在,则从物理内存分配器申请一个新的物理帧,初始化为新的页表,并将其添加到 frames 中管理。
  • 最终返回指向叶子 PTE 的可变引用,以便后续修改权限或映射物理页。

逻辑流程

  1. 从根页表开始,依次根据 vpn 的索引(level 2 -> 1 -> 0)查找 PTE。
  2. 遇到无效的 PTE(V=0),分配新帧,建立链接,继续下一级。
  3. 到达第三级(level 0)时,返回该 PTE 的引用。

遍历过程(逐级下沉):

vpn.indexes() = [i0, i1, i2]   // [VPN[2], VPN[1], VPN[0]]

1. 查找 i0
  若 pte.is_valid() == false:
    分配新帧 frame,令 pte = PageTableEntry::new(frame.ppn, V)
    将 frame 存入 self.frames(转移所有权)
    进入二级页表

2. 查找 i1
    同上,若不存在则分配新帧
    进入三级页表

3. 查找 i2
  直接返回该 PTE 的可变引用

函数签名:

fn find_pte(&self, vpn: VirtPageNum) -> Option<&mut PageTableEntry>

作用:找到 VPN 对应的叶子 PTE 的可变引用。若任意中间节点无效,返回 None

find_pte_create 的区别:

find_pte_createfind_pte
接收者&mut self&self
中间节点缺失分配新帧并创建节点返回 None
用途建立新映射(map查询/删除现有映射(translate, unmap

参考find_pte_create函数,仔细思考其中区别

函数签名:

pub fn map(&mut self, vpn: VirtPageNum, ppn: PhysPageNum, flags: PTEFlags)

作用:在页表中建立一个虚拟页到物理页的映射。

实现逻辑:

1. 找到(或创建)叶子节点

2. 检查该 VPN 尚未被映射(防御性检查,避免重复映射)

3. 写入 PPN 和权限标志,并置位 V(使映射有效)

函数签名:

pub fn unmap(&mut self, vpn: VirtPageNum)

作用:将 VPN 对应的叶子 PTE 清零,解除映射。

实现逻辑:

1. 找到叶子节点(若不存在则 panic)

2. 检查该映射确实存在

3.  将 PTE 清零(等价于 bits = 0),V 位变为 0,映射失效

MapArea::map_one(vpn, page_table) 为例(Framed 映射):

MapArea::map(&mut self, page_table: &mut PageTable)
  └─ for vpn in self.vpn_range
       └─ self.map_one(page_table, vpn)

              ├─ (Framed) frame = frame_alloc()
              │           ppn = frame.ppn
              │           self.data_frames.insert(vpn, frame)

              └─ page_table.map(vpn, ppn, PTEFlags)
                      └─ find_pte_create(vpn)
                              └─ 按需分配中间节点
                         *pte = PTE::new(ppn, flags | V)
MapArea::unmap(&mut self, page_table: &mut PageTable)
  └─ for vpn in self.vpn_range
       ├─ page_table.unmap(vpn)
       │       └─ find_pte(vpn)
       │              *pte = PTE::empty()
       │           (页表项清零,映射失效)

       └─ (Framed) self.data_frames.remove(&vpn)
                   (FrameTracker Drop,物理帧归还)

/// 查询 VPN 对应的 PTE(用于内核访问用户内存)
pub fn translate(&self, vpn: VirtPageNum) -> Option<PageTableEntry> {
    self.find_pte(vpn).map(|pte| *pte)
}

/// 翻译虚拟地址到物理地址
pub fn translate_va(&self, va: VirtAddr) -> Option<PhysAddr> {
    self.find_pte(va.floor()).map(|pte| {
        let aligned_pa: PhysAddr = pte.ppn().into();
        let offset = va.page_offset();
        (usize::from(aligned_pa) + offset).into()
    })
}

translated_byte_buffer 通过上述接口,将用户程序传入的虚拟地址指针 实现用户物理页内的数据复制。