页表

本节介绍NUDT-OS中的页表抽象。

页表项

/// 页表项
#[derive(Clone, Copy)]
#[repr(transparent)]
pub struct PageTableEntry(usize);

impl PageTableEntry {
    /// 1111..._0000_0000_0000
    const PHYS_ADDR_MASK: usize = !(PAGE_SIZE - 1);

    /// 将一个物理地址和flags填入一个页表项中
    pub const fn new_page(pa: PhysAddr, flags: PageTableFlags) -> Self {
        Self((pa.0 & Self::PHYS_ADDR_MASK) | flags.bits() as usize)
    }

    ...
}

每个页表项中存放一个物理地址(下一级页表的起始物理地址),这个地址是关于4096字节对齐的,故低十二位用来存放页表项可能的标记。new_page方法将一个物理地址和一组flags写入一个页表项中。

页表

/// 页表
pub struct PageTable {
    /// 4级页表的起始物理地址
    pub root_pa: PhysAddr,
    /// 页表包含的一组物理页帧
    tables: Vec<PhysFrame>,
}

页表结构体PageTable中包含了四级页表首地址和一个物理页帧的队列。每个物理页帧存放一张大小为4096字节的某级页表。

内核运行时,cr3寄存器中存放四级页表的起始物理地址,硬件的MMU模块寻址虚拟地址时会从cr3寄存器中取出页表地址并查询,最终转换得虚拟地址的物理地址。进程切换时,将cr3寄存器载入自己的页表的起始物理地址即实现了地址空间切换。

x64架构中虚拟地址的各字段含义和地址翻译过程见下图:

/// 从虚拟地址获取四级页表索引,cr3中存四级页表首地址
///
/// 按这个索引取出的是三级页表首地址
const fn p4_index(va: VirtAddr) -> usize {
    (va.0 >> (12 + 27)) & (ENTRY_COUNT - 1)
}

/// 从虚拟地址获取三级页表索引
const fn p3_index(va: VirtAddr) -> usize {
    (va.0 >> (12 + 18)) & (ENTRY_COUNT - 1)
}

/// 从虚拟地址获取二级页表索引
const fn p2_index(va: VirtAddr) -> usize {
    (va.0 >> (12 + 9)) & (ENTRY_COUNT - 1)
}

/// 从虚拟地址获取一级页表索引
const fn p1_index(va: VirtAddr) -> usize {
    (va.0 >> 12) & (ENTRY_COUNT - 1)
}

/// 获取一个虚拟地址的物理地址,可能创建低级页表
fn get_entry_or_create(&mut self, va: VirtAddr) -> Option<&mut PageTableEntry> {
    let p4 = table_of(self.root_pa);
    let p4e = &mut p4[p4_index(va)];
    let p3 = next_table_or_create(p4e, || self.alloc_table())?;
    let p3e = &mut p3[p3_index(va)];
    let p2 = next_table_or_create(p3e, || self.alloc_table())?;
    let p2e = &mut p2[p2_index(va)];
    let p1 = next_table_or_create(p2e, || self.alloc_table())?;
    let p1e = &mut p1[p1_index(va)];
    Some(p1e)
}

将上图中的地址翻译过程用Rust表示如上。

/// 映射一个虚拟地址到一个物理地址,写入页表
impl PageTable {
    pub fn map(&mut self, va: VirtAddr, pa: PhysAddr, flags: PageTableFlags) {
        let entry = self.get_entry_or_create(va).unwrap();
        if !entry.is_unused() {
            panic!("{:#x?} is mapped before mapping", va);
        }
        *entry = PageTableEntry::new_page(pa.align_down(), flags);
    }

    /// 取消映射一个虚拟地址,清除页表
    pub fn unmap(&mut self, va: VirtAddr) {
        let entry = get_entry(self.root_pa, va).unwrap();
        if entry.is_unused() {
            panic!("{:#x?} is invalid before unmapping", va);
        }
        entry.0 = 0;
    }
}

最终实现页表的mapunmap方法