【MIT6.S081】Lab3 page tables(上)

前言

页表是最常用的机制,操作系统通过它为每个进程提供自己的私有地址空间和内存。页表决定了内存地址的含义,以及可以访问物理内存的哪些部分。在本文中,记录了Lab: page tables的前两个实验:加速系统调用和打印页表。

Speed up system calls (easy)

通常我们在需要执行系统调用时,在操作系统中会发生从用户态到内核态的切换,这是因为这些核心的操作只能交给内核去完成。在这个实验中,xv6要求我们通过在用户空间和内核之间共享只读区域中的数据来加快某些系统调用。

由于现在只是入门如何将映射添加至页表中,这个实验只需要为xv6中的getpid()系统调用进行优化。

在xv6的实验指导书中:

创建每个进程时,在USYSCALL(memlayout.h中定义的虚拟地址)映射一个只读页。在该页面的开头,存储一个usyscall结构体(也在memlayout.h中定义),并对其进行初始化以存储当前进程的PID。

既然如此,进程结构体中也应当有一个usyscall结构体:

1
2
3
4
5
6
7
8
9
10
// kernel/proc.h

struct proc {
...
pagetable_t pagetable; // User page table
struct trapframe *trapframe; // data page for trampoline.S
struct usyscall *usyscall; // data page for USYSCALL
struct context context; // swtch() here to run process
...
};

这样就可以通过p->usyscall来获取了。

memlayout.h中我们可以看到用户态空间内存布局:

1
2
3
4
5
6
7
8
9
Address zero first:
text
original data and bss
fixed-size stack
expandable heap
...
USYSCALL (shared with kernel)
TRAPFRAME (p->trapframe, used by the trampoline)
TRAMPOLINE (the same page as in the kernel)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
MAXVA-> -------------------------------------
| TRAMPOLINE (与内核相同的页面) |
-------------------------------------
| TRAPFRAME (p->trapframe, 由跳板使用)|
-------------------------------------
| USYSCALL (与内核共享) |
-------------------------------------
| ... |
-------------------------------------
| 可扩展堆 |
-------------------------------------
| 固定大小的栈 |
-------------------------------------
| 原始数据和BSS |
-------------------------------------
| text |
0 -> -------------------------------------

我们需要做的就是仿照TRAPFRAMEUSYSCALL也做一层映射。

allocproc()为进程分配物理页时,使用kalloc()对usyscall的分配(kalloc每次从空闲页表中取出一个项,其大小为4KB):

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

proc_pagetable()函数中,其为指定进程创建用户页表,不含用户内存,但有trampoline 和 trapframe页。以下代码是对trampoline 和 trapframe进行映射。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 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;
}

// map the trapframe page just below the trampoline page, for
// trampoline.S.
if(mappages(pagetable, TRAPFRAME, PGSIZE,
(uint64)(p->trapframe), PTE_R | PTE_W) < 0){
uvmunmap(pagetable, TRAMPOLINE, 1, 0);
uvmfree(pagetable, 0);
return 0;
}

如此,我们照猫画虎,也可以写出usyscall的映射。需要注意的是,该页是read-only的,并且允许用户态访问,因此其权限应该为PTE_RPTE_U

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

在结束进程时,即freeproc函数中,也需要对usyscall的空间进行释放:

1
2
3
if(p->usyscall)
kfree((void*)p->usyscall);
p->usyscall = 0;

同时还应当在proc_freepagetable函数中解除之前对usyscall的映射:

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

这个实验要求我们将页表打印出来。在实验开始前,让我们先看看xv6中的页表。

xv6中的页表为三级页表,在VA转换为PA的过程中,处理单元会通过satp寄存器找到当前进程的页表基地址,然后取出VA中的L2部分找到一级页表的项,一级页表中的项(PTE)保存二级页表的地址,再通过L1可获取二级页表中的项,依次类推即可将VA转换为PA。

这样看来,想要打印页表,有点类似于DFS算法,需要使用递归。按照实验指导书所说,我们可以从freewalk函数中获取灵感,查看其源码可以知道如何去遍历页表项。那么按照要求所实现打印页表就比较容易了:

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
static void print_pgtbl(pagetable_t pagetable, int depth) {
if (depth > 2) {
return;
}
for(int i = 0; i < 512; i++){
pte_t pte = pagetable[i];
if (pte & PTE_V) {
if (depth == 0) {
printf("..");
} else if (depth == 1) {
printf(".. ..");
} else if (depth == 2) {
printf(".. .. ..");
}
uint64 child = PTE2PA(pte);
printf("%d: pte %p pa %p\n", i, pte, PTE2PA(pte));
print_pgtbl((pagetable_t)child, depth + 1);
}
}
}

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

由于打印页表这个操作是进程号为1的init进程做的,所以不要忘记在kernel/exec.cexec函数中添加:

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

并且在kernel/defs.h中添加vmprint的函数声明:

1
void vmprint(pagetable_t);