Lab 3: RV64 虚拟内存管理¶
1 实验目的¶
- 学习虚拟内存的相关知识,实现物理地址到虚拟地址的切换。
- 了解 RISC-V 架构中 SV39 分页模式,实现虚拟地址到物理地址的映射,并对不同的段进行相应的权限设置。
2 实验环境¶
- Environment in previous labs
3 背景知识¶
3.0 前言¶
在 lab2 中我们赋予了 OS 对多个线程调度以及并发执行的能力,由于目前这些线程都是内核线程,因此他们可以共享运行空间,即运行不同线程对空间的修改是相互可见的。但是如果我们需要线程相互 隔离 ,以及在多线程的情况下更加 高效 的使用内存,就必须引入虚拟内存
这个概念。
虚拟内存可以为正在运行的进程提供独立的内存空间,制造一种每个进程的内存都是独立的假象。同时虚拟内存到物理内存的映射也包含了对内存的访问权限,方便 Kernel 完成权限检查。
在本次实验中,我们需要关注 OS 如何 开启虚拟地址 以及通过设置页表来实现 地址映射 和 权限控制 。
3.1 Kernel 的虚拟内存布局¶
start_address end_address
0x0 0x3fffffffff
│ │
┌────┘ ┌─────┘
↓ 256G ↓
┌───────────────────────┬──────────┬────────────────┐
│ User Space │ ... │ Kernel Space │
└───────────────────────┴──────────┴────────────────┘
↑ 256G ↑
┌─────────────┘ │
│ │
0xffffffc000000000 0xffffffffffffffff
start_address end_address
0x0000004000000000
以下的虚拟空间作为 user space
。将 0xffffffc000000000
及以上的虚拟空间作为 kernel space
。由于我们还未引入用户态程序,目前我们只需要关注 kernel space
。
具体的虚拟内存布局可以参考这里。
在
RISC-V Linux Kernel Space
中有一段区域被称为direct mapping area
,为了方便 kernel 可以高效率的访问 RAM,kernel 会预先把所有物理内存都映射至这一块区域 ( PA + OFFSET == VA ), 这种映射也被称为linear mapping
。在 RISC-V Linux Kernel 中这一段区域为0xffffffe000000000 ~ 0xffffffff00000000
, 共 124 GB 。
3.2 RISC-V Virtual-Memory System (Sv39)¶
3.2.1 satp
Register(Supervisor Address Translation and Protection Register)¶
63 60 59 44 43 0
---------------------------------------------------------------------
| MODE | ASID | PPN |
---------------------------------------------------------------------
- MODE 字段的取值如下图:
RV 64 ---------------------------------------------------------- | Value | Name | Description | |----------------------------------------------------------| | 0 | Bare | No translation or protection | | 1 - 7 | --- | Reserved for standard use | | 8 | Sv39 | Page-based 39 bit virtual addressing | <-- 我们使用的mode | 9 | Sv48 | Page-based 48 bit virtual addressing | | 10 | Sv57 | Page-based 57 bit virtual addressing | | 11 | Sv64 | Page-based 64 bit virtual addressing | | 12 - 13 | --- | Reserved for standard use | | 14 - 15 | --- | Reserved for standard use | -----------------------------------------------------------
- ASID ( Address Space Identifier ) : 此次实验中直接置 0 即可。
- PPN ( Physical Page Number ) :顶级页表的物理页号。我们的物理页的大小为 4KB, PA >> 12 == PPN。
- 具体介绍请阅读 RISC-V Privileged Spec 4.1.10 。
3.2.2 RISC-V Sv39 Virtual Address and Physical Address¶
38 30 29 21 20 12 11 0
---------------------------------------------------------------------
| VPN[2] | VPN[1] | VPN[0] | page offset |
---------------------------------------------------------------------
Sv39 virtual address
55 30 29 21 20 12 11 0
-----------------------------------------------------------------------------
| PPN[2] | PPN[1] | PPN[0] | page offset |
-----------------------------------------------------------------------------
Sv39 physical address
- Sv39 模式定义物理地址有 56 位,虚拟地址有 64 位。但是,虚拟地址的 64 位只有低 39 位有效。通过虚拟内存布局图我们可以发现,其 63-39 位为 0 时代表 user space address, 为 1 时 代表 kernel space address。
- Sv39 支持三级页表结构,VPN2-0分别代表每级页表的
虚拟页号
,PPN2-0分别代表每级页表的物理页号
。物理地址和虚拟地址的低12位表示页内偏移(page offset)。 - 具体介绍请阅读 RISC-V Privileged Spec 4.4.1 。
3.2.3 RISC-V Sv39 Page Table Entry¶
63 54 53 28 27 19 18 10 9 8 7 6 5 4 3 2 1 0
-----------------------------------------------------------------------
| Reserved | PPN[2] | PPN[1] | PPN[0] | RSW |D|A|G|U|X|W|R|V|
-----------------------------------------------------------------------
| | | | | | | | |
| | | | | | | | `---- V - Valid
| | | | | | | `------ R - Readable
| | | | | | `-------- W - Writable
| | | | | `---------- X - Executable
| | | | `------------ U - User
| | | `-------------- G - Global
| | `---------------- A - Accessed
| `------------------ D - Dirty (0 in page directory)
`---------------------- Reserved for supervisor software
- 0 ~ 9 bit: protection bits
- V : 有效位,当 V = 0, 访问该 PTE 会产生 Pagefault。
- R : R = 1 该页可读。
- W : W = 1 该页可写。
- X : X = 1 该页可执行。
- U , G , A , D , RSW 本次实验中设置为 0 即可。
- 具体介绍请阅读 RISC-V Privileged Spec 4.4.1
3.2.4 RISC-V Address Translation¶
虚拟地址转化为物理地址流程图如下,具体描述见 RISC-V Privileged Spec 4.3.2 :
Virtual Address Physical Address
9 9 9 12 55 12 11 0
┌────────────────┬────────────┬────────────┬─────────────┬────────────────┐ ┌────────────┬──────────┐
│ │ VPN[2] │ VPN[1] │ VPN[0] │ OFFSET │ │ PPN │ OFFSET │
└────────────────┴────┬───────┴─────┬──────┴──────┬──────┴───────┬────────┘ └────────────┴──────────┘
│ │ │ │ ▲ ▲
│ │ │ │ │ │
│ │ │ │ │ │
┌────────────────────────┘ │ │ │ │ │
│ │ │ │ │ │
│ │ │ └─────────────────┼──────────┘
│ ┌─────────────────┐ │ │ │
│511 │ │ ┌────────────┘ │ │
│ │ │ │ │ │
│ │ │ │ ┌─────────────────┐ │ │
│ │ │ │ 511 │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ ┌─────────────────┐ │
│ │ 44 10 │ │ │ │ │ 511 │ │ │
│ ├────────┬────────┤ │ │ │ │ │ │ │
└───►│ PPN │ flags │ │ │ │ │ │ │ │
├────┬───┴────────┤ │ │ 44 10 │ │ │ │ │
│ │ │ │ ├────────┬────────┤ │ │ │ │
│ │ │ └────►│ PPN │ flags │ │ │ │ │
│ │ │ ├────┬───┴────────┤ │ │ 44 10 │ │
│ │ │ │ │ │ │ ├────────┬────────┤ │
1 │ │ │ │ │ │ └────►│ PPN │ flags │ │
│ │ │ │ │ │ ├────┬───┴────────┤ │
0 │ │ │ │ │ │ │ │ │ │
└────┼────────────┘ 1 │ │ │ │ │ │ │
▲ │ │ │ │ │ └────────────┼────────┘
│ │ 0 │ │ │ │ │
│ └────────────────────►└────┼────────────┘ 1 │ │
│ │ │ │
┌───┴────┐ │ 0 │ │
│ satp │ └────────────────────►└─────────────────┘
└────────┘
4 实验步骤¶
4.1 准备工程¶
- 此次实验基于 lab3 同学所实现的代码进行。
- 需要修改
defs.h
, 在defs.h
添加 如下内容:#define OPENSBI_SIZE (0x200000) #define VM_START (0xffffffe000000000) #define VM_END (0xffffffff00000000) #define VM_SIZE (VM_END - VM_START) #define PA2VA_OFFSET (VM_START - PHY_START)
- 从
repo
同步以下代码:vmlinux.lds.S
,Makefile
。并按照以下步骤将这些文件正确放置。这里我们通过. └── arch └── riscv └── kernel ├── Makefile └── vmlinux.lds.S
vmlinux.lds.S
模版生成vmlinux.lds
文件。链接脚本中的ramv
代表VMA ( Virtual Memory Address )
即虚拟地址,ram
则代表LMA ( Load Memory Address )
, 即我们 OS image 被 load 的地址,可以理解为物理地址。使用以上的 vmlinux.lds 进行编译之后,得到的System.map
以及vmlinux
采用的都是虚拟地址,方便之后 Debug。 - 从本实验开始我们需要使用刷新缓存的指令扩展,并自动在编译项目前执行
clean
任务来防止对头文件的修改无法触发编译任务。在项目顶层目录的Makefile
中需要做如下更改:# Makefile ... ISA=rv64imafd_zifencei ... all: clean ${MAKE} -C lib all ${MAKE} -C test all ${MAKE} -C init all ${MAKE} -C arch/riscv all @echo -e '\n'Build Finished OK ...
4.2 开启虚拟内存映射。¶
在 RISC-V 中开启虚拟地址被分为了两步:setup_vm
以及 setup_vm_final
,下面将介绍相关的具体实现。
4.2.1 setup_vm
的实现¶
- 将 0x80000000 开始的 1GB 区域进行两次映射,其中一次是等值映射 ( PA == VA ) ,另一次是将其映射至高地址 ( PA + PV2VA_OFFSET == VA )。如下图所示:
Physical Address ------------------------------------------- | OpenSBI | Kernel | ------------------------------------------- ^ 0x80000000 ├───────────────────────────────────────────────────┐ | | Virtual Address ↓ ↓ ----------------------------------------------------------------------------------------------- | OpenSBI | Kernel | | OpenSBI | Kernel | ----------------------------------------------------------------------------------------------- ^ ^ 0x80000000 0xffffffe000000000
- 完成上述映射之后,通过
relocate
函数,完成对satp
的设置,以及跳转到对应的虚拟地址。 - 至此我们已经完成了虚拟地址的开启,之后我们运行的代码也都将在虚拟地址上运行。
// arch/riscv/kernel/vm.c /* early_pgtbl: 用于 setup_vm 进行 1GB 的 映射。 */ unsigned long early_pgtbl[512] __attribute__((__aligned__(0x1000))); void setup_vm(void) { /* 1. 由于是进行 1GB 的映射 这里不需要使用多级页表 2. 将 va 的 64bit 作为如下划分: | high bit | 9 bit | 30 bit | high bit 可以忽略 中间9 bit 作为 early_pgtbl 的 index 低 30 bit 作为 页内偏移 这里注意到 30 = 9 + 9 + 12, 即我们只使用根页表, 根页表的每个 entry 都对应 1GB 的区域。 3. Page Table Entry 的权限 V | R | W | X 位设置为 1 */ }
# head.S _start: call setup_vm call relocate ... j start_kernel relocate: # set ra = ra + PA2VA_OFFSET # set sp = sp + PA2VA_OFFSET (If you have set the sp before) ###################### # YOUR CODE HERE # ###################### # set satp with early_pgtbl ###################### # YOUR CODE HERE # ###################### # flush tlb sfence.vma zero, zero # flush icache fence.i ret .section .bss.stack .globl boot_stack boot_stack: ...
Hint 1:
sfence.vma
指令用于刷新 TLBHint 2:
fence.i
指令用于刷新 icacheHint 3: 在 set satp 前,我们只可以使用**物理地址**来打断点。设置 satp 之后,才可以使用虚拟地址打断点,同时之前设置的物理地址断点也会失效,需要删除
4.2.2 setup_vm_final
的实现¶
- 由于 setup_vm_final 中需要申请页面的接口, 应该在其之前完成内存管理初始化, 可能需要修改 mm.c 中的代码,mm.c 中初始化的函数接收的起始结束地址需要调整为虚拟地址。
-
对 所有物理内存 (128M) 进行映射,并设置正确的权限。
Physical Address PHY_START PHY_END ↓ ↓ -------------------------------------------------------- | OpenSBI | Kernel | | -------------------------------------------------------- ^ ^ 0x80000000 └───────────────────────────────────────────────────┐ └───────────────────────────────────────────────────┐ | | | VM_START | Virtual Address ↓ ↓ ---------------------------------------------------------------------------------------------------- | OpenSBI | Kernel | | ----------------------------------------------------------------------------------------------------- ^ 0xffffffe000000000
-
不再需要进行等值映射
- 不再需要将 OpenSBI 的映射至高地址,因为 OpenSBI 运行在 M 态, 直接使用的物理地址。
- 采用三级页表映射。
- 在 head.S 中 适当的位置调用 setup_vm_final 。
- 请不要修改 create_mapping 的函数声明,并注意阅读下方对参数的描述。该函数会被用于测试实验的正确性。
// arch/riscv/kernel/vm.c /* swapper_pg_dir: kernel pagetable 根目录, 在 setup_vm_final 进行映射。 */ unsigned long swapper_pg_dir[512] __attribute__((__aligned__(0x1000))); void setup_vm_final(void) { memset(swapper_pg_dir, 0x0, PGSIZE); // No OpenSBI mapping required // mapping kernel text X|-|R|V create_mapping(...); // mapping kernel rodata -|-|R|V create_mapping(...); // mapping other memory -|W|R|V create_mapping(...); // set satp with swapper_pg_dir YOUR CODE HERE // flush TLB asm volatile("sfence.vma zero, zero"); // flush icache asm volatile("fence.i") return; } /* 创建多级页表映射关系 */ /* 不要修改该接口的参数和返回值 */ create_mapping(uint64 *pgtbl, uint64 va, uint64 pa, uint64 sz, int perm) { /* pgtbl 为根页表的基地址 va, pa 为需要映射的虚拟地址、物理地址 sz 为映射的大小 perm 为映射的读写权限 创建多级页表的时候可以使用 kalloc() 来获取一页作为页表目录 可以使用 V bit 来判断页表项是否存在 */ }
4.3 编译及测试¶
- 由于加入了一些新的 .c 文件,可能需要修改一些Makefile文件,请同学自己尝试修改,使项目可以编译并运行。
- 输出示例
OpenSBI v0.9 ____ _____ ____ _____ / __ \ / ____| _ \_ _| | | | |_ __ ___ _ __ | (___ | |_) || | | | | | '_ \ / _ \ '_ \ \___ \| _ < | | | |__| | |_) | __/ | | |____) | |_) || |_ \____/| .__/ \___|_| |_|_____/|____/_____| | | |_| ... Boot HART MIDELEG : 0x0000000000000222 Boot HART MEDELEG : 0x000000000000b109 ...mm_init done! ...proc_init done! Hello RISC-V idle process is running! switch to [PID = 28 COUNTER = 1] [PID = 28] is running! thread space begin at 0xffffffe007fa2000 switch to [PID = 12 COUNTER = 1] [PID = 12] is running! thread space begin at 0xffffffe007fb2000 switch to [PID = 14 COUNTER = 2] [PID = 14] is running! thread space begin at 0xffffffe007fb0000 [PID = 14] is running! thread space begin at 0xffffffe007fb0000 switch to [PID = 9 COUNTER = 2] [PID = 9] is running! thread space begin at 0xffffffe007fb5000 [PID = 9] is running! thread space begin at 0xffffffe007fb5000 switch to [PID = 2 COUNTER = 2] [PID = 2] is running! thread space begin at 0xffffffe007fbc000 [PID = 2] is running! thread space begin at 0xffffffe007fbc000 switch to [PID = 1 COUNTER = 2] [PID = 1] is running! thread space begin at 0xffffffe007fbd000 [PID = 1] is running! thread space begin at 0xffffffe007fbd000 switch to [PID = 29 COUNTER = 3] [PID = 29] is running! thread space begin at 0xffffffe007fa1000 [PID = 29] is running! thread space begin at 0xffffffe007fa1000 [PID = 29] is running! thread space begin at 0xffffffe007fa1000 switch to [PID = 11 COUNTER = 3] [PID = 11] is running! thread space begin at 0xffffffe007fb3000 ...
思考题¶
- 验证
.text
,.rodata
段的属性是否成功设置,给出截图。 - 为什么我们在
setup_vm
中需要做等值映射? - 在 Linux 中,是不需要做等值映射的。请探索一下不在
setup_vm
中做等值映射的方法。
作业提交¶
同学需要提交实验报告以及整个工程代码,在提交前请使用 make clean
清除所有构建产物。
实验报告接收截止时间2022/11/26;实验验收截止时间2022/12/01