6.1810 Lab - Page Tables

实验指导:Lab - Page Tables

Inspect a user-process page table

运行 pgtbltest,print_pgtbl 会打印最前面的 10 个和最后面的 10 个页。这个任务是通过输出理解这些页面包含了什么、权限位是什么,以理解 xv6 user space 的内存组织结构。

pgtbltest's output
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
va 0x0 pte 0x21FC885B pa 0x87F22000 perm 0x5B
va 0x1000 pte 0x21FC7C17 pa 0x87F1F000 perm 0x17
va 0x2000 pte 0x21FC7807 pa 0x87F1E000 perm 0x7
va 0x3000 pte 0x21FC74D7 pa 0x87F1D000 perm 0xD7
va 0x4000 pte 0x0 pa 0x0 perm 0x0
va 0x5000 pte 0x0 pa 0x0 perm 0x0
va 0x6000 pte 0x0 pa 0x0 perm 0x0
va 0x7000 pte 0x0 pa 0x0 perm 0x0
va 0x8000 pte 0x0 pa 0x0 perm 0x0
va 0x9000 pte 0x0 pa 0x0 perm 0x0
va 0xFFFF6000 pte 0x0 pa 0x0 perm 0x0
va 0xFFFF7000 pte 0x0 pa 0x0 perm 0x0
va 0xFFFF8000 pte 0x0 pa 0x0 perm 0x0
va 0xFFFF9000 pte 0x0 pa 0x0 perm 0x0
va 0xFFFFA000 pte 0x0 pa 0x0 perm 0x0
va 0xFFFFB000 pte 0x0 pa 0x0 perm 0x0
va 0xFFFFC000 pte 0x0 pa 0x0 perm 0x0
va 0xFFFFD000 pte 0x0 pa 0x0 perm 0x0
va 0xFFFFE000 pte 0x21FD08C7 pa 0x87F42000 perm 0xC7
va 0xFFFFF000 pte 0x2000184B pa 0x80006000 perm 0x4B

对内容稍加整理可以发现中间的大部分 pages 是未使用的(有效位 flag PTE_V 是 0)。

根据 kernel/memlayout.h 中的定义以及 xv6 book 中的图 3.4,user space 最高的两个页面分别是 trampoline 和 trapframe。也可以从 permission mask 看出:trampoline 页可读可执行,但是不可写,用户态不可访问;trapframe 可读写,不可执行,用户态不可访问;与图 3.4 一致。

用同样的方法可以判断出低地址的四个页面分别是 Text,Data,Guard 和 Stack。

answer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
        va        pte         pa  perm
0x0 0x21fc885b 0x87f22000 VR-XU -> Text (program to execute)
0x1000 0x21fc7c17 0x87f1f000 VRW-U -> Data
0x2000 0x21fc7807 0x87f1e000 VRW-- -> Guard
0x3000 0x21fc74d7 0x87f1d000 VRW-U -> Stack
0x4000 0x0 0x0 ----- -> N/A
0x5000 0x0 0x0 ----- -> N/A
0x6000 0x0 0x0 ----- -> N/A
0x7000 0x0 0x0 ----- -> N/A
0x8000 0x0 0x0 ----- -> N/A
0x9000 0x0 0x0 ----- -> N/A
0xffff6000 0x0 0x0 ----- -> N/A
0xffff7000 0x0 0x0 ----- -> N/A
0xffff8000 0x0 0x0 ----- -> N/A
0xffff9000 0x0 0x0 ----- -> N/A
0xffffa000 0x0 0x0 ----- -> N/A
0xffffb000 0x0 0x0 ----- -> N/A
0xffffc000 0x0 0x0 ----- -> N/A
0xffffd000 0x0 0x0 ----- -> N/A
0xffffe000 0x21fd08c7 0x87f42000 VRW-- -> <MAXVA - 2 * PGSIZE>, Trapframe
0xfffff000 0x2000184b 0x80006000 VR-X- -> <MAXVA - 1 * PGSIZE>, Trampoline

Speed up system calls

这个任务要求内核把一个 usyscall 页面映射给用户空间,让 ugetpid 函数可以通过读取这个页面中的信息直接获得 pid,不经过系统调用。参考 kernel/proc.c 中对 trapframe 页面的处理方式。

首先看看 trapframe 是怎么处理的。

  • kernel/proc.c:fork 创建新的进程时,会先使用 allocproc 分配并初始化一个 PCB。
  • allocproc 会分配一个 trapframe,它是 per-process 的。
  • 接着 allocproc 会创建并初始化用户的 page table,在 proc_pagetable 函数中。这个时候会 map trampoline / trapframe 到用户内存。前一步中 trampoline 没有用 kalloc 重新分配,因为 trampoline 是所有 process 共享的。

于是我们要做的事情就是 per process 地创建 usyscall 页面,把它放在 PCB 里,然后通过 mappages 来把它引射到用户态,共享 pid。

可以看到 memlayout.h 已经定义好了一个 usyscall 的结构,以及 usyscall page 应该位于的用户地址(trapframe 底下一个页面):

kernel/memlayout.h
1
2
3
4
5
6
// ...
#define USYSCALL (TRAPFRAME - PGSIZE)

struct usyscall {
int pid; // Process ID
};

我们需要在 PCB 的定义中加一个指向 usyscall 页面的指针:

kernel/proc.h
1
2
3
4
5
6
7
// Per-process state
struct proc {
// ...

// data page for USYSCALL.
struct usyscall *usyscall;
};

由于 usyscall 也是 per-process 的(因为这个页面创建的目的是为了共享 per-process 的数据),所以我们需要在每个进程被 fork 出来的时候单独用 kalloc 分配一个页面:

kernel/proc.c:allocproc
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
static struct proc*
allocproc(void)
{
struct proc *p;

// ...

found:
p->pid = allocpid();
p->state = USED;

// Allocate a trapframe page.
if((p->trapframe = (struct trapframe *)kalloc()) == 0){
// ...
}

// Allocate a usyscall page.
if((p->usyscall = (struct usyscall *)kalloc()) == 0){
freeproc(p);
release(&p->lock);
return 0;
}

// ...

return p;
}

分配完这个页面之后,要在 user page table 创建并初始化的时候把这个 page map 到 user space。这里可以在 map 完 trampoline 和 trapframe 之后添加对 usyscall 的 map,要注意如果 map 失败则要取消对前两个页面的 map。permission 设置为用户可读。

kernel/proc.c:proc_pagetable
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_pagetable(struct proc *p)
{
pagetable_t pagetable;

// An empty page table.
pagetable = uvmcreate();
if(pagetable == 0)
return 0;

// map the trampoline code (for system call return).
// ...

// map the trapframe page.
// ...

// map the usyscall page.
if (mappages(pagetable, USYSCALL, PGSIZE, (uint64)(p->usyscall), PTE_R | PTE_U) < 0) {
uvmunmap(pagetable, TRAMPOLINE, 1, 0);
uvmunmap(pagetable, TRAPFRAME, 1, 0);
uvmfree(pagetable, 0);
return 0;
}

return pagetable;
}

这样我们就在分配并初始化 PCB 的时候分配、初始化并映射了这个 usyscall 的页面,那我们也需要在 PCB 生命周期结束的时候回收这个页面:

kernel/proc.c:freeproc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static void
freeproc(struct proc *p)
{
if(p->trapframe)
kfree((void*)p->trapframe);
p->trapframe = 0;

// release the usyscall page.
if (p->usyscall)
kfree((void *)p->usyscall);
p->usyscall = 0;

if(p->pagetable)
proc_freepagetable(p->pagetable, p->sz);
p->pagetable = 0;
// ...
}
kernel/proc.c:proc_freepagetable
1
2
3
4
5
6
7
8
void
proc_freepagetable(pagetable_t pagetable, uint64 sz)
{
uvmunmap(pagetable, TRAMPOLINE, 1, 0);
uvmunmap(pagetable, TRAPFRAME, 1, 0);
uvmunmap(pagetable, USYSCALL, 1, 0);
uvmfree(pagetable, sz);
}

最后,我们需要在 fork 的时候把 pid 写到这个页面上:

kernel/proc.c:fork
1
2
3
4
5
6
7
8
9
10
11
12
int
fork(void)
{
// ...

pid = np->pid;
np->usyscall->pid = pid; // share the pid with the user space.

// ...

return pid;
}

这个任务还留了一个问题:

Which other xv6 system call(s) could be made faster using this shared page? Explain how.

例如 read 函数从 pipe 读这个场景。xv6 的 pipe 是一个内核 buffer,用 ring buffer 实现。read 操作的本质对于 pipe 来说是维护 nread(读指针)的位置,并且把已读的内容通过 copyout 的方式复制到用户空间。这里就存在一个优化空间,如果可以把这个 kernel buffer 给 map 到 user space,让用户程序可以从这个 buffer 读,并且让用户程序维护这个 nread,就可以避免一次系统调用。不过这样从一定程度上削弱了内核对这个 buffer 的控制——原来的设计只需要用户调用一个接口就可以从 pipe 中读,使用这种方法来做纯用户态的实现需要用户理解这个 ring buffer 的模型。

这里应该还有其他的例子,等刷完在回来看看。待填坑。

Print a page table

这个任务是遍历三级页表,打印出所有有效的 PTE 的信息。思路不复杂,从给出的最顶级页表开始 DFS 即可。

不过从这个任务可以帮助理解页表的结构:

  1. typedef uint64 *pagetable_t 从定义可以看出 pagetable_t 是一个指向 uint64 的指针类型,这里其实代表了一个长度为 \(2^9 = 512\) 的 PTE 数组。
  2. 每一级页表中 PTE 所代表的区间长度是不一样的:顶级页表最大,有 \(2^9 \times 2^9 \times 2^{12}\);最低级的别表最小,只有 \(2^{12}\)
  3. 三级页表是一个三层的 512 叉树。从最终打印出来的结果可以看到用户地址空间的页表常常是稀疏的,所以多级页表可以避免页表的空间浪费。但是三级页表比一级页表多了「两跳」,这是一种用时间换空间。
  4. PTE 的后 10 位是标志位,接着的 44 位是物理地址的页面号 PPN。所以这里的 PTE2PA 是把 pte 先右移 10 位,再左移 12 位。
  5. 从 freewalk 函数可以看出当满足 (pte & (PTE_R | PTE_W | PTE_X)) == 0 条件时,当前的 pte 指向更低一级别的页表。
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
void
vmprint_dfs(pagetable_t pagetable, uint64 va, int depth) {
uint64 va_step = (1UL << (9 * (3 - depth))) << 12;
for (int i = 0; i < 512; ++i) {
pte_t pte = pagetable[i];
if (!(pte & PTE_V)) {
continue;
}

for (int j = 0; j < depth; ++j) {
printf(" ..");
}

printf("%p: pte %p pa %p\n", (void*)(va + i * va_step), (void*)pte, (void*)PTE2PA(pte));

if (depth < 3) {
vmprint_dfs((pagetable_t)PTE2PA(pte), va + i * va_step, depth + 1);
}
}
}

void
vmprint(pagetable_t pagetable) {
printf("page table %p\n", pagetable);
vmprint_dfs(pagetable, 0, 1);
}

For every leaf page in the vmprint output, explain what it logically contains and what its permission bits are, and how it relates to the output of the earlier print_pgtbl() exercise above. Figure 3.4 in the xv6 book might be helpful, although note that the figure might have a slightly different set of pages than the process that's being inspected here.

这边的 7 个页面,其中 6 个可以和第一个任务中的 6 个页面对应起来,还有一个是我们在第二个任务中加入的 usyscall。

1
2
3
4
5
6
7
8
9
10
11
..0x0000000000000000: pte 0x0000000021fc7801 pa 0x0000000087f1e000
.. ..0x0000000000000000: pte 0x0000000021fc7401 pa 0x0000000087f1d000
.. .. ..0x0000000000000000: pte 0x0000000021fc7c5b pa 0x0000000087f1f000 -> VR-XU Text
.. .. ..0x0000000000001000: pte 0x0000000021fc70d7 pa 0x0000000087f1c000 -> VRW-U Data
.. .. ..0x0000000000002000: pte 0x0000000021fc6c07 pa 0x0000000087f1b000 -> VRW-- Guard
.. .. ..0x0000000000003000: pte 0x0000000021fc68d7 pa 0x0000000087f1a000 -> VRW-U Stack
..0x0000003fc0000000: pte 0x0000000021fc8401 pa 0x0000000087f21000
.. ..0x0000003fffe00000: pte 0x0000000021fc8001 pa 0x0000000087f20000
.. .. ..0x0000003fffffd000: pte 0x0000000021fd4c13 pa 0x0000000087f53000 -> VR--U Usyscall
.. .. ..0x0000003fffffe000: pte 0x0000000021fd00c7 pa 0x0000000087f40000 -> VRW-- Trapframe
.. .. ..0x0000003ffffff000: pte 0x000000002000184b pa 0x0000000080006000 -> VR-X- Trampoline

Use superpages

这个任务要修改 xv6 内核实现 2MB 的大页(superpage),目标是通过 pgtbltest.c 中的 superpg_test 测试。

这个任务还是有些难度的。

使用大页的意义

Use of superpages decreases the amount of physical memory used by the page table, and can decrease misses in the TLB cache. For some programs this leads to large increases in performance.

减少页表占用物理内存的大小,减少 TLB 缓存为命中次数。对于一些程序来说可以很大程度提高性能。

思路和实现

从 sbrk 入手。在内存分配的过程中,它经过了这样一条链路 sbrk - growproc - uvmalloc,在 uvmalloc 中原来的代码按照每次分配一个 PGSIZE 来增长堆内存的大小,每个 PGSIZE 由 kalloc 分配。我们这里需要在待分配的大小大于 2MB 的时候,优先分配大页,也就是用 superalloc 代替 kalloc,我们需要加入这三个函数。

kernel/defs.h
1
2
3
void* superalloc(void);
void superfree(void*);
int superpage_allocable();

superalloc / superfree 好理解,用来分配和释放大页。superpage_allocable 可以用来判断当前是否可以分配大页,用来做异常触发的条件,这个函数是在我最后跑 usertests 的时候发现有 regression 才加上的。

接下来我们要仿照 4KB 页面的分配回收方法来实现这几个函数,理解这个过程很重要。4KB 的页面采用一个链表 kmem.freelist 来维护空闲的页面,每个页面的最前端的几个字节(一个指针的大小)指向下一个空闲页,这个设计很精妙,用页面本身来存下一个页面的地址。

这些页面所对应的节点在 kinit - freerange 的时候被初始化并加入 freelist。kinit 把 kernel 之后的所有虚拟地址空间(一直到 PHYSTOP)都加入了 kmem.freelist。位了实现大页,我们希望其中的一些小页面的内存合并起来,并且合并后的大页的起始地址需要和 2MB 对齐。这个对齐是必要的,因为它保证了后 21 位是 offset。从这一点看大页是在 4KB 页面机制的基础下把 L0 的页表归入了 offset。

实现它的办法有多种。我们既可以预先分配一个 4KB / 2MB 页面在 VA 中的界限,kalloc / superalloc 分别管这两个 VA 段;也可以动态地寻找这个 2MB 的页面,让 4KB 页面和 2 MB 页面混在一起。通过阅读 pgtbltest 的代码,我们知道这里只需要我们预留 8 个 2MB(原进程 (8 * (1 << 20)) = \(4 \times 2^{21}\) = 4 * 2 MB 即 4 个大页,fork 出来的进程也有 4 个大页,共 8 个)。简单起见,我们选择前者。我的代码中定义的这个界限够分配 10 个大页。以下是 kalloc.c 中增加 / 修改的部分。

kernel/kalloc.c
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#define SUPERPAGES_NUM 10
#define SUPERPAGES_START (PHYSTOP - SUPERPAGES_NUM * 2 * 1024 * 1024)

struct {
struct spinlock lock;
struct run *freelist;
} kmem,
kmem_super;

void
kinit()
{
initlock(&kmem.lock, "kmem");
freerange(end, (void*)(SUPERPAGES_START - 1));

initlock(&kmem_super.lock, "kmem_super");
freerange_super((void*)SUPERPAGES_START, (void*)PHYSTOP);
}

void
freerange_super(void *pa_start, void *pa_end) {
char *p;
p = (char *)SUPERPGROUNDUP((uint64)pa_start);
for(; p + SUPERPGSIZE <= (char*)pa_end; p += SUPERPGSIZE)
superfree(p);
}

void
superfree(void *pa)
{
struct run *r;

if(((uint64)pa % SUPERPGSIZE) != 0 || (uint64)pa < (uint64)SUPERPAGES_START || (uint64)pa >= PHYSTOP)
panic("superfree");

memset(pa, 1, SUPERPGSIZE);

r = (struct run*)pa;

acquire(&kmem_super.lock);
r->next = kmem_super.freelist;
kmem_super.freelist = r;
release(&kmem_super.lock);
}

void*
superalloc(void)
{
struct run *r;

acquire(&kmem_super.lock);
r = kmem_super.freelist;

if(r)
kmem_super.freelist = r->next;
release(&kmem_super.lock);

if(r)
memset((char*)r, 5, SUPERPGSIZE);
return (void*)r;
}

int
superpage_allocable()
{
return kmem_super.freelist != 0;
}

uvmalloc 中使用 mappages 把 kalloc / superalloc 分配出来的页面 map 到一个可用的物理地址。这里我们可以巧用 perm 中保留的第八位来作为大页的标志,好让 mappages 知道我们需要 map 一个 4KB 页还是 2 MB 页面。

kernel/riscv.h
1
2
3
4
5
6
#define PTE_V (1L << 0) // valid
#define PTE_R (1L << 1)
#define PTE_W (1L << 2)
#define PTE_X (1L << 3)
#define PTE_U (1L << 4) // user can access
#define PTE_S (1L << 8) // Super page PTE

我们也需要让 walk 知道。walk 有两个作用:一个是页面还没有 map 的时候找到一个 pte 来 map 这个页面,如果页表不存在则创建一个 4KB 的页面作为一级页表索引;另一个作用是在 VA 所对应的页面和 PTE 都是有效的情况下,可以手动翻译一个 VA。mappages 使用了前者。所以我们要让 walk 知道,如果是 2MB 页面的话,不要在 L0 级停,要在 L1 级停,因为对于 2MB 页面来说,后 21 位全是 offset。

这里我们用 alloc 参数为 2 来代表分配 2MB,当然这有些投机取巧,不过足以区分。

kernel/vm.c
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
29
30
// alloc = 1: 4KB
// alloc = 2: 2MB
pte_t *
walk(pagetable_t pagetable, uint64 va, int alloc)
{
if(va >= MAXVA)
panic("walk");

for(int level = 2; level > 0; level--) {
pte_t *pte = &pagetable[PX(level, va)];
if(*pte & PTE_V) {
pagetable = (pagetable_t)PTE2PA(*pte);
#ifdef LAB_PGTBL
if(PTE_LEAF(*pte)) {
return pte;
}
#endif
} else {
if(!alloc || (pagetable = (pde_t*)kalloc()) == 0)
return 0;
memset(pagetable, 0, PGSIZE);
*pte = PA2PTE(pagetable) | PTE_V;
}
if (alloc == 2 && level == 2) {
pagetable[PX(1, va)] |= (PTE_R);
return &pagetable[PX(1, va)];
}
}
return &pagetable[PX(0, va)];
}
kernel/vm.c
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
29
30
31
int
mappages(pagetable_t pagetable, uint64 va, uint64 size, uint64 pa, int perm)
{
uint64 a, last;
pte_t *pte;
uint64 pg_size = (perm & PTE_S) ? SUPERPGSIZE : PGSIZE;

if((va % pg_size) != 0)
panic("mappages: va not aligned");

if((size % pg_size) != 0)
panic("mappages: size not aligned");

if(size == 0)
panic("mappages: size");

a = va;
last = va + size - pg_size;
for(;;){
if((pte = walk(pagetable, a, (perm & PTE_S) ? 2 : 1)) == 0)
return -1;
if(*pte & PTE_V)
panic("mappages: remap");
*pte = PA2PTE(pa) | perm | PTE_V;
if(a == last)
break;
a += pg_size;
pa += pg_size;
}
return 0;
}

实现了这些依赖后,我们就可以实现 uvmalloc 了。位了让大页和 2MB 对齐,我们需要把当前 proc->sz 到它 roundup 到第一个 2MB 对齐位置之间的内存用 4KB 的页面分配掉。起初我尝试不分配这部分,但是在内存释放的时候出现了问题,xv6 设计的内存释放假设堆内存是连续的(非常 make sense 的假设),这部分参考 uvmfree - uvmunmap。接下来遵循待分配大于等于 2MB 时优先分配 2MB 页面的规则。这里一定要模仿原来 4KB 页面的写法,如果页面分配失败或者 map 失败,需要进行回滚,否则会在 usertests 中测出 regression。

kernel/vm.c
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
uint64
uvmalloc(pagetable_t pagetable, uint64 oldsz, uint64 newsz, int xperm)
{
char *mem;
uint64 a;
int sz;
uint64 oldsz_bk = oldsz;

if (newsz < oldsz) {
return oldsz;
}

oldsz = PGROUNDUP(oldsz);

if (newsz >= oldsz + SUPERPGSIZE) {
uint64 s_oldsz = SUPERPGROUNDUP(oldsz);
for (a = oldsz; a < s_oldsz; a += PGSIZE) {
sz = PGSIZE;
mem = kalloc();
if (mem == 0) {
uvmdealloc(pagetable, a, oldsz_bk);
return 0;
}
memset(mem, 0, sz);
if (mappages(pagetable, a, sz, (uint64)mem, PTE_R | PTE_U | xperm) != 0) {
kfree(mem);
uvmdealloc(pagetable, a, oldsz_bk);
return 0;
}
}

oldsz = s_oldsz;
}

for(a = oldsz; a < newsz; a += sz){
if (newsz >= oldsz + SUPERPGSIZE && superpage_allocable()) {
sz = SUPERPGSIZE;
mem = superalloc();
if (mem == 0) {
uvmdealloc(pagetable, a, oldsz_bk);
return 0;
}
memset(mem, 0, sz);
if (mappages(pagetable, a, sz, (uint64)mem, PTE_S | PTE_R | PTE_U | xperm) != 0) {
superfree(mem);
uvmdealloc(pagetable, a, oldsz_bk);
return 0;
}
} else {
sz = PGSIZE;
mem = kalloc();
if(mem == 0){
uvmdealloc(pagetable, a, oldsz_bk);
return 0;
}
#ifndef LAB_SYSCALL
memset(mem, 0, sz);
#endif
if(mappages(pagetable, a, sz, (uint64)mem, PTE_R|PTE_U|xperm) != 0){
kfree(mem);
uvmdealloc(pagetable, a, oldsz_bk);
return 0;
}
}
}

return newsz;
}

下面我们要实现 fork 的时候 copy 大页的功能。我们可以看出 fork 使用 uvmcopy 从父进程复制虚拟地址空间。由于我们使用了 PTE_S 标记大页,我们只需要在 uvmcopy 中通过这个标记决定使用 kalloc 还是 superalloc 分配即可。

kernel/vm.c
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
int
uvmcopy(pagetable_t old, pagetable_t new, uint64 sz)
{
pte_t *pte;
uint64 pa, i;
uint flags;
char *mem;
int szinc;
uint64 pages_alloc = 0;

for(i = 0; i < sz; i += szinc){
if((pte = walk(old, i, 0)) == 0)
panic("uvmcopy: pte should exist");
if((*pte & PTE_V) == 0)
panic("uvmcopy: page not present");
pa = PTE2PA(*pte);
flags = PTE_FLAGS(*pte);

if (flags & PTE_S) {
if ((mem = superalloc()) == 0)
goto err;
memmove(mem, (char*)pa, SUPERPGSIZE);
if (mappages(new, i, SUPERPGSIZE, (uint64)mem, flags) != 0) {
superfree(mem);
goto err;
}
} else {
if((mem = kalloc()) == 0)
goto err;
memmove(mem, (char*)pa, PGSIZE);
if(mappages(new, i, PGSIZE, (uint64)mem, flags) != 0){
kfree(mem);
goto err;
}
}

++pages_alloc;
szinc = (flags & PTE_S) ? SUPERPGSIZE : PGSIZE;
}
return 0;

err:
uvmunmap(new, 0, pages_alloc, 1);
return -1;
}

最后我们要实现回收进程的时候释放页面。释放页面的操作在 wait 函数中,链路是 wait - freeproc - proc_freepagetable - uvmfree - uvmunmap。这里我们需要改 uvmunmap。

我们看到 uvmunmap 的参数是 p->sz / PGSIZE,可能会觉得这里不太好处理,因为它把大页也按照小页面算,我最初考虑是不是要单独写函数释放大页,甚至希望把前面的设计推倒。但是这里如果只是为了过测试,可以再做个投机取巧的设计:在 uvmunmap 中还是通过 PTE_S 来决定循环的步长,每遇到一个大页相当于跳 512 个小页。在真实的设计中,需要考虑更加优雅的方法。

kernel/vm.c
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
29
30
31
32
33
34
35
36
37
38
39
void
uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free)
{
uint64 a;
pte_t *pte;

if((va % PGSIZE) != 0)
panic("uvmunmap: not aligned");

a = va;
for (uint64 i = 0; i < npages; ) {
if((pte = walk(pagetable, a, 0)) == 0)
panic("uvmunmap: walk");

if((*pte & PTE_V) == 0) {
printf("va=%ld pte=%ld\n", a, *pte);
panic("uvmunmap: not mapped");
}

if(PTE_FLAGS(*pte) == PTE_V)
panic("uvmunmap: not a leaf");

int super = (*pte & PTE_S);
if(do_free){
uint64 pa = PTE2PA(*pte);
if (super) {
superfree((void *)pa);
} else {
kfree((void*)pa);
}
}

a += super ? SUPERPGSIZE : PGSIZE;
*pte = 0;
i += super ? 512 : 1;
}

return;
}

至此,我们就实现了第四个任务所有的功能。

实验结果

搞定!