这是 os summer of code 2020 项目每日记录的一部分:
每日记录github地址(包含根据实验指导实现的每个阶段的代码):https://github.com/yunwei37/os-summer-of-code-daily
这里参考的是rCore tutorial的第三版:https://github.com/rcore-os/rCore-Tutorial
lab3 学习报告
lab3 和 lab2 联系紧密,是其后续部分,在 lab2 中涉及通过页的方式对物理内存进行管理:
在 lab3 中主要涉及:
- 虚拟地址和物理地址的概念和关系
- 利用页表完成虚拟地址到物理地址的映射
- 实现内核的重映射
这一部分的代码将会在 lab2 的实验结果上面继续添加;
从虚拟内存到物理内存
原理
在现代的操作系统中,为了让其他的程序能方便的运行在操作系统上,需要完成的一个很重要的抽象是「每个程序有自己的地址空间,且地址空间范围是一样的」,这将会减少了上层程序的大量麻烦,否则程序本身要维护自己需要的物理内存,这也会导致极大程度的不安全。
这个执行上看到的地址空间,就是虚拟内存。而访问虚拟内存的地址就是虚拟地址(Virtual Address)
,与之对应的是物理地址(Physical Address)
。
这样的设计会导致上层的应用程序可能会访问同一个值相等的虚拟地址,所以操作系统需要做的就是替这些程序维护这个虚拟地址到物理地址的映射。甚者,为了统一和连贯,内核自己本身访问内存也将会通过虚拟地址。
Sv39
选择 RISC-V 本身硬件支持的 Sv39 模式作为页表的实现:
- 物理地址有 56 位
- 虚拟地址有 64 位,只有低 39 位有效。 63-39 位的值必须等于第 38 位的值。
- 物理页号为 44 位,每个物理页大小为 4KB
- 虚拟页号为 27 位,每个虚拟页大小也为 4KB
- 物理地址和虚拟地址的最后 12 位都表示页内偏移
页表项
页表项(PTE,Page Table Entry)
是用来描述一个虚拟页号如何映射到物理页号的:
Sv39 模式里面的一个页表项大小为 64 位(即 8 字节)。其中第 53-10 共 44 位为一个物理页号,表示这个虚拟页号映射到的物理页号。后面的第 9-0 位则描述页的相关状态信息。
- V 表示这个页表项是否合法。如果为 0 表示不合法,此时页表项其他位的值都会被忽略。
- R,W,X 分别表示是否可读(Readable)、可写(Writable)和可执行(Executable)。
- 如果 R,W,X 均为 0,文档上说这表示这个页表项指向下一级页表。
- U 为 1 表示用户态运行的程序可以通过该页表项完成地址映射。需要将 S 态的状态寄存器 sstatus 上的 SUM (permit Supervisor User Memory access) 位手动设置为 1 才可以访问通过这些 U 为 1 的页表项进行映射的用户态内存空间。
多级页表
在 Sv39 模式中我们采用三级页表
页表基址
- 页表寄存器 satp:页表的基址(起始地址)一般会保存在一个特殊的寄存器中。
快表(TLB)
使用快表(TLB, Translation Lookaside Buffer)
来作为虚拟页号到物理页号的映射的缓存。
- 需要使用 sfence.vma 指令刷新 TLB
修改内核
首先需要把内核的运行环境从物理地址空间转移到虚拟地址空间:将内核代码放在虚拟地址空间中以 0xffffffff80200000 开头的一段高地址空间中。
这是一种临时的线性映射:
os/src/linker.ld:
需要将起始地址修改为虚拟地址,增加 4K 对齐:
/* 目标架构 */
OUTPUT_ARCH(riscv)
/* 执行入口 */
ENTRY(_start)
/* 数据存放起始地址 */
BASE_ADDRESS = 0xffffffff80200000;
SECTIONS
{
/* . 表示当前地址(location counter) */
. = BASE_ADDRESS;
/* start 符号表示全部的开始位置 */
kernel_start = .;
. = ALIGN(4K);
text_start = .;
/* .text 字段 */
.text : {
/* 把 entry 函数放在最前面 */
*(.text.entry)
/* 要链接的文件的 .text 字段集中放在这里 */
*(.text .text.*)
}
. = ALIGN(4K);
rodata_start = .;
/* .rodata 字段 */
.rodata : {
/* 要链接的文件的 .rodata 字段集中放在这里 */
*(.rodata .rodata.*)
}
. = ALIGN(4K);
data_start = .;
/* .data 字段 */
.data : {
/* 要链接的文件的 .data 字段集中放在这里 */
*(.data .data.*)
}
. = ALIGN(4K);
bss_start = .;
/* .bss 字段 */
.bss : {
/* 要链接的文件的 .bss 字段集中放在这里 */
*(.sbss .bss .bss.*)
}
/* 结束地址 */
. = ALIGN(4K);
kernel_end = .;
}
修改 os/src/memory/config.rs 中的 KERNEL_END_ADDRESS 修改为虚拟地址并加入偏移量:
lazy_static! {
/// 内核代码结束的地址,即可以用来分配的内存起始地址
///
/// 因为 Rust 语言限制,我们只能将其作为一个运行时求值的 static 变量,而不能作为 const
pub static ref KERNEL_END_ADDRESS: VirtualAddress = VirtualAddress(kernel_end as usize);
}
/// 内核使用线性映射的偏移量
pub const KERNEL_MAP_OFFSET: usize = 0xffff_ffff_0000_0000;
需要加入两个关于位操作的辅助 crate:
os/Cargo.toml:
bitflags = "1.2.1"
bit_field = "0.10.0"
os/src/memory/address.rs
对虚拟地址和虚拟页号这两个类进行了封装,同时也支持了一些诸如 VirtualAddress::from(PhysicalAddress) 的转换 trait(即一些加减偏移量等操作):
(略过)
在启动时、在进入 rust_main 之前我们要完成一个从物理地址访存模式到虚拟访存模式的转换:我们要写一个简单的页表,完成这个线性映射:
os/src/entry.asm
......
_start:
# 计算 boot_page_table 的物理页号
lui t0, %hi(boot_page_table)
li t1, 0xffffffff00000000
sub t0, t0, t1
srli t0, t0, 12
# 8 << 60 是 satp 中使用 Sv39 模式的记号
li t1, (8 << 60)
or t0, t0, t1
# 写入 satp 并更新 TLB
csrw satp, t0
sfence.vma
# 加载栈地址
lui sp, %hi(boot_stack_top)
addi sp, sp, %lo(boot_stack_top)
# 跳转至 rust_main
lui t0, %hi(rust_main)
addi t0, t0, %lo(rust_main)
jr t0
.....
# 初始内核映射所用的页表
.section .data
.align 12
boot_page_table:
.quad 0
.quad 0
# 第 2 项:0x8000_0000 -> 0x8000_0000,0xcf 表示 VRWXAD 均为 1
.quad (0x80000 << 10) | 0xcf
.zero 507 * 8
# 第 510 项:0xffff_ffff_8000_0000 -> 0x8000_0000,0xcf 表示 VRWXAD 均为 1
.quad (0x80000 << 10) | 0xcf
.quad 0
上面的代码完成了:
- 把 CPU 的访问模式改为 Sv39,这里需要做的就是把一个页表的物理页号和 Sv39 模式写入 satp 寄存器,然后刷新 TLB。
- 先使用一种最简单的页表构造方法:将一个三级页表项的标志位 R,W,X 不设为全 0,可以将它变为表示 1GB 的一个大页。
- 有一个从 0x80000000 到 0x80000000 的映射,在跳转到 rust_main 之前(即 jr t0)之前,PC 的值都还是 0x802xxxxx 这样的地址,即使是写入了 satp 寄存器,但是 PC 的地址不会变。为了执行这段中间的尴尬的代码,我们在页表里面也需要加入这段代码的地址的映射。(过渡使用
还需要记得修改一下 allocator.rs:
lazy_static! {
/// 帧分配器
pub static ref FRAME_ALLOCATOR: Mutex<FrameAllocator<AllocatorImpl>> = Mutex::new(FrameAllocator::new(Range::from(
PhysicalPageNumber::ceil(PhysicalAddress::from(*KERNEL_END_ADDRESS))..PhysicalPageNumber::floor(MEMORY_END_ADDRESS),
)
));
}