【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可能不是页面对齐的(这个不知道有啥用);成功时返回 00,如果walk()不能分配所需的页表返回 1-1perm是选用在kernel/riscv.h中的参数(也是PTE的属性,RW跟是读写权限),根据提示这里为了普通用户只读应使用PTE_R | PTE_U

    • uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free):删除从va开始的映射的npagesva必须是页面对齐的。映射必须存在,是可以选择性的释放内存

    • 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解除了上面分配的TRAMPOLINETRAPFRAME后释放页,根据上面已经查过的需要删除所有叶子映射,这里的proc_freepagetable()函数也需要改动

实现

注意:在写的过程中会出现USYSCALL报错和struct usyscall *在使用时报错,而kernel/memlayout.h中是用了ifdef LAB_PGTBL,所以不用管,跑起来不会报错

kernel/proc.c中的proc_pagetable()函数中补充:

1
2
3
4
5
6
7
8
// map USYSCALL
if (mappages(pagetable, USYSCALL, PGSIZE,
(uint64)(p->ucall), PTE_R | PTE_U) < 0) {
uvmunmap(pagetable, TRAMPOLINE, 1, 0);
uvmunmap(pagetable, TRAPFRAME, 1, 0);
uvmfree(pagetable, 0);
return 0;
}

kernel/proc.h中补充:

1
2
3
4
5
// Per-process state
struct proc {
...
struct usyscall *ucall; // for USYSCALL
}

kernel/proc.c中的allocproc()函数中补充:

1
2
3
4
5
6
7
8
9
// Allocate a usyscall page.
if ((p->ucall = (struct usyscall*)kalloc()) == 0) {
freeproc(p);
release(&p->lock);
return 0;
}
...
p->ucall->pid = p->pid; // init usyscall
return 0;

kernel/proc.c中的freeproc()函数中补充:

1
2
3
4
if(p->ucall)
kfree((void*)p->ucall);
p->ucall = 0;
// before proc_freepgetable()

kernel/proc.c中的proc_freepagetable()函数中补充:

1
uvmunmap(pagetable, USYSCALL, 1, 0);

测试

输入命令:

1
sudo python3 grade-lab-pgtbl ugetpid

Print a page table (easy)

要求

写一个函数打印页表内容,参数是一个pagetable_t,在exec.creturn 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.creturn argc之前添加:

1
2
3
// print pagetable
if(p->pid == 1)
vmprint(p->pagetable);

kernel/defs.h中添加:

1
2
// vm.c
void vmprint(pagetable_t);

kernel/vm.c中添加两个函数,其中vmprint_help()是用于递归打印的函数,vmprint()用于打印第一行和开启递归(主要是需要打印一个单独的第一行,递归改迭代太麻烦,单独写一个函数用于递归了):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void vmprint_help(pagetable_t pagetable, int depth) {
for (int i = 0; i < 512; i++) {
pte_t pte = pagetable[i];
if ((pte & PTE_V) && depth <= 3) { // valid pte
uint64 child = PTE2PA(pte);
printf("..");
for (int i = 0; i < depth - 1; i++)
printf(" ..");
printf("%d: pte %p pa %p\n", i, pte, child);
vmprint_help((pagetable_t)child, depth + 1);
}
}
}
void vmprint(pagetable_t pagetable) {
printf("page table %p\n", pagetable);
vmprint_help(pagetable, 1);
}

测试

输入命令:

1
sudo python3 grade-lab-pgtbl pte printout

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指向的位置,成功返回 00 ,否则返回 1-1
    • pte_t * walk(pagetable_t pagetable, uint64 va, int alloc) :返回对应虚拟地址va的PTE的地址,如果alloc不为 00 则创建所需的页。大致过程是找到合法的最顶层pte(与上一个题目中的freewalk()是反着的),我们可以利用这个找到每一页的pte
  • 在系统调用函数的实现中,首先从寄存器把三个参数拿到,接着调用一个工具函数得到结果,最后使用copyout()拷贝结果给用户层
  • 工具函数主要是完成答案的收集,从指定页开始的 nn 个页,每一页利用walk()函数找到顶层PTE,检查PTE_A并更新答案
  • pgaccess_test()里可以查到调用时的三个参数为char *,整数,uint *

实现

kernel/risc.h中添加PTE_A的定义:

1
2
// access bit 
#define PTE_A (1L << 6)

kernel/sysproc.c中补充sys_pgaccess()函数:

1
2
3
4
5
6
7
8
9
10
11
int sys_pgaccess(void) {
uint64 addr, bits;
int npages;
if (argaddr(0, &addr) < 0 || argint(1, &npages) < 0 || argaddr(2, &bits) < 0)
return -1;
struct proc* p = myproc();
uint64 res = check_pgaccess(p->pagetable, addr, npages);
if (copyout(p->pagetable, bits, (char*)&res, sizeof(res)) < 0)
return -1;
return 0;
}

kernel/vm.c中添加在上面函数中用到的检查访问位的工具函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// check PTE_A
uint64
check_pgaccess(pagetable_t pagetable, uint64 va, int npages) {
if (va >= MAXVA)
panic("walk");
uint64 res = 0;
for (int i = 0; i < npages; i++) { // check each page
pte_t* pte = walk(pagetable, va + i * PGSIZE, 0);
if (pte && (*pte & PTE_A)) { // access
res |= (1L << i);
*pte &= ~PTE_A; // 111...0111... reset *pte
}
}
return res;
}

测试

1
sudo python3 grade-lab-pgtbl pgtbltest