【MIT 6.S081】Lab3: Page Tables
MIT 6.S081的Lab3: Page Tables 题解
Speed up system calls (easy)
要求
操作系统如Linux通过在用户空间和内核之间共享只读区域中的数据来加速某些系统调用,实现系统调用getpid()
的优化。
分析
-
说明:创建每个进程时映射一个只读页面到虚拟地址
USYSCALL
(在memlayout.h
中定义),页面开头存储struct ussyscall
(也在memlayout.h
中定义),并初始化它以存储当前进程的 PID,用户空间已有ugetpid()
,使用USYSCALL 映射 -
先看
ugetpid()
,将宏定义的USYSCALL
所指内容强制转换成usyscall
类型的指针从而找到进程号,就是找了共享的数据 -
提示要在
kernel/proc.c
中的proc_pagetable()
函数中完成映射,可以看到这个函数返回了一个分配的页表,过程用到了提示中也给到的mappages
做其他东西的映射,所以要用类似的形式写一个映射USYSCALL,看一下这些形式用的函数都是什么意思:-
int mappages(pagetable_t pagetable, uint64 va, uint64 size, uint64 pa, int perm)
:为从va
开始的虚拟地址(这题就是USYSCALL了)创建PTE,指向pa
开始的物理地址(这里要在proc结构体里多安排一个指针指向实际内容,也就是前面的usyscall
指针);va和size可能不是页面对齐的(这个不知道有啥用);成功时返回 ,如果walk()
不能分配所需的页表返回 ;perm
是选用在kernel/riscv.h
中的参数(也是PTE的属性,RW跟是读写权限),根据提示这里为了普通用户只读应使用PTE_R | PTE_U
-
uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free)
:删除从va
开始的映射的npages
,va
必须是页面对齐的。映射必须存在,是可以选择性的释放内存 -
uvmfree(pagetable_t pagetable, uint64 sz)
:释放用户内存页,然后释放页-表页 -
这题如果映射失败,需要把之前两个映射成功的全部释放,再释放用户内存页,因为在上述
uvmfree()
函数中调用了一个freewalk()
的函数,要求必须已经删除所有叶子映射 -
提示要在
kernel/proc.c
中的allocproc()
函数里初始化上面映射的内容,这个函数中看到了给熟悉的trapframe分配页表的判定,所以模仿着写一个对usyscall分配页表的判定;初始化那就是将这个指针指向的内容pid
赋值为这个函数前段已经获得了的pid
值
-
-
提示在
kernel/proc.c
中的freeproc()
函数中释放页面:模仿trapframe的写法即可,查了一下释放trapframe指针后面的函数proc_freepagetable()
函数,用uvmunmap
解除了上面分配的TRAMPOLINE
和TRAPFRAME
后释放页,根据上面已经查过的需要删除所有叶子映射,这里的proc_freepagetable()
函数也需要改动
实现
注意:在写的过程中会出现USYSCALL
报错和struct usyscall *
在使用时报错,而kernel/memlayout.h
中是用了ifdef LAB_PGTBL
,所以不用管,跑起来不会报错
在kernel/proc.c
中的proc_pagetable()
函数中补充:
1 | // map USYSCALL |
在kernel/proc.h
中补充:
1 | // Per-process state |
在kernel/proc.c
中的allocproc()
函数中补充:
1 | // Allocate a usyscall page. |
在kernel/proc.c
中的freeproc()
函数中补充:
1 | if(p->ucall) |
在kernel/proc.c
中的proc_freepagetable()
函数中补充:
1 | uvmunmap(pagetable, USYSCALL, 1, 0); |
测试
输入命令:
1 | sudo python3 grade-lab-pgtbl ugetpid |
![getpid](/posts/16f06914/getpid.jpg)
Print a page table (easy)
要求
写一个函数打印页表内容,参数是一个pagetable_t
,在exec.c
的return argc
前添加代码if(p->pid==1) vmprint(p->pagetable)
,打印格式:第一行page table 参数
,之后每行一个PTE,以..
表示树中的深度”,后面依次打印PTE 索引、PTE 位和 PTE 中提取的物理地址
分析
-
vmprint()
写在vm.c
中,函数要在defs.h
声明 -
上一个题有查到
kernel/vm.c
中的freewalk()
函数是递归释放页表的,提示中也提到了这个函数,用递归的方式释放所有的叶子,把释放改成打印信息即可 -
提示在
printf
里用%p
打印十六进制PTE
实现
在kernel/exec.c
中return argc
之前添加:
1 | // print pagetable |
在kernel/defs.h
中添加:
1 | // vm.c |
在kernel/vm.c
中添加两个函数,其中vmprint_help()
是用于递归打印的函数,vmprint()
用于打印第一行和开启递归(主要是需要打印一个单独的第一行,递归改迭代太麻烦,单独写一个函数用于递归了):
1 | void vmprint_help(pagetable_t pagetable, int depth) { |
测试
输入命令:
1 | sudo python3 grade-lab-pgtbl pte printout |
![pte](/posts/16f06914/pte.jpg)
Detecting which pages have been accessed (hard)
要求
实现pgaccess()
的系统调用,报告哪些页面已被访问。检查 RISC-V 页表中的访问位来检测并向用户空间报告此信息,RISC-V 硬件page walker在解决一个 TLB 未命中时会在 PTE 中标记这些位。这个系统调用有三个参数,第一个参数是第一个用户页的虚拟地址,第二个参数是页数,第三个参数是一个指针传递结果(第一页对应LSB,后面依次)
分析
- 在
kernel/sysproc.c
中实现sys_pgaccess()
:需要用到Lab2中介绍过的解析参数的函数argint()
和argaddr()
;内核传给用户信息需要Lab2中介绍过的copyout()
函数;walk()
函数对于找PTE很有用int argint(int n, int *p)
获取第n
个寄存器的值,将指针p指向它的值,argaddr(int n, uint64 *ip)
拿的是指针copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
:从内核拷贝内容到用户层,从src
拷贝len
字节到虚拟地址dstva
在页表pagetable
指向的位置,成功返回 ,否则返回pte_t * walk(pagetable_t pagetable, uint64 va, int alloc)
:返回对应虚拟地址va
的PTE的地址,如果alloc
不为 则创建所需的页。大致过程是找到合法的最顶层pte(与上一个题目中的freewalk()
是反着的),我们可以利用这个找到每一页的pte
- 在系统调用函数的实现中,首先从寄存器把三个参数拿到,接着调用一个工具函数得到结果,最后使用
copyout()
拷贝结果给用户层 - 工具函数主要是完成答案的收集,从指定页开始的 个页,每一页利用
walk()
函数找到顶层PTE,检查PTE_A
并更新答案 - 在
pgaccess_test()
里可以查到调用时的三个参数为char *
,整数,uint *
实现
在kernel/risc.h
中添加PTE_A
的定义:
1 | // access bit |
在kernel/sysproc.c
中补充sys_pgaccess()
函数:
1 | int sys_pgaccess(void) { |
在kernel/vm.c
中添加在上面函数中用到的检查访问位的工具函数:
1 | // check PTE_A |
测试
1 | sudo python3 grade-lab-pgtbl pgtbltest |
![pgtbl](/posts/16f06914/pgtbl.jpg)