Lab 3: RV64 虚拟内存管理
实验目的
- 学习虚拟内存的相关知识,实现物理地址到虚拟地址的切换。
- 了解 RISC-V 架构中 SV39 分页模式,实现虚拟地址到物理地址的映射,并对不同的段进行相应的权限设置。
实验环境
- Environment in previous labs
背景知识
前言
在 Lab2 中我们赋予了操作系统对多个线程调度以及并发执行的能力,由于目前这些线程都是内核线程,因此他们可以共享运行空间,即运行不同线程对空间的修改是相互可见的。但是如果我们需要线程相互隔离,以及在多线程的情况下更加高效的使用内存,就必须引入虚拟内存
这个概念。
虚拟内存可以为正在运行的进程提供独立的内存空间,制造一种每个进程的内存都是独立的假象。同时虚拟内存到物理内存的映射也包含了对内存的访问权限,方便内核完成权限检查。
在本次实验中,我们需要关注内核如何开启虚拟地址以及通过设置页表来实现地址映射和权限控制。
Kernel 的虚拟内存布局
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
0x0000004000000000
以下的虚拟空间作为 user space
。将 0xffffffc000000000
及以上的虚拟空间作为 kernel space
。由于我们还未引入用户态程序,目前我们只需要关注 kernel space
。
具体的虚拟内存布局可以参考这里。
在
RISC-V Linux Kernel Space
中有一段虚拟地址空间中的区域被称为direct mapping area
,为了方便访问内存,内核会预先把所有物理内存都映射至这一块区域,这种映射也被称为linear mapping
,因为该映射方式就是在物理地址上添加一个偏移,使得VA = PA + PA2VA_OFFSET
。在 RISC-V Linux Kernel 中这一段区域为0xffffffe000000000 ~ 0xffffffff00000000
,共 124 GB 。
RISC-V Virtual-Memory System (Sv39)
satp
Register(Supervisor Address Translation and Protection Register)
1 2 3 4 |
|
- MODE 字段的取值如下图:
1 2 3 4 5 6 7 8 9 10 11 12 13
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 。
RISC-V Sv39 Virtual Address and Physical Address
1 2 3 4 5 |
|
1 2 3 4 5 |
|
VPN[2] VPN[1] VPN[0]
(Virtual Page Number) 分别代表每级页表的虚拟页号
,PPN[2] PPN[1] PPN[0]
(Physical Page Number) 分别代表每级页表的物理页号
。物理地址和虚拟地址的低12位表示页内偏移(page offset)。
* 具体介绍请阅读 RISC-V Privileged Spec 4.4.1 。
RISC-V Sv39 Page Table Entry
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
- 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
RISC-V Address Translation
虚拟地址转化为物理地址流程图如下,具体描述见 RISC-V Privileged Spec 4.3.2 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
|
实验步骤
准备工程
- 此次实验基于 lab3 同学所实现的代码进行。
- 需要修改
defs.h
,在defs.h
添加如下内容:1 2 3 4 5 6 7
#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
。并按照以下步骤将这些文件正确放置。新的链接脚本中的1 2 3 4 5
. └── arch └── riscv └── kernel └── vmlinux.lds
ramv
代表VMA ( Virtual Memory Address )
即虚拟地址,ram
则代表LMA ( Load Memory Address )
,即我们 OS image 被 load 的地址,可以理解为物理地址。使用以上的 vmlinux.lds 进行编译之后,得到的System.map
以及vmlinux
中的符号采用的都是虚拟地址,方便之后 Debug。 - 从本实验开始我们需要使用刷新缓存的指令扩展,并自动在编译项目前执行
clean
任务来防止对头文件的修改无法触发编译任务。在项目顶层目录的Makefile
中需要做如下更改:1 2 3 4 5 6 7 8 9 10 11
# 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 ...
开启虚拟内存映射。
在 RISC-V 中开启虚拟地址被分为了两步:setup_vm
以及 setup_vm_final
,下面将介绍相关的具体实现。
setup_vm
的实现
- 将 0x80000000 开始的 1GB 区域进行两次映射,其中一次是等值映射 ( PA == VA ) ,另一次是将其映射到
direct mapping area
( 使得PA + PV2VA_OFFSET == VA
)。如下图所示:1 2 3 4 5 6 7 8 9 10 11 12 13 14
Physical Address ------------------------------------------- | OpenSBI | Kernel | ------------------------------------------- ↑ 0x80000000 ├───────────────────────────────────────────────────┐ | | Virtual Address ↓ ↓ ----------------------------------------------------------------------------------------------- | OpenSBI | Kernel | | OpenSBI | Kernel | ----------------------------------------------------------------------------------------------- ↑ ↑ 0x80000000 0xffffffe000000000
- 完成上述映射之后,通过
relocate
函数,完成对satp
的设置,以及跳转到对应的虚拟地址。 - 至此我们已经完成了虚拟地址的开启,之后我们运行的代码也都将在虚拟地址上运行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
// 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 */ }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
# 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 之后,才可以使用虚拟地址打断点,同时之前设置的物理地址断点也会失效,需要删除
setup_vm_final
的实现
- 由于
setup_vm_final
中需要申请页面的接口,应该在其之前完成内存管理初始化,可能需要修改mm.c
中的代码,mm.c
中初始化的函数接收的起始结束地址需要调整为虚拟地址。 -
对 所有物理内存 (128M) 进行映射,并设置正确的权限。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
Physical Address PHY_START PHY_END ↓ ↓ -------------------------------------------------------- | OpenSBI | Kernel | | -------------------------------------------------------- ↑ ↑ 0x80000000 └────────────────────────┐ └────────────────────────┐ | | | VM_START | Virtual Address ↓ ↓ ------------------------------------------------------------------------- | OpenSBI | Kernel | | ------------------------------------------------------------------------- ↑ 0xffffffe000000000
-
不再需要进行等值映射
- 不再需要将 OpenSBI 的映射到
direct mapping area
,因为 OpenSBI 运行在 M 态, 直接使用的物理地址。 - 采用三级页表映射。
- 在 head.S 中 适当的位置调用
setup_vm_final
。 - 请不要修改 create_mapping 的函数声明,并注意阅读下方对参数的描述。该函数会被用于测试实验的正确性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
// 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, uint64 perm) { /* pgtbl 为根页表的基地址 va, pa 为需要映射的虚拟地址、物理地址 sz 为映射的大小,单位为字节 perm 为映射的权限 (即页表项的低 8 位) 创建多级页表的时候可以使用 kalloc() 来获取一页作为页表目录 可以使用 V bit 来判断页表项是否存在 */ }
编译及测试
- 由于加入了一些新的 .c 文件,可能需要修改一些Makefile文件,请同学自己尝试修改,使项目可以编译并运行。
- 输出示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
OpenSBI v1.1 ____ _____ ____ _____ / __ \ / ____| _ \_ _| | | | |_ __ ___ _ __ | (___ | |_) || | | | | | '_ \ / _ \ '_ \ \___ \| _ < | | | |__| | |_) | __/ | | |____) | |_) || |_ \____/| .__/ \___|_| |_|_____/|____/_____| | | |_| ... Boot HART MIDELEG : 0x0000000000000222 Boot HART MEDELEG : 0x000000000000b109 ...mm_init done! ...proc_init done! Hello RISC-V idle process is running! SET [PID = 1 PRIORITY = 1 COUNTER = 1] SET [PID = 2 PRIORITY = 4 COUNTER = 4] SET [PID = 3 PRIORITY = 10 COUNTER = 10] SET [PID = 4 PRIORITY = 4 COUNTER = 4] switch to [PID = 3 PRIORITY = 10 COUNTER = 10] [PID = 3] is running. auto_inc_local_var = 1 ... [PID = 3] is running. auto_inc_local_var = 10 switch to [PID = 4 PRIORITY = 4 COUNTER = 4] [PID = 4] is running. auto_inc_local_var = 1 ... [PID = 4] is running. auto_inc_local_var = 4 switch to [PID = 2 PRIORITY = 4 COUNTER = 4] [PID = 2] is running. auto_inc_local_var = 1 ... [PID = 2] is running. auto_inc_local_var = 4 switch to [PID = 1 PRIORITY = 1 COUNTER = 1] [PID = 1] is running. auto_inc_local_var = 1 SET [PID = 1 PRIORITY = 1 COUNTER = 1] SET [PID = 2 PRIORITY = 4 COUNTER = 4] SET [PID = 3 PRIORITY = 10 COUNTER = 10] SET [PID = 4 PRIORITY = 4 COUNTER = 4] switch to [PID = 3 PRIORITY = 10 COUNTER = 10] [PID = 3] is running. auto_inc_local_var = 11 ...
思考题
- 验证
.text
,.rodata
段的属性是否成功设置,给出截图。 - 为什么我们在
setup_vm
中需要做等值映射? - 在 Linux 中,是不需要做等值映射的。请探索一下不在
setup_vm
中做等值映射的方法。
作业提交
同学需要提交实验报告以及整个工程代码,在提交前请使用 make clean
清除所有构建产物。