你说的 easy 不是 easy,我说的 hard 是什么 hard😭,实验难度 easy, easy, hard,结果第一题差点把我送走了……MIT,你坏事做尽 😭
实验准备
把第三章看懂(条件很简单,也很 tm 难,第三章应该是我目前为止没有把英文原本读完的一章了,实在是看不下去啊 😭),可以读中文版,也可以阅读*《现代操作系统:原理与实现》*(处理器架构有所不同但无伤大雅)。
其实把网站上橙色部分都做完就行。
不如给网站:Lab: page tables (mit.edu)
请务必完成前置要求,否则你根本不知道应该修改什么(或者是不知道为什么要修改)
还有要记住的就是:fllow the hints
Speed up your system calls
在进程被创建的同时创建一个只读页面,以便内核与用户程序的交互,这样就可以不需要每次系统调用的时候都去请求读写了,典型的一种空间换时间的提高速度的方法。这里只要求优化getpid()这个系统调用。
官方能不能把函数在那个文件都说一下啊!一个一个去找真的很累啊焯
首先,我们可以《很轻易》的在 user/ulib.c 中找到 ugetpid() 的定义:
#ifdef LAB_PGTBLintugetpid(void){ struct usyscall *u = (struct usyscall *)USYSCALL; return u->pid;}#endif会发现这是灰的,但是没事!注意到第一行有一个 LIB_PGTBL,也就是当你在做 PGTBL这个实验的时候,这个函数是可以被使用的,否则就不行。
分析这个函数,可以发现用了一个 struct usyscall ,还好 MIT 告诉我们在哪里可以找到这个: kernel/memlayout.h,我们可以在里面找到一起出现的一个常量 USYSCALL。
我们可以发现,这个 u 指向的位置就是 USYSCALL 的地址,也就是说 USYSCALL 处应当存储一个 struct usyscall 才可以,根据对 ugetpid() 的描述,我们知道 USYSCALL 就存在需要我们创建的只读页表之中。
我们查看 USYSCALL 的定义:
// USYSCALL (shared with kernel)// TRAPFRAME (p->trapframe, used by the trampoline)// TRAMPOLINE (the same page as in the kernel)#define TRAMPOLINE (MAXVA - PGSIZE)#define TRAPFRAME (TRAMPOLINE - PGSIZE)#define USYSCALL (TRAPFRAME - PGSIZE)可以发现 USYSCALL 在跳板下两个 PGSIZE 的位置(可以看教材中的图来确定位置,虽然这对实验没什么作用)
接下来跟着提示来:
- 可以在
kernel/proc.c中的proc_pagetable()中执行映射 - 页面必须是只读的(且用户可操作)
- 在
allocproc()初始化进程时,开辟这个页面(初始化) - 需要在
freeproc()中释放这个页面(当进程被kill时)
那不妨从开辟页面开始:
static struct proc*allocproc(void) {// ...found: // Allocate a trapframe page. if ((p->trapframe = (struct trapframe*)kalloc()) == 0) { freeproc(p); release(&p->lock); return 0; }
// Add if ((p->usyscall = (struct usyscall*)kalloc()) == 0) { freeproc(p); release(&p->lock); return 0; } p->usyscall->pid = p->pid; // ... return 0; }这里的代码是仿照开辟 trapframe 页面的代码写的,当然,我们在这之前,还需要在 kernel/proc.h 中为 struct proc 添加一个成员变量 stryct usyscall* usyscall,以储存这个页面的地址。
在开辟完页面后,我们还需要将 pid 存进这个页面中,也就是 struct usyscall 的 pid 中。
然后,我们需要将新开辟的页面映射到物理内存上(开辟页面一律指在虚拟内存中开辟),在proc_pagetable() 中添加:
pagetable_tproc_pagetable(struct proc* p) { pagetable_t pagetable; //...
// map the trampoline code (for system call return) // at the highest user virtual address. // only the supervisor uses it, on the way // to/from user space, so not PTE_U. if (mappages(pagetable, TRAMPOLINE, PGSIZE, (uint64)trampoline, PTE_R | PTE_X) < 0) { uvmfree(pagetable, 0); return 0; } // ...
if (mappages(pagetable, USYSCALL, PGSIZE, (uint64)(p->usyscall), PTE_R | PTE_U) < 0) { uvmunmap(pagetable, TRAPFRAME, 1, 0); uvmunmap(pagetable, TRAMPOLINE, 1, 0); uvmfree(pagetable, 0); return 0; }
return pagetable;}这里需要注意两点:
- 由于是只读的,我们需要添加
PTE_R,但注意到注释中有解释,若想在用户空间中调用,需要添加PTE_U - 在映射失败时,需要取消映射,但我们需要把前两个都取消了,为什么能想到,不妨看看上面两个
if后面都跟着什么(顺序需要从后往前取消,因为后一个是依赖于前一个的)
最后就是删除这个页面:
static voidfreeproc(struct proc* p) { if (p->trapframe) kfree((void*)p->trapframe); p->trapframe = 0;
//Add if (p->usyscall) kfree((void*)p->usyscall); p->usyscall = 0; // ...}同样是仿照如何删除 trapframe 页面。但这里我们还需要在 proc_freepagetable 中释放页面映射的物理地址:
voidproc_freepagetable(pagetable_t pagetable, uint64 sz) { // ... // Add uvmunmap(pagetable, USYSCALL, 1, 0); // ...}取消虚拟内存到物理内存的映射即可。
回答一下网页上的问题:还有哪些系统调用是可以通过这种方式加速的?
这个问题很简单,这种方法本质上只是内核为用户共享了一个只读页面,因此那些需要读取内核信息的系统调用都可以使用这种方式来加速。
Print a page table
在 kernel/vm.c 中实现一个遍历进程的页表的函数 vmprint(pagetable_t pagetable),当进程的pid == 1 时执行。
实现的方式可以参照同文件下的函数 freewalk()。
voidvmprint(pagetable_t pagetable, int level) { if (!level) printf("page table %p\n", pagetable); for (int i = 0;i < 512;i++) { pte_t pte = pagetable[i]; if ((pte & PTE_V) && (pte & (PTE_R | PTE_W | PTE_X)) == 0) { // this PTE points to a lower-level page table. uint64 child = PTE2PA(pte); if (level == 0) printf("..%d: pte %p pa %p\n", i, pte, child); else if (level == 1) printf(".. ..%d: pte %p pa %p\n", i, pte, child); vmprint((pagetable_t)child, level + 1); } else if ((pte & PTE_V)) { printf(".. .. ..%d: pte %p pa %p\n", i, pte, PTE2PA(pte)); } }}显然,我们还需要在 kernel/defs.h 中注册这个函数。
最后,在 kernel/exec.c 中 return argc 之前调用即可。(这个难度确实是 easy,没骗人)
Explain the output of
vmprintin 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? What does the third to last page contain?或许可以画个图来描述?

方框中数字表示索引,长方形的解释可以参见教材中的插图:

参照教材中对PTE的解释,我们来分析后面的问题:

0x0000000021fda00f 分析后十位:0000001111,可以发现是可读可写,且可被用户访问的。
Detecting which pages have been accessed
这个题一点也不 hard 。。
从一个用户页表地址开始,搜索所有被访问过的页表并返回一个bitmap 来显示这些页是否被访问过(一个比特串,从右往左数,第 位若为 则代表第 个页面被访问过)。
需要在 kernel/sysproc.c 中实现这个函数。
当然,我们可以继续跟着提示做(这个题因为有提示所以一点都不难了)
intsys_pgaccess(void) { // lab pgtbl: your code here. uint64 buf, abits, ret = 0; int size; pte_t* pte_addr; pte_t pte; if (argaddr(0, &buf) < 0 || argaddr(2, &abits)) return -1; if (argint(1, &size) < 0) return -1; pagetable_t pagetable = myproc()->pagetable; for (int i = 0;i < size;i++) { pte_addr = walk(pagetable, buf, 0); pte = *pte_addr; if (pte & PTE_A) { *pte_addr = pte & (~PTE_A); ret |= (1 << i); } buf += PGSIZE; } if (copyout(pagetable, abits, (char*)&ret, sizeof(ret)) < 0) return -1; return 0;}主要注意四点:
- 我们需要在
kernel/defs.h中注册walk()函数才可以使用(不知道为什么这个函数是存在的但是没被注册) PTE_A这个值是需要在kernel/riscv.h中定义的,具体是第几位可以看上面关于PTE中FLAGS的图。- 计数后需要把页面设置为未访问,也就是把那一位设为
0,与上掩码的反码就可以了。 - 为什么
ret不是自加而是要这么运算呢,请注意要求返回的结果是bitmap。
如果你中途出错的话,或许可以使用vmprint()来 debug。
最终成绩

最后记得 git add . && git commit -m "finish"
其实我觉得这部分 xv6的教材讲的比较简略,可以看 《现代操作系统:原理与实现》 做进一步了解(不知道CSAPP行不行,我还没看到那)