6.S081 lab3 page tables

环境配置

前两个 lab 比较基础,就不写博客记录了,于是从 lab3 开始。

环境配置参考官网 。如果使用ubuntu20.04的话,环境配置比较简单,只需要从qemu 官网下载源码,手动 build 就完成了;或者使用archlinux,一条命令便全部配置完成。笔者使用的平台是macOS 11.2.1,使用homebrew安装的qemu在前两个 lab 没有问题,但是在第三个 lab 出现了 crash,改为从源码手动编译安装qemu 5.1.0解决了。

2021-02-24 修正:做 lab4 查看call.asm,发现.text 指令长度不一,有的为 2,有的为 4,遂找人请教,猜测是指令压缩导致,于是联想到之前几乎所有人都遇到的一个问题,使用 gdb 打断点调试时,出现:”Cannot access memory at address xxx”。经过大佬查阅并尝试,发现在.gdbinit.tmpl-riscv中加入set riscv use-compressed-breakpoints yes可以有效解决。

Print a page table

该部分的内容是打印出第一个进程的用户页表。这个非常简单:

参照freewalk函数,首先在kernel/vm.c添加vmprint:

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
void
_vmprint(pagetable_t pagetable, int level) {
int j;
// there are 2^9 = 512 PTEs in a page table.
for(int i = 0; i < 512; i++){
pte_t pte = pagetable[i];
if(pte & PTE_V){
for(j = 0; j <= level; j++) {
if(j == 0)
printf("..");
else
printf(" ..");
}
uint64 child = PTE2PA(pte);
printf("%d: pte %p pa %p\n", i, pte, child);
// this PTE points to a lower-level page table.
if ((pte & (PTE_R | PTE_W | PTE_X)) == 0) {
_vmprint((pagetable_t)child, level + 1);
}
}
}
}
// print the page tables
void
vmprint(pagetable_t pagetable) {
printf("page table %p\n", pagetable);
_vmprint(pagetable, 0);
}

然后在exec.c中插入代码打印第一个进程的用户页表:

1
2
3
if(p->pid == 1) {
vmprint(p->pagetable);
}

启动后打印出如下内容:

1
2
3
4
5
6
7
8
9
10
page table 0x0000000087f67000
..0: pte 0x0000000021fd8c01 pa 0x0000000087f63000
.. ..0: pte 0x0000000021fd8801 pa 0x0000000087f62000
.. .. ..0: pte 0x0000000021fd901f pa 0x0000000087f64000
.. .. ..1: pte 0x0000000021fd840f pa 0x0000000087f61000
.. .. ..2: pte 0x0000000021fd801f pa 0x0000000087f60000
..255: pte 0x0000000021fd9801 pa 0x0000000087f66000
.. ..511: pte 0x0000000021fd9401 pa 0x0000000087f65000
.. .. ..510: pte 0x0000000021fdd807 pa 0x0000000087f76000
.. .. ..511: pte 0x0000000020001c0b pa 0x0000000080007000

在用户地址空间最高处,511,510 entry 对应trampolinetrapframe。在用户地址空间最低处,0,1,2 entry 对应text\dataguard pagestack,如果修改下_vmprint打印出更多信息,可以发现 entry 1 的PTE_U是无效的,可以防止栈溢出。顶级页表只使用到第 255 个 entry,因为xv6只使用了 38 位地址。

A kernel page table per process

第二部分是让每个进程拥有单独的内核页表,为第三部分直接使用用户虚拟地址做准备。

首先在kernel/proc.h中的struct proc定义中添加

1
pagetable_t kpagetable;

仿照kvminit,实现一个初始化进程内核页表的函数:

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
pagetable_t
proc_kvminit(void)
{
int i;
pagetable_t proc_kpagetable = uvmcreate();
if (proc_kpagetable == 0) {
return 0;
}
for(i = 1; i < 512; i++) {
proc_kpagetable[i] = kernel_pagetable[i];
}

ukvmmap(proc_kpagetable, UART0, UART0, PGSIZE, PTE_R | PTE_W);
ukvmmap(proc_kpagetable, VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);
ukvmmap(proc_kpagetable, CLINT, CLINT, 0x10000, PTE_R | PTE_W);
ukvmmap(proc_kpagetable, PLIC, PLIC, 0x400000, PTE_R | PTE_W);

return proc_kpagetable;
}

void
ukvmmap(pagetable_t kernel_pagetable ,uint64 va, uint64 pa, uint64 sz, int perm)
{
if(mappages(kernel_pagetable, va, sz, pa, perm) != 0)
panic("kvmmap");
}

根据后续实验,我们能修改的内核地址空间不超过顶级页表的第一个 entry 的地址范围,所以我们和kernel_pagetable共享其他 entry,直接进行复制,这样能够节约次级页表占用的内存空间。

kernel/proc.c中的allocproc函数,负责分配、初始化进程,在其中如下调用:

1
2
3
4
5
6
p->kpagetable = proc_kvminit();
if (p->kpagetable == 0) {
freeproc(p);
release(&p->lock);
return 0;
}

之后,官网的hint提到需要为每个进程初始化kernel stack,可能需要将proinit中的部分代码转移到allocproc中,由于我们和kernel_pagetable共享了顶级页表 entry 1 意外的所有页表,所以仍可以将kernel stack的初始化代码留在procinit中。

接下来,修改scheduler,当调度到进程执行时,将进程的内核页表载入stap寄存器(参考kvminithart),当没有进程运行时,使用kernel_pagetable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if(p->state == RUNNABLE) {
// Switch to chosen process. It is the process's job
// to release its lock and then reacquire it
// before jumping back to us.
p->state = RUNNING;
c->proc = p;

w_satp(MAKE_SATP(p->kpagetable));
sfence_vma();
swtch(&c->context, &p->context);

// Process is done running for now.
// It should have changed its p->state before coming back.
c->proc = 0;
kvminithart();
found = 1;
}

之后,我们需要在free_proc中添加释放内核页表的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
if(p->kpagetable) {
proc_freekpagetable(p->kpagetable);
}


void
proc_freekpagetable(pagetable_t kpagetable) {
pte_t pte = kpagetable[0];
pagetable_t level1 = (pagetable_t) PTE2PA(pte);
for (int i = 0; i < 512; i++) {
pte_t pte = level1[i];
if (pte & PTE_V) {
uint64 level2 = PTE2PA(pte);
kfree((void *) level2);
level1[i] = 0;
}
}
kfree((void *) level1);
kfree((void *) kpagetable);
}

注意,由于和kernel_pagetable进行了共享,所以仅释放第一个 entry 对应的次级页表;如果没有共享则需释放整个三级页表(都不能释放物理内存)。

此外,如果将kernel stack的初始化代码放置在了allocproc中,那么需要在freeproc中释放并 ummapkernel stack,并且需要在kvmpa做出修改,使用:

1
pte = walk(myproc()->kpagetable, va, 0);

Simplify copyin/copyinstr

该部分需要利用第二部分中的进程内核页表简化copyin/copyinstr函数,使之不需要传递用户页表。

根据提示,将进程的用户页表复制到其内核页表中,这样每个进程内核页表都有其对应用户页表的副本。复制的用户页表虚拟地址不能超过PLIC,之上是kernel占有的地址空间,所以需要判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void
u2kvmcopy(pagetable_t pagetable, pagetable_t kpagetable, uint64 oldsz, uint64 newsz)
{
uint64 va;
pte_t *upte;
pte_t *kpte;

if(newsz >= PLIC)
panic("u2kvmcopy: newsz too large");

for (va = oldsz; va < newsz; va += PGSIZE) {
upte = walk(pagetable, va, 0);
kpte = walk(kpagetable, va, 1);
*kpte = *upte;
// because the user mapping in kernel page table is only used for copyin
// so the kernel don't need to have the W,X,U bit turned on
*kpte &= ~(PTE_U|PTE_W|PTE_X);
}
}

注意将复制到内核页表的 entry 取消PTE_U权限。

之后在exec/fork/sbrk中,每次用户页表发生改变时,复制到内核页表中。

对于exec:

1
2
3
4
5
if(p->pid == 1) {
vmprint(p->pagetable);
}

u2kvmcopy(p->pagetable, p->kpagetable, 0, p->sz);

对于fork:

1
2
3
4
5
u2kvmcopy(np->pagetable, np->kpagetable, 0, np->sz);

release(&np->lock);

return pid;

对于sbrk,修改growproc:

1
2
3
4
5
6
7
8
9
10
11
if(n > 0){
if (PGROUNDUP(sz + n) >= PLIC)
return -1;
if((sz = uvmalloc(p->pagetable, sz, sz + n)) == 0) {
return -1;
}
} else if(n < 0){
sz = uvmdealloc(p->pagetable, sz, sz + n);
// clean that pte bits
}
u2kvmcopy(p->pagetable, p->kpagetable, p->sz, sz);

之后,在userinit中,将第一个进程的用户页表复制到内核页表:

1
2
3
4
5
p->state = RUNNABLE;

u2kvmcopy(p->pagetable, p->kpagetable, 0, p->sz);

release(&p->lock);

最后,将原cpoyin/copyinstr修改为对cpoyin_new/copyinstr_new的调用即可。

copyin_new中,做了srcva + len < srcva判断条件。这是为了防止len过大,导致溢出。

最终代码见github 仓库

作者

Naruto210

发布于

2021-02-22

更新于

2021-04-07

许可协议

评论