天天看点

使用Rust开发操作系统(4级分页内存)基础控制位开始干活下一步要做什么

文章目录

  • 基础控制位
    • 分页模式
    • 分页内存的转化过程
    • 4级分页(PML4T)
      • 4级分页模式下各级页表项位功能
      • CR3寄存器:
      • PML4TE
      • PDPTE
      • PDE
      • PTE
      • 4级分页模式下不同规格的物理页寻址过程
        • 4KB分页寻址
        • 2MB分页寻址
        • 1GB分页寻址
    • 48位虚拟地址空间划分
      • 4 KiB pages
      • 2 MiB pages
      • 1 GiB pages
  • 开始干活
    • 建立页表索引
    • 计算线性地址所对应的页表索引
    • 抽象出页表项结构
    • 建立页面结构
    • 定义页表
  • 下一步要做什么

基础控制位

基础控制位是用于控制内存分页行为,主要为以下寄存器

  • CR0寄存器: CR0寄存器的

    WP

    (第16位)和

    PG

    位(第31位)
  • CR4寄存器: CR0寄存器的

    PSE

    (第4位),

    PAE

    (第5位),

    PGE

    (第7位),

    PCIDE

    (第17位),

    SMEP

    (第20位),

    SMAP

    (第21位),

    PKE

    (第22位),

    CET

    (第23位)标志位
  • MSR寄存器: IA32_EFER MSR寄存器

    LME

    (第8位)和

    NXE

    (第11位)标志位
  • EFLAGS寄存器:EFLAGS寄存器的

    AC

    (第18位)标志位

程序可以通过设置

CR0

PG

位(以后表示为

CR0.PG

)来开启分页,在开启分页之前程序应该保证

CR3

寄存器所包含有效的物理地址

分页模式

如果CR0.PG=0则表示未启用分页模式,逻辑处理器将线性地址当做物理地址对待,CR4.PAE,IA32_EFER.LME,CR0.WP,CR4.PSE,CR4.PGE,CR4.SMEP

CR4.SMAP,IA32_EFER.NXE等标志位将会被忽视

如果CR0.PE=1(开启保护模式)并且CR0.PG=1时则表示启用分页模式,如果分页已经启用,可以使用3种分页模式,其中CR4.PAE和IA32_EFER.LME两个标志位

将决定启用的分页模式

  • 如果CR0.PG=1并且CR4.PAE=0,则表示启用32位分页模式
  • 如果CR0.PG=1,CR4.PAE=1,并且IA32_EFER.LME=0则启用PAE分页模式
  • 如果CR0.PG=1,CR4.PAE=1,并且IA32_EFER.LME=1则启用4级分页模式,4级分页模式只能用于支持64位架构的处理器

3种分页模式的不同之处体现在:

  • 线性地址宽度,可被用于转换的线性地址大小
  • 物理地址宽度,可以被用于分页的物理地址大小
  • 页大小,线性地址被映射的粒度,将同一页面上的线性地址转化为同一页面上对应的物理地址
  • 提供禁用执行权限功能,有些分页模式可以防止程序从其他只读页面中获取执行指令
  • 支持

    PCID

    (进程上下文标识符,4级分页), 该功能可以使逻辑处理器可以缓存多个线性地址空间的信息,当程序在不同的线性地址之前切换时处理器可以保留缓存信息
  • 支持保护锁(protection keys,4级分页),该功能可以为每个线性地址相关联的保护锁,程序可以使用新的控制寄存器来确定如何访问与保护锁关联的线性地址

    以下是不同分页模式属性

分页模式 CR0.PG CR4.PAE IA32_EFER.LME 线性地址宽度 物理地址宽度 页大小 关闭执行权限 PCID和保护锁
未分页 N/A N/A 32 32 N/A NO NO
32位分页模式 1 32 上限为40位(2^40B) 4KB,4MB NO NO
PAE分页模式 1 1 32 上限为52位(2^52B) 4KB,2MB Yes NO
4级分页 1 1 1 48 上限为52位(2^52B) 4KB,2MB,1GB Yes Yes

注意:

MAXPHYADDR获取物理地址宽度

4级分页功能只能用于IA-32模式

分页模式转换图

使用Rust开发操作系统(4级分页内存)基础控制位开始干活下一步要做什么

所以如果我们从未分页模式转为4级分页模式顺序如下

使用Rust开发操作系统(4级分页内存)基础控制位开始干活下一步要做什么

注意:

  1. 在64位模式下使用

    canonical

    会产生一般保护异常(#GP);处理器不会尝试使用4级分页来转换

    non-canonical

    地址。
  2. 当CR4.PAE=0且IA32_EFER.LME=1时,无法启用分页(通过将CR0.PG设置为1),如果将MOV指令修改CR0会导致一般保护异常(#GP)。
  3. 当启用4级分页(CR0.PG=1和IA32_EFER.LME=1)时,无法复位CR4.PAE,如果将MOV指令修改CR4会导致一般保护异常(#GP)。
  4. 无论当前使用哪种分页模式,都可以通过MOV指令复位CR0.1的CR0.PG来禁用分页
  5. 程序可以使用MOV指令置位/复位CR4.PAE在32位分页个PAE分页模式切换
  6. 4级分页模式不能直接转换到其他2种分页模式,必须复位CR0.PG位后清除CR4.PAE位和IA32_EFER.LME位之后重新置位CR0.PG位重新开启分页

分页内存的转化过程

每个分页结构大小为4096字节,并且包含单独的项,在32位分页中每一个项为32位(4字节)因此总共包含1024个项,在PAE分页模式和4级分页模式中每一个项为4位(8字节)因此总共包含512个项,PAE分页有一个特例是一个为32字节的分页结构,其中包含4个64位项处理器会使用线性地址的高位来标识分页结构的项,这些项的最后一个标识符用于将线性地址转换为物理地址所在的区域称为页帧,线性地址的低位用于标识线性地址转换后的特定地址称为页偏移

每个项包含一个物理地址,该地址可以是另一个分页结构的地址或者是页帧地址,如果一个项包含另一个分页结构的地址那么我们称为该项引用其他分页结构,如果一个项包含的是页帧,那么我们称为该项映射到页面

我们以IA-32e中的4级分页模式(4kb)来说明转换过程

通过CR3寄存器获取到第一个分页结构的地址,该分页结构包含512个项,然后将线性地址的[47:39]位(共9位)作为分页结构的索引来获取分页结构中的其中一个项,通过该项找到所引用的第二个分页结构,将线性地址的[38:30]位(共9位)作为第二个分页结构的索引在获取第二个分页结构中的其中一项,以此类推[29:21]作为第三个分页结构的项索引,然后[20:12]作为第4个页表结构的索引,该索引出的项为物理页面的起始地址,最后的[11:0]作为页偏移来定位物理地址,以上过程表示如下

使用Rust开发操作系统(4级分页内存)基础控制位开始干活下一步要做什么

下表为不同分页模式的分页结构(PML4表示Page Map Level 4,4级页表)

使用Rust开发操作系统(4级分页内存)基础控制位开始干活下一步要做什么

因此如果我们要搞清楚分页内存,需要掌握这个东西(以PML4T为例)

可以将页表当做一个数组,那么每一项就是这个数组的每一个元素

  • 4级页表: 页表对应上表中的

    PML4T

    (Page Map Level 4 Table),基地址由CR3提供,线性地址的47-39位用于索引
  • 页表项: 对应上表中的

    PML4E

    (Page Map Level Entry),每一项为提供了下一级页表的基地址
  • 页表目录指针: 对应上表中的

    PDPT

    (Page Directory Pointer Table) 基地址由

    PML4E

    提供,如果开启1GB分页,则线性地址的38-30位作为物理页的基地址,线性地址的29-0位作为物理页内偏移,如果开启2MB或4kb分页则线性地址的38-30位作为索引
  • 页表目录项: 对应上表中的

    PDPTE

    (Page Directory Pointer Table Entry) 如果开启1GB分页,该项会映射到对应的1GB物理页面,如果开启2MB或4KB提供了下一级页表的基地址
  • 页目录: 对应上表中的

    PD

    (Page Directory) 基地址由

    PDPTE

    提供,如果开启了2MB分页,则线性地址的29-21位作为物理页的基地址,线性地址的20-0位作为物理页内偏移,如果开启4KB分页,则线性地址的29-21位作为索引
  • 页目录项: 对应上表中的

    PDE

    (Page Directory Entry),如果开启2MB分页,该项会映射到对应的2MB物理页的基地址,如果开启4KB分页则提供了下一级页表基地址
  • 页表: 对应上表中的

    PT

    (Page Table) 基地址由

    PDE

    提供,线性地址的第20-12位提供了4KB物理页面的基地址
  • 页表项: 对应上表中的

    PTE

    (Page Table Entry) 该项会映射到4KB物理页面的基地址,线性地址的第11-0项作为物理页内偏移

4级分页(PML4T)

当同时置位CR0.PG,CR4.PAE,IA32_EFER.LME便可开启4页分页模式,在4级分页中可以将Canonical型地址映射到52位物理地址空间(由处理器的最高物理可寻址位宽

MAXPHYADDR

决定),可以在IA-32e模式下可寻址4PB的物理地址空间,以及256TB的线性地址空间

4级分页模式下各级页表项位功能

CR3寄存器:

使用Rust开发操作系统(4级分页内存)基础控制位开始干活下一步要做什么
使用Rust开发操作系统(4级分页内存)基础控制位开始干活下一步要做什么
  • PTW: 页级直写模式(Page-level Write-through)
  • PCD: 页级缓存禁用(Page-level Cache Disable)

PML4TE

使用Rust开发操作系统(4级分页内存)基础控制位开始干活下一步要做什么
  • Present(第0位): 如果要引用

    PDPT

    必须置位
  • R/W: 读/写(Read/Write),如果为0,则不允许对该项控制的512GB区域进行写操作
  • U/S: 用户/内核(User/supervisor)如果为0,则不允许用户模式访问此项控制的512GB区域
  • PTW: 页级直写模式(Page-level Write-through)
  • PCD: 页级缓存禁用(Page-level Cache Disable)
  • A: 已访问(Access); 指示此项是否已用于线性地址转换

PDPTE

使用Rust开发操作系统(4级分页内存)基础控制位开始干活下一步要做什么
  • Present(第0位): 如果要引用

    PDT

    必须置位
  • R/W: 读/写(Read/Write);如果为0,则不允许对该项控制的1GB区域进行写操作
  • U/S: 用户/内核(User/supervisor);如果为0,则不允许用户模式访问此项控制的512GB区域
  • PTW: 页级直写模式(Page-level Write-through)
  • PCD: 页级缓存禁用(Page-level Cache Disable)
  • A: 已访问(Access); 指示此项是否已用于线性地址转换
  • PS(第7位): 页大小(PageSize ); 必须置1否则表示应用下级的页结构
  • XD: 如果IA32_EFER.NXE = 1,则禁用代码执行(如果为1,则不允许从此项所控制的1GB页面中提取指令,否则必须为0)
使用Rust开发操作系统(4级分页内存)基础控制位开始干活下一步要做什么
  • Present(第0位): 如果要映射1GB页面必须置位
  • R/W: 读/写(Read/Write);如果为0,则不允许对该项所控制的1GB区域进行写操作
  • U/S: 用户/内核(User/supervisor);如果为0,则不允许用户模式访问此项控制的1GB区域
  • PTW: 页级直写模式(Page-level Write-through)
  • PCD: 页级缓存禁用(Page-level Cache Disable)
  • A: 已访问(Access); 指示此项是否已用于线性地址转换
  • D: 脏页(Dirty); 指示程序是否已写入此项所引用的1GB页面
  • PS(第7位): 页大小(PageSize ); 必须置1否则表示应用下级的页结构
  • G: 全局标识(Global);如果CR4.PGE=1,则表示缓存转换是全局的
  • PAT: 间接表示用于访问此项所引用的1GB页面的内存类型(所有支持4级分页的处理器都支持PAT)
  • XD: 如果IA32_EFER.NXE = 1,则禁用代码执行(如果为1,则不允许从此项所控制的1GB页面中提取指令,否则必须为0)

PDE

使用Rust开发操作系统(4级分页内存)基础控制位开始干活下一步要做什么
  • Present(第0位): 如果要引用

    PDT

    必须置位
  • R/W: 读/写(Read/Write);如果为0,则不允许对该项控制的2MB区域进行写操作
  • U/S: 用户/内核(User/supervisor);如果为0,则不允许用户模式访问此项控制的2MB区域
  • PTW: 页级直写模式(Page-level Write-through)
  • PCD: 页级缓存禁用(Page-level Cache Disable)
  • A: 已访问(Access); 指示此项是否已用于线性地址转换
  • PS(第7位): 页大小(PageSize); 必须置0否则映射为2MB物理页
  • XD: 如果IA32_EFER.NXE = 1,则禁用代码执行(如果为1,则不允许从此项所控制的2MB页面中提取指令,否则必须为0)
使用Rust开发操作系统(4级分页内存)基础控制位开始干活下一步要做什么
  • Present(第0位): 如果要引用

    PDT

    必须置位
  • R/W: 读/写(Read/Write);如果为0,则不允许对该项控制的2MB区域进行写操作
  • U/S: 用户/内核(User/supervisor);如果为0,则不允许用户模式访问此项控制的2MB区域
  • PTW: 页级直写模式(Page-level Write-through)
  • PCD: 页级缓存禁用(Page-level Cache Disable)
  • A: 已访问(Access); 指示此项是否已用于线性地址转换
  • D: 脏页(Dirty); 指示程序是否已写入此项所引用的2MB页面
  • PS(第7位): 页大小(PageSize); 必须置1否则表示应用下级的页结构
  • G: 全局标识(Global);如果CR4.PGE=1,则表示缓存转换是全局的
  • PAT: 间接表示用于访问此项所引用的2MB页面的内存类型(所有支持4级分页的处理器都支持PAT)
  • XD: 如果IA32_EFER.NXE = 1,则禁用代码执行(如果为1,则不允许从此项所控制的2MB页面中提取指令,否则必须为0)

PTE

使用Rust开发操作系统(4级分页内存)基础控制位开始干活下一步要做什么
  • Present(第0位): 必须置1映射为4kb页面
  • R/W: 读/写(Read/Write);如果为0,则不允许对该项控制的4KB区域进行写操作
  • U/S: 用户/内核(User/supervisor);如果为0,则不允许用户模式访问此项控制的4KB区域
  • PTW: 页级直写模式(Page-level Write-through)
  • PCD: 页级缓存禁用(Page-level Cache Disable)
  • A: 已访问(Access); 指示此项是否已用于线性地址转换
  • D: 脏页(Dirty); 指示程序是否已写入此项所引用的4KB页面
  • PAT: 间接表示用于访问此项所引用的2MB页面的内存类型(所有支持4级分页的处理器都支持PAT)
  • G: 全局标识(Global);如果CR4.PGE=1,则表示缓存转换是全局的
  • XD: 如果IA32_EFER.NXE = 1,则禁用代码执行(如果为1,则不允许从此项所控制的2MB页面中提取指令,否则必须为0)

4级分页模式下不同规格的物理页寻址过程

4KB分页寻址

使用Rust开发操作系统(4级分页内存)基础控制位开始干活下一步要做什么

2MB分页寻址

使用Rust开发操作系统(4级分页内存)基础控制位开始干活下一步要做什么

1GB分页寻址

使用Rust开发操作系统(4级分页内存)基础控制位开始干活下一步要做什么

48位虚拟地址空间划分

以下表格来自osdev.org

4 KiB pages

Level Table Size Range Bits Entries Pages Recursive mapping
(page) - 0x1000(4 KiB) 12 bits - 0x1 (1)
1 PT 0x1000 (4 KiB) 0x20 0000 (2 MiB) 9 bits 512 0x200 (512)
2 PD 0x1000 (4 KiB) 0x4000 0000 (1 GiB) 9 bits 512 0x40000 (262144)
3 PDP 0x1000 (4 KiB) 0x80 0000 0000 (512 GiB) 9 bits 512 0x800 0000 (134217728)
4 PML4 0x1000 (4 KiB) 0x10000 0000 0000 (256 TiB) 9 bits 512 0x10 0000 0000 (68719476736)

2 MiB pages

Level Table Size Range Bits Entries Pages Recursive mapping
(page) - 0x200000(2 MiB) 21 bits - 0x1 (1)
2 PD 0x1000 (4 KiB) 0x4000 0000 (1 GiB) 9 bits 512 0x200 (512)
3 PDP 0x1000 (4 KiB) 0x80 0000 0000 (512 GiB) 9 bits 512 0x40000 (262144)
4 PML4 0x1000 (4 KiB) 0x10000 0000 0000(256 TiB) 9 bits 512 0x8000000 (134217728)

1 GiB pages

Level Table Size Range Bits Entries Pages Recursive mapping
(page) - 0x4000 0000 (1 GiB) 30 bit - 0x1 (1)
3 PDP 0x1000(4 KiB) 0x80 0000 0000 (512 GiB) 9 bits 512 0x200(512)
4 PML4 0x1000(4 KiB) 0x10000 0000 0000 (256 TiB) 9 bits 512 0x40 000(262144)

关于虚拟内存:使用分页时,线性地址空间的某些部分不需要映射到物理地址空间。未映射地址的数据可以存储在外部(例如,磁盘上)。这种映射线性地址空间的方法称为虚拟内存或按需分页的虚拟内存。在本章中不讨论虚拟内存,以后会有专门的文章讨论

开始干活

我们需要完成以下内容

  1. 计算页表索引
  2. 为页表项建立适当的抽象
  3. 为物理页帧建立适当的抽象
  4. 为页表项所物理页面进建立适当抽象
  5. 建立页表

知道我们需要做的事情以后我们开始对之前的

VirtAddr

进行改造,

VirtAddr

表示得是Canonical型地址,我们要提供计算4页表索引的方法,我们可以把PML4T当做一个数组

线性地址的47:39做为数组的索引,因此我们可以编写一个

PageIndex

结构(PageOffset同理不在赘述)

建立页表索引

// in /kernel/system/src/ia_32e/paging/page_ops.rs
/// 页表的9位索引。
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct PageIndex(u16);
           

虽然页表的索引只需要9位即可,但是rust没有提供u9这样的数据类型,因此使用了u16,随后我们创建PageIndex的构造函数

// in /kernel/system/src/ia_32e/paging/page_ops.rs
pub const ENTRY_COUNT: usize = 512;
impl PageIndex{
    /// 根据给定的索引创建页表索引值
    /// 索引最大不能超过512个
    pub fn new(index: u16) -> Self {
        assert!(usize::from(index) < ENTRY_COUNT);
        Self(index)
    }
}
           

需要注意的是,我们使用的4级页表结构,因此一个页表的最大Entry数不会超过512,因此我们的索引也不能超过512,我们还可以提供了另一种方法来保证

index

不会超过512

这里可以使用取模即可,例如

2 % 512 = 2

(在Rust中

%

为算术取模运算)假设我们给的索引为513超过512,那么取模后的结果为

513 % 512 = 1

结果相当于又从0开始计数

例如刚好为512,

512 % 512 = 0

索引刚好被重置为0

// in /kernel/system/src/ia_32e/paging/page_ops.rs
pub const ENTRY_COUNT: usize = 512;
impl PageIndex{
    /// 索引最大不能超过512个,使用取模运算可防止超出512
    pub const fn new_truncate(index: u16) -> Self {
        Self(index % ENTRY_COUNT as u16)
    }
}
impl From<PageIndex> for u16 {
    fn from(index: PageIndex) -> Self {
        index.0
    }
}

impl From<PageIndex> for u32 {
    fn from(index: PageIndex) -> Self {
        u32::from(index.0)
    }
}

impl From<PageIndex> for u64 {
    fn from(index: PageIndex) -> Self {
        u64::from(index.0)
    }
}
           

这样我们可以使用

new_truncate

这样并不会出现断言错误了,并且提供了u16,u32,u64对应的转换函数

计算线性地址所对应的页表索引

为了查看方便提供了线性地址划分图

|63-48|47-39|38-30|29-21|21-12| 11-0 |
+-----+-----+-----+-----+-----+------+
|Sign |PML4 |PDPT | PDT | PT  |Offset|
+-----+-----+-----+-----+-----+------+
           

计算1级页表索引的方式很简单,只需要右移12位(0-11共12位)把Offset挤出去即可因此我们可以得到如下函数

new_truncate

函数会保证不会超过512

// in /kernel/system/src/ia_32e/addr.rs
impl VirtAddr {
    // .....
    /// 返回一级页表索引(9位)
    pub fn page1_index(&self) -> PageIndex {
        PageIndex::new_truncate((self.0 >> 12) as u16)
    }
}
           

那么计算2级页表索引的方式只需要在1级页表索引的基础上右移9位(1级页表索引为9位),其他索引方式类似

// in /kernel/system/src/ia_32e/addr.rs
impl VirtAddr {
    // .....
    /// 返回一级页表索引(9位)
    pub fn page1_index(&self) -> PageIndex {
        PageIndex::new_truncate((self.0 >> 12) as u16)
    }

    /// 返回二级页表索引(9位)
    pub fn page2_index(&self) -> PageIndex {
        PageIndex::new_truncate((self.0 >> 12 >> 9) as u16)
    }

    /// 返回三级页表索引(9位)
    pub fn page3_index(&self) -> PageIndex {
        PageIndex::new_truncate((self.0 >> 12 >> 9 >> 9) as u16)
    }

    /// 返回四级页表索引(9位)
    pub fn page4_index(&self) -> PageIndex {
        PageIndex::new_truncate((self.0 >> 12 >> 9 >> 9 >> 9) as u16)
    }
}
           

抽象出页表项结构

然后我们将

PML4TE

,

PDPTE

,

PDE

,

PTE

统一抽象为

PageEntry

结构如下

// in /kernel/system/src/ia_32e/paging/page_table.rs
#[derive(Clone, Copy)]
#[repr(transparent)]
pub struct PageTableEntry {
    entry: u64
}
           

当一个页表项无效时/未使用(Present位为0)位图结构如下(以PML4E为例,其他均一致)

使用Rust开发操作系统(4级分页内存)基础控制位开始干活下一步要做什么

可以看到其结构整个均为0,因此我们可以通过该entry是否为0来判断该表项是否被使用,以及将该表项置0表示无效表项

// in /kernel/system/src/ia_32e/paging/page_table.rs
impl PageTableEntry {
    /// 创建一个空的页表页表项
    pub const fn new() -> Self {
        PageTableEntry { entry: 0 }
    }
    /// 判断页表页表项是否被使用
    pub const fn is_unused(&self) -> bool {
        self.entry == 0
    }
    /// 将页表页表项设置位未使用
    pub fn set_unused(&mut self) {
        self.entry = 0;
    }
}
           

根据之前的

PML4TE

,

PDPTE

,

PDE

,

PTE

位图功能我们建立PageTableFlags结构,来表示对应的标志位,结构如下

// in kernel/bit/flags.rs
bitflags! {
    /// Page Entry flag
    #[allow(non_upper_case_globals)]
    pub struct PageTableFlags: u64 {
        /// 页存在标志位,如果置1表示存在否则表示不存在
        const PRESENT =         1 << 0;
        /// 物理页可写标志位
        /// 如果1级页表没有设置该标志位,那么对应的物理页是只读
        /// 如果其他高等级页表没有设置该位,那么表示表示这个该页所映射的整个范围都是只读的
        const WRITABLE =        1 << 1;
        /// 表示该页是否能在用户模式访问 置1时用户模式,置0为内核模式
        const USER_ACCESSIBLE = 1 << 2;
        /// 页级写穿标志位, 如果置1表示写穿`write-through`用于缓存 置0表示 回写`write-back`
        const WRITE_THROUGH =   1 << 3;
        /// 禁止页级缓存标志位 置1时表示页不能缓存,否则表示页可以缓存
        const NO_CACHE =        1 << 4;
        /// 访问标示位, 置0时表示CPU未访问,置1时表示CPU已访问
        const ACCESSED =        1 << 5;
        /// 脏页标志位。 置1时为脏页,置0时为干净页
        const DIRTY =           1 << 6;
        /// 页面属性标志位,只能用于2级或3级页表(如果支持PAT则置为1否则必须值0)
        const HUGE_PAGE =       1 << 7;
        /// 全局属性标志位, 如果置1表示全局页面,置0表示局部页面,
        /// 更新CR3控制寄存器时不会刷新TLB内的全局页表项
        const GLOBAL =          1 << 8;
        /// 9-11无映射,可自用
        const BIT_9 =           1 << 9;
        const BIT_10 =          1 << 10;
        const BIT_11 =          1 << 11;
        /// 52-58无映射,可自用
        const BIT_52 =          1 << 52;
        const BIT_53 =          1 << 53;
        const BIT_54 =          1 << 54;
        const BIT_55 =          1 << 55;
        const BIT_56 =          1 << 56;
        const BIT_57 =          1 << 57;
        const BIT_58 =          1 << 58;
        const BIT_59 =          1 << 59;
        /// Protection key如果CR4.PKE=1表示页不保护键,可以忽略
        const PROTECTION_60 =          1 << 60;
        const PROTECTION_61 =          1 << 61;
        const PROTECTION_62 =          1 << 62;
        /// 如果IA32_EFER.NXE = 1,则禁用执行
        /// (如果为1,则不允许从此条目控制的1 GB页面中提取指令;请参见4.6节)
        /// 否则,保留(必须为0)
        /// 仅当在EFER寄存器中启用了不执行页面保护功能时才可以使用
        const NO_EXECUTE =      1 << 63;
    }
}
           

这样我们可以增加一个新的函数

// in /kernel/system/src/ia_32e/paging/page_table.rs
use crate::bits::PageTableFlags;
impl PageTableEntry {
    // ....
    /// 获取当前页表项的bitmap
    pub const fn flags(&self) -> PageTableFlags {
        PageTableFlags::from_bits_truncate(self.entry)
    }
}
           

建立页面结构

当页表项与物理页面映射时物理页面有不同的大小,为了适应这种变化我们可以使用trait来解决

// in /kernel/system/src/ia_32e/paging/page.rs
/// 针对3种不同的页大小的抽象
pub trait PageSize: Copy + Eq + PartialEq + Ord {
    /// 当前页表项所映射的页帧大小
    const P_SIZE: u64;
    /// 显示字符串
    const DISPLAY_STR: &'static str;
}
           

随后我们建立

Page4KB

,

Page2MB

,

Page1GB

结构表示4KB页面,2MB页面,1GB页面

#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub struct Page4KB {}

impl PageSize for Page4KB {
    const P_SIZE: u64 = 4096;
    const DISPLAY_STR: &'static str = "page 4 KB";
}

#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub struct Page2MB {}

impl PageSize for Page2MB {
    const P_SIZE: u64 = Page4KB::P_SIZE * 512;
    const DISPLAY_STR: &'static str = "page 1 MB";
}

#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub struct Page1GB {}

impl PageSize for Page1GB {
    const P_SIZE: u64 = Page2MB::P_SIZE * 512;
    const DISPLAY_STR: &'static str = "page 1 GB";
}
           

随后我们可以4KB,2MB,1GB等页面建立统一的抽象Page

#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
#[repr(C)]
pub struct Page<S: PageSize = Page4KB> {
    /// 页面起始地址
    start_address: VirtAddr,
    /// 页表的大小
    size: PhantomData<S>,
}
           

关于PhantomData

PhantomData<T>

是一个零大小类型的标记结构体主要的作用如下

  1. 标记不会使用到的类型或参数
  2. 协变

    关于PhantomData解释如下

类型或生命周期逻辑上与一个结构体关联起来了,但是却不属于结构体的任何一个成员

这种情况对于生命周期尤为常见。比如,&'a [T] 的 Iter 大概是这么定义的

struct Iter<'a, T: 'a> {
     ptr: *const T,
     end: *const T,
 }
           

但是,因为’a 没有在结构体内被使用,它是无界的。由于一些历史原因,无界生命周期和类型禁止出现在结构体定义中。所以我们必须想办法在结构体内用到这些类型,这也是正确的变性检查和 drop 检查的必要条件。

我们使用一个特殊的标志类型 PhantomData 做到这一点。PhantomData 不消耗存储空间,它只是模拟了某种类型的数据,以方便静态分析。这么做比显式地告诉类型系统你需要的变性更不容易出错,而且还能提供 drop 检查需要的信息。

Iter 逻辑上包含一系列 &'a T,所以我们用 PhantomData 这样去模拟它:

use std::marker;

struct Iter<'a, T: 'a> {
    ptr: *const T,
    end: *const T,
    _marker: marker::PhantomData<&'a T>,
}
           

就是这样,生命周期变得有界了,你的迭代器对于’a 和 T 也可变了。一切尽如人意

————————————————

原文作者:Rust 技术论坛文档:《Rust 高级编程(2018)》

转自链接:https://learnku.com/docs/nomicon/2018/310-phantom-data/4721

版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请保留以上作者信息和原文链接。

关于Rust协变,逆变等信息可参考https://ioover.net/dev/variance-and-subtyping/和https://doc.rust-lang.org/nomicon/subtyping.html

随后我们希望可以将给定的地址转换为对应的页面,以及返回可能包含指定的地址的页面

impl<S: PageSize> Page<S> {
    /// 当前页大小
    pub const SIZE: u64 = S::P_SIZE;
    /// 返回给定地址的页表
    /// 如果地址没有正确对齐将会返回错误
    pub fn from_start_address(address: VirtAddr) -> Result<Self, ()> {
        // 判断指定地址是否按照对应页面对齐
        if !address.is_aligned(S::P_SIZE) {
            return Err(());
        }
        Ok(Page::include_address(address))
    }
    /// 返回包含给定地址的页表
    pub fn include_address(address: VirtAddr) -> Self {
        // align_down可以获取给定地址按照S::P_SIZE对齐后的起始地址
        Page {
            start_address: address.align_down(S::P_SIZE),
            size: PhantomData,
        }
    }
}
           

随后提供描述Page自身信息的函数

impl<S: PageSize> Page<S> {
    /// 获取当前页的虚拟地址
    pub fn start_address(&self) -> VirtAddr {
        self.start_address
    }

    /// 获取当前页大小
    pub const fn size(&self) -> u64 {
        S::P_SIZE
    }

    /// 获取4级页表索引
    pub fn p4_index(&self) -> PageIndex {
        self.start_address.page4_index()
    }
    /// 获取3级页表索引
    pub fn p3_index(&self) -> PageIndex {
        self.start_address.page3_index()
    }
}
           

对于不同规格的页面大小各级页表项索引所组成的页面方式需要分情况完成

impl Page<Page4KB>{
    // 返回含有指定页索引内存页(4kb)
    pub fn from_page_table_indices(
        p4_index: PageIndex,
        p3_index: PageIndex,
        p2_index: PageIndex,
        p1_index: PageIndex,
    ) -> Self {
        use crate::bits::BitOpt;

        let mut addr = 0;
        addr.set_bits(39..48, u64::from(p4_index));
        addr.set_bits(30..39, u64::from(p3_index));
        addr.set_bits(21..30, u64::from(p2_index));
        addr.set_bits(12..21, u64::from(p1_index));
        Page::include_address(VirtAddr::new(addr))
    }
}

impl Page<Page2MB>{
    // 返回含有指定页索引内存页(2mb)
    pub fn from_page_table_indices_2mib(
        p4_index: PageIndex,
        p3_index: PageIndex,
        p2_index: PageIndex,
    ) -> Self {
        use crate::bits::BitOpt;

        let mut addr = 0;
        addr.set_bits(39..48, u64::from(p4_index));
        addr.set_bits(30..39, u64::from(p3_index));
        addr.set_bits(21..30, u64::from(p2_index));
        Page::include_address(VirtAddr::new(addr))
    }
}

impl Page<Page1GB>{
    // 返回含有指定页索引内存页(1GB)
    pub fn from_page_table_indices_1gib(
        p4_index: PageIndex,
        p3_index: PageIndex,
    ) -> Self {
        use crate::bits::BitOpt;
        let mut addr = 0;
        addr.set_bits(39..48, u64::from(p4_index));
        addr.set_bits(30..39, u64::from(p3_index));
        Page::include_address(VirtAddr::new(addr))
    }
}
           

关于页帧结构:其实页帧结构与页面结构几乎一致,不同是的是

start_addr

不再是线性地址了,而是物理地址

PhysAddr

,并且需要实现

Add

,

Sub

,

AddAssign

,

SubAssign

等trait,结构如下,关于给定物理地址获取对应页帧的函数可参考Page的做法不在赘述

#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[repr(C)]
pub struct Frame<S: PageSize = Page4KB> {
    start_addr: PhysAddr,
    size: PhantomData<S>,
}
           

定义完Frame后我们可以为PageTableEntry提供页表所映射的页帧了

impl PageTableEntry {
    /// 返回当前Entry的页帧
    /// # Error
    /// * `FrameError::FrameNotPresent` 表示当前Entry没有被置`PRESENT`位
    pub fn frame(&self) -> Result<Frame, FrameError> {
        if !self.flags().contains(PageTableFlags::PRESENT) {
            Err(FrameError::FrameNotPresent)
        } else if self.flags().contains(PageTableFlags::HUGE_PAGE) {
            Err(FrameError::HugeFrame)
        } else {
            Ok(Frame::include_address(self.addr()))
        }
    }
    /// 将entry与指定的页帧做映射
    pub fn set_frame(&mut self, f: Frame, flags: PageTableFlags) {
        assert!(!flags.contains(PageTableFlags::HUGE_PAGE));
        self.set_addr(f.start_address(), flags)
    }
}
           

定义页表

页表就是有512个

PageTableEntry

的大数组,但是这个数组需要按照0x1000(4096)对齐

pub const ENTRY_COUNT: usize = 512;

#[repr(align(4096))]
#[repr(C)]
pub struct PageTable {
    entries: [PageTableEntry; ENTRY_COUNT],
}
           

为了能让我们像操作数组一样操作PageTable因此我们要为PageTable实现Index trait

impl Index<usize> for PageTable {
    type Output = PageTableEntry;

    fn index(&self, index: usize) -> &Self::Output {
        &self.entries[index]
    }
}

impl IndexMut<usize> for PageTable {
    fn index_mut(&mut self, index: usize) -> &mut Self::Output {
        &mut self.entries[index]
    }
}

impl Index<PageIndex> for PageTable {
    type Output = PageTableEntry;

    fn index(&self, index: PageIndex) -> &Self::Output {
        &self.entries[cast::usize(u16::from(index))]
    }
}

impl IndexMut<PageIndex> for PageTable {
    fn index_mut(&mut self, index: PageIndex) -> &mut Self::Output {
        &mut self.entries[cast::usize(u16::from(index))]
    }
}
           

很简单,我们只需要复用底层的数组即可,我们用到了

cast

crate 我们要在Cargo.toml中添加以下内容

[dependencies.cast]
version = "0.2.2"
default-features = false
           

最后创建跟清空页表的操作就比较简单了

impl PageTable {
    /// 创建一个空的页表
    pub const fn new() -> Self {
        PageTable {
            entries: [PageTableEntry::new(); ENTRY_COUNT]
        }
    }
    /// 清空表中所有内容
    pub fn zero(&mut self) {
        for entry in self.entries.iter_mut() {
            entry.set_unused();
        }
    }
}
           

到此为止我们页表部分的结构就编写完成了

下一步要做什么

在下一篇文章中我们利用现在编写的页结构编写基本的页面与物理页的映射功能

继续阅读