xv6-labs-2020.lab2.pagetable
lab2 pagetable
1.作业链接
- https://pdos.csail.mit.edu/6.828/2020/labs/pgtbl.html
2. 实习内容
2.1 vmprint()
- 实现一个函数
vmprint(pagetable_t)
,实现输出页表的功能 - 输出格式如下
- 第一行为输入的参数
- 接着输出
valid
的PTE
以及指向的pa
(物理地址)
1 | page table 0x0000000087f6e000 |
(1) 一些提示
- 可以在
kernel/vm.c
中实现vmprint()
- 使用在文件
kernel/riscv.h.
结尾定义的宏 - 参考
freewalk()
的实现 - 在
kernel/defs.h
中声明vmprint()
- 使用
%p
输出地址
(2) 准备工作
- 声明函数原型
1 | // kernel/defs.h |
- 插入测试代码
1 | /// kernel/exec.c |
(3) 实现
- 在
vm.c
中实现代码 - 实现上比较简单,只需要逐级输出即可
- 模仿
freewalk()
1 | static char* pgtbl_level[] = { |
(4) 回答问题
Explain the output of vmprint in terms of Fig 3-4 from the text.
What does page 0 contain?
What is in page 2?
When running in user mode, could the process read/write the memory mapped by page 1?
- xv6 的 3 级页表如下所示
- page 0 中保存的是第 2 级页表的起始地址,以及权限位
- page 2 中保存的是实际物理地址的页号,以及权限位
- 用户态下访问不到这里 page 1映射的地址空间,因为用户态和内核态使用的不是同一个页表,翻译得到的物理地址不同
2.2 a kernel page table per process
- xv6 实现的时候
- 所有的进程共享一个内核的 pagetable,是虚拟地址直接映射物理地址实现的
- 为每一个进程维护了一个 pagetable,利虚存技术实现的,虚拟地址从 0 开始
- 这样的话,进入内核之后用户态的页表就失效了,于是如果我们在内核态下想要直接访问用户态传过来的指针,必须要先进行额外的翻译操作,而不是直接去页表中翻译
- 这一部分实验要求每一个进程都拥有一个自己的内核页表,这个内核页表需要和当前存在的全局内核页表一致
(1) 一些提示
- 在数据结构
struct proc
中增加一个字段,为每一个进程保存它的内核页表 - 在调用
allocproc()
的时候,为每个进程分配一个内核页表- 具体的实现参照
kvminit()
- 具体的实现参照
- 确保每个进程的内核栈都在其内核页表中有映射
- 之前是都保存在内核页表中,在
procinit()
中实现的 - 每一个进程应该只需要保存他自己的内核栈的映射即可
- 之前是都保存在内核页表中,在
- 当进程调度的时候,需要切换页表
- 参考
kvminithart()
的实现 - 先调用
w_satp()
,设置第 1 级页表的基地址 - 再调用
sfence_vma()
清空 TLB(失效了)
- 参考
- 当没有其他进程运行的时候,
scheduler()
需要使用kernel_pagetable
freeproc()
进程释放的时候需要同时释放页表- 当你释放页表的时候,不能够把最后对应的物理内存也释放了,因为他们是共享的
- 因为他们都共享原来内核的代码之类的,这些不能释放
- 内核栈也不用释放,一次分配,多次使用
- 之前实现的
vmprint()
用于DEBUG
- 用于
DEBUG
,缺页异常导致的结果- A missing page table mapping will likely cause the kernel to
encounter a page fault. It will print an error that includes
sepc=0x00000000XXXXXXXX
. You can find out where the fault occurred by searching forXXXXXXXX
inkernel/kernel.asm
.
- A missing page table mapping will likely cause the kernel to
encounter a page fault. It will print an error that includes
(2) 实现
struct proc
增加一个保存内核页表的字段
1 | // kernel/proc.h |
allocproc()
分配页表- 在对页表进行映射的时候,我们需要对当前进程的内核页表进行映射
- 通过修改
kvmmap()
为kvmmap_k()
实现
1 | // kernel/proc.c |
1 | // kernel/vm.c |
- 映射内核栈
- 只需要为每个进程映射自己的内核栈,同时在
procinit()
不需要分配栈
1 | // kernel/proc.c |
- 调度的时候切换内核页表
1 | void scheduler(void) { |
- 在
freeproc()
的时候- 首先释放栈的映射
- 释放页表,但是不能释放第 3 级页表指向的物理页
1 | // kernel/proc.c |
1 | // kernel/vm.c |
- 将上面的函数加入
kernel/defs.h
1 | // kernel/proc.c |
- 执行完之后报错
1 | panic: virtio_disk_intr status |
- 这是由于在
kvmpa()
中调用walk()
的时候没有使用进程对应的内核页表导致的
1 | // kernel/vm.c |
(3) 一些细节
- 以上的解法会导致一些其他问题,例如资源申请,发现不够了,此时应该
kill
掉这个进程,而不是panic
- 例如
kvmmap_k()
的设计,我们将panic
修改为检查返回值
1 | // kernel/vm.c |
1 | // kernel/proc.c |
2.3 Simplify copyin/copyinstr
copyin()
函数在读取用户空间的虚拟地址的时候,需要先使用用户空间的页表,将其翻译成物理地址- 我们在这里需要实现将每个进程用户空间保存的页表映射存到内核页表中,从而可以直接翻译访问
(1) 一些提示
- 这个机制依赖于用户空间和内核空间的虚拟地址的范围没有交集
- 于是我们需要让用户空间的地址增长小于最小的内核虚拟地址
- xv6 启动正常工作之后的最低地址 PLIC(0C000000)
- 需要让用户空间的地址增长小于 PLIC
- 为什么不是 CLINT 呢?CLINT 都是在 M-Mode 中映射的
- 先实现
copyin()
,在copyin()
中调用copyin_new()
,之后再去实现copyinstr_new()
- 每次对用户页表进行修改的时候,同时修改内核页表
fork()
,exec()
,sbrk()
- 不要忽略第一个用户进程,
usertint()
中也需要将用户页表的映射加入到内核页表中 - 权限设置:A page with
PTE_U
set cannot be accessed in kernel mode.
(2) 实现
[1] CLINT
- 去掉内核页表对于
CLINT
的映射,注释掉即可
1 | // kernel/proc.c |
- 用户虚拟地址的增长不能超过
PLIC
1 | // kernel/proc.c |
[2] 内核页表
- 在修改用户页表的同时修改内核页表,设置权限
- 注意在释放内核页表的时候,都不能释放物理页
- 找到
uvmalloc()
函数修改页表的地方 - 构造一个复制函数
1 | // kernek/defs.h |
1 | // kernel/vm.c |
userinit()
1 | // kernel/proc.c |
fork()
1 | // kernel/proc.c |
exec()
- 注意这里需要将原来内核页表中的映射关系解除(内核映射不需要)
1 | // kernel/exec.c |
sys_sbrk()
- 可能会解除映射关系,定义函数
uvmdealloc_k()
进行处理(不释放具体的物理页)
1 | // kernel/proc.c |
1 | uint64 sys_sbrk(void) { |
[3] 测试函数
- 最后修改测试函数
1 | // kernel/vm.c |
(3) 一些细节
- 同样是
panic
的问题,在设计上我们不应该让用户的操作造成操作系统的崩溃,因此uvmalloc_k()
中的panic
应该处理掉 - 其中
panic("uvmalloc_k: pte should exist!");
是合理的,因为用于页表的映射是存在 panic("uvmalloc_k: kalloc error!");
这个需要操作系统处理,而不是报错崩溃
1 | uint64 uvmalloc_k (pagetable_t pgnew, pagetable_t pgold, uint64 oldsz, uint64 newsz) { |
- 各处检查返回值即可
1 | // kernel/exec.c |
1 | // kernel/proc.c |
1 | // kernel/proc.c |
1 | // kernel/sysproc.c |
(4) 回答问题
copyin()
中的 3 个测试
1 | if(srcva >= p->sz || srcva+len >= p->sz || srcva+len < srcva) |
- 前两个判断保证复制的整块内存区域是已经分配的,第三个判断保证 len 不为负
- 第三个判断是必要的,如果没有这个判断,
memmove()
的第三个参数类型为uint
,会导致把大量的用户页表的内容带入内核,可能对造成内核代码修改崩溃
1 | void* memmove(void *dst, const void *src, uint n); |
3. 实验结果
- 在修改完 2.3 的 (3) 之后,有一次结果超时,后来都能通过测试
1 | == Test pte printout == |
4. 遇到的困难以及收获
- 整体思路上不算是很难,而且提示给的很充足
- 但是由于测试需要跑的时间较长,整个 lab 也花了较多时间
- 做完这个 lab
之后对于操作系统内部的页表组织有了更深刻的理解,同时对比不同的组织方式,理解了它们之间的优劣
- 每个进程拥有一个页表,整个内核态拥有一个页表
- 每个进程拥有一个内核页表和用户页表
- 感觉系统性的东西,能够把想法正确实现就是很棒了
5. 对课程或 lab 的意见和建议
- 暂时没有意见
6. 参考文献
- https://pdos.csail.mit.edu/6.828/2019/xv6/book-riscv-rev1.pdf
- https://pdos.csail.mit.edu/6.828/2020/labs/pgtbl.html