# MIT6.s081 Lab: page tables
###### tags: `mit6.s081` `作業系統 Operating System`
在本次實驗中,學員將熟悉 page table 的相關概念,並藉由修改部分 xv6 程式碼,簡化資料從 kernel space 複製到 user space 的過程。本文前面為基本觀念的筆記,Lab 相關內容請從 Print a page table 開始閱讀
## page table
作業系統的三個重要特性為:
1. 虛擬化 (Virtualization)
2. 並行 (Concurrency)
3. 持久性 (Persistence)
page table 提供了記憶體空間的虛擬化機制,其實現了一種記憶體映射,紀錄虛擬地址與物理地址的對應關係,並定義虛擬地址的有效性,以及可訪問的物理地址。作業系統為每一個進程維護一個 page table,每個都有自己的虛擬地址空間,而透過 page table 的映射規則,限制進程間隨意訪問對方的記憶體空間,達到進程間的隔離性。虛擬地址與物理地址間的映射關係,可以是一對一、一對多或多對一,這提供了很大的彈性,實現物理地址的複用。
對於任何帶有地址的指令,其地址應被考慮為虛擬地址而非物理地址,虛擬地址會被送到記憶體管理單元 MMU (Memory Management Unit),MMU 透過 page table 將虛擬地址轉換為物理地址,再以物理地址要求儲存在記憶體對應位置的資料。

## RISC-V 的 page table 結構
RISC-V 採用分頁的方式管理記憶體,一個頁的大小為 4096B (4KB),我們可以將物理地址看成一塊塊等大小的陣列,這種設計方式可以很好地支持稀疏的虛擬地址空間,解決外部碎片的問題

RISC-V 的暫存器為 64-bit,代表有 2^64 個地址,如果每個地址都有一個對應的映射關係,page table 會變得異常龐大,很有可能直接耗盡記憶體空間。值得一提的是,RISC-V 的虛擬地址只使用 39-bit,大約是 512 GB,若未來處理器的設計者認為需要更大的空間,只要從未使用的 25-bit 中拿出一部分來擴充即可。
RISC-V 採用三級的結構,虛擬地址前 27-bit 拆分為 3 段,每一段的 9 個 bit 對應相應級別 page directory 的索引,page directory 的一個條目稱為 PTE(page table entry),一個 page directory 的大小為 512*8=4096 B,與一個 page 的大小相同。
物理地址的大小為 56-bit,其中前 44-bit 為物理頁面編號 PPN(Physical Page Number),後 12-bit 則為 offset,完全繼承自虛擬記憶體的最後 12-bit。
SATP 暫存器儲存最高級 page directory 的物理地址,我們透過虛擬地址的 L2 進行索引,PTE 裡頭儲存的是下一級 page directory 的物理地址 (PPN + 0*12-bit),走過三級的結構後,最終我們可以取得實際物理地址的 PPN,再加上虛擬地址的 offset 位,完成虛擬地址到物理地址的轉換。另外要注意的是,我們不能讓地址翻譯依賴於另一個地址翻譯,因此無論是 SATP 或 PTE 儲存的都是物理地址。
PTE 後 10-bit 儲存了一些 flag,其中前 5 個比較重要,分別為
- V --> Valid,為 1 代表 PTE 是合法的
- R --> Readable,該 page 是否可讀
- W --> Writable,該 page 是否可寫
- X --> Executable,該 page 是否可執行
- U --> User,運行在 user space 的進程可否訪問

採用多級 page table 可以大幅降低記憶體空間,但每次尋址都有三次記憶體讀取的動作,這樣的代價有點高,因此幾乎所有的處理器都會有緩存紀錄最近使用過的虛擬地址,這個緩存稱為 TLB (Translation Lookside Buffer)。整個處理流程大致如下:
1. CPU 從 L1 緩存中尋找虛擬地址是否存在
2. 若不存在則向 MMU 請求資料
3. MMU 從 TLB 查找虛擬地址是否有對應的緩存
4. 若有對應緩存,則 MMU 將虛擬地址直接轉換為物理地址
5. 若無對應緩存,則 MMU 透過 page table 進行轉換,並將轉換結果寫入 TLB 中
6. 向 L2、L3 緩存甚至主記憶體請求資料 (代表這些記憶體空間都透過物理地址尋址)

## Print a page table (easy)
第一題算是熱身,讓我們熟悉 RISC-V page tables 的基本結構,題目要求在 `kernel/exec.c` 中加入函數 `vmprint`,且在 pid 為 1 時才呼叫,我們可以在 `kernel/proc.c` 中找到 pid 起始數值的定義。在 xv6 初始化 user space 的過程中,會呼叫 `allocproc` 配置第一個使用者進程,`allocproc` 則呼叫 `allocpid` 為進程提供一個編號,而 `allocpid` 使用全域變數 `nextpid` 來控管進程編號,所以 `exec` 打印的,是第一個 user 進程的 page table。
```cpp
...
int nextpid = 1;
...
// Set up first user process.
void
userinit(void)
{
struct proc *p;
p = allocproc();
initproc = p;
...
}
// Look in the process table for an UNUSED proc.
// If found, initialize state required to run in the kernel,
// and return with p->lock held.
// If there are no free procs, or a memory allocation fails, return 0.
static struct proc*
allocproc(void)
{
struct proc *p;
for(p = proc; p < &proc[NPROC]; p++) {
acquire(&p->lock);
if(p->state == UNUSED) {
goto found;
} else {
release(&p->lock);
}
}
return 0;
found:
p->pid = allocpid();
// Allocate a trapframe page.
if((p->trapframe = (struct trapframe *)kalloc()) == 0){
release(&p->lock);
return 0;
}
int
allocpid() {
int pid;
acquire(&pid_lock);
pid = nextpid;
nextpid = nextpid + 1;
release(&pid_lock);
return pid;
}
```
`vmprint` 接收 `pagetable_t` 型別的參數,並打印如下列格式,第一行為 `vmprint` 的輸入參數值,接著以深度優先的方式,打印出頁表的所有 PTE," \.\." 表達 page directory 的層別
```
page table 0x0000000087f6e000
..0: pte 0x0000000021fda801 pa 0x0000000087f6a000
.. ..0: pte 0x0000000021fda401 pa 0x0000000087f69000
.. .. ..0: pte 0x0000000021fdac1f pa 0x0000000087f6b000
.. .. ..1: pte 0x0000000021fda00f pa 0x0000000087f68000
.. .. ..2: pte 0x0000000021fd9c1f pa 0x0000000087f67000
..255: pte 0x0000000021fdb401 pa 0x0000000087f6d000
.. ..511: pte 0x0000000021fdb001 pa 0x0000000087f6c000
.. .. ..510: pte 0x0000000021fdd807 pa 0x0000000087f76000
.. .. ..511: pte 0x0000000020001c0b pa 0x0000000080007000
```
`pagetable_t` 定義於 `riscv.h`,為指向 `uint64` 的指針
```cpp
typedef uint64 *pagetable_t; // 512 PTEs
```
我們參考 `freewalk` 的實作,觀察後可以發現
- 若 PTE 存在且 Valid bit 被設置
- 若 PTE 不可讀 & 不可寫 & 不可執行,代表為第一/二級的頁表
- 反之只要 WRX 其中一個 bit 被設置,就是最後一級的頁表
```cpp
kernel/vm.c`
// Recursively free page-table pages.
// All leaf mappings must already have been removed.
void
freewalk(pagetable_t pagetable)
{
// 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) && (pte & (PTE_R|PTE_W|PTE_X)) == 0){
// this PTE points to a lower-level page table.
uint64 child = PTE2PA(pte);
freewalk((pagetable_t)child);
pagetable[i] = 0;
} else if(pte & PTE_V){
panic("freewalk: leaf");
}
}
kfree((void*)pagetable);
}
```
基於這個原則實作如下
```cpp
kernel/vm.c
void
vmprint(pagetable_t pagetable, int level) {
if (level == 0)
printf("page table %p\n", pagetable);
// iterate 512 PTEs
for (int i = 0; i < 512; i++) {
pte_t pte = pagetable[i];
if (pte & PTE_V) {
uint64 pa = PTE2PA(pte);
printf("..");
for (int j = 0; j < level; j++) {
printf(" ..");
}
printf("%d: pte %p pa %p\n",i, pte, pa);
// PTE without any WRX bit set points to low-level page table
if ((pte & (PTE_W|PTE_R|PTE_X)) == 0)
vmprint((pagetable_t)pa, level + 1);
}
}
}
```
## A kernel page table per process (hard)
xv6 為每個 user 進程維護一個 page table,並為 kernel 也維護了一個 page table,由於 kernel 並沒有 user space 的地址映射關係,若要從 kernel space 複製資料到 user space,kernel 必需透過 `walkaddr` 函數及進程的 user page table 翻譯地址,才能讀寫對應物理地址的資料及參數
接下來兩題要求修改 xv6 程式碼,讓 kernel 為每個進程維護一個 kernel page table,當陷入 kernel 時,只要將進程的 kernel page table 載入 `satp` 暫存器 ,就可以透過 `MMU` 直接使用 user space 的虛擬地址來讀寫
首先我們在 `struct proc` 中 (kernel/proc.h),添加新的變量 `k_pagetable`
```cpp
kernel/proc.h
// Per-process state
struct proc {
struct spinlock lock;
// p->lock must be held when using these:
enum procstate state; // Process state
struct proc *parent; // Parent process
void *chan; // If non-zero, sleeping on chan
int killed; // If non-zero, have been killed
int xstate; // Exit status to be returned to parent's wait
int pid; // Process ID
// these are private to the process, so p->lock need not be held.
uint64 kstack; // Virtual address of kernel stack
uint64 sz; // Size of process memory (bytes)
pagetable_t pagetable; // User page table
pagetable_t k_pagetable; // Kernel page table
struct trapframe *trapframe; // data page for trampoline.S
struct context context; // swtch() here to run process
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
char name[16]; // Process name (debugging)
};
```
添加新的變數後,需要一個函數來幫忙初始化,當陷入 kernel 時,會加載進程的 `k_pagetable`,而硬體、中斷控制等映射必需存在,kernel 才能正常工作,因此函式邏輯與 `kvminit` 相同。我們在 `kernel/vm.c` 增加兩個函數 `kptinit` 及 `ukvmmap`
```cpp
kernel/vm.c
// initialize kernel table for each process
pagetable_t
kptinit() {
pagetable_t k_pagetable;
k_pagetable = uvmcreate();
// uart registers
ukvmmap(k_pagetable, UART0, UART0, PGSIZE, PTE_R | PTE_W);
// virtio mmio disk interface
ukvmmap(k_pagetable, VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);
// CLINT
ukvmmap(k_pagetable, CLINT, CLINT, 0x10000, PTE_R | PTE_W);
// PLIC
ukvmmap(k_pagetable, PLIC, PLIC, 0x400000, PTE_R | PTE_W);
// map kernel text executable and read-only.
ukvmmap(k_pagetable, KERNBASE, KERNBASE, (uint64)etext-KERNBASE, PTE_R | PTE_X);
// map kernel data and the physical RAM we'll make use of.
ukvmmap(k_pagetable, (uint64)etext, (uint64)etext, PHYSTOP-(uint64)etext, PTE_R | PTE_W);
// map the trampoline for trap entry/exit to
// the highest virtual address in the kernel.
ukvmmap(k_pagetable, TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);
return k_pagetable;
}
// add a mapping to the kernel page table of user process.
void
ukvmmap(pagetable_t pagetable, uint64 va, uint64 pa, uint64 sz, int perm)
{
if(mappages(pagetable, va, sz, pa, perm) != 0)
panic("ukvmmap");
}
```
xv6 的調度器支持多核心與多進程,代表同一時間可能有多個核心與進程在 kernel space 中,而原先所有進程共享一個 kernel page table,因此 xv6 在初始化進程時,必需替每個進程都配置對應的 kernel stack,並映射到不同的物理地址確保隔離性
現在每個進程都有自己的 kernel page table,而每個進程的 kernel stack 都可以透過各自的 kernel page table 來尋址,==這代表我們可以將不同進程的 kernel stack 都放在同一個虛擬地址==,只要映射到不同的物理地址即可,概念可參考下圖

另外原先 `kvminithart` 的功能為載入更新後的 `kernel_pagetable`,但進程的 kernel page table 將在 `allocproc` 配置進程時動態創建及載入,所以 `kvminithart` 也可以註解掉
最終 `procinit` 僅留下配置 kernel stack 虛擬地址的部份,並將所有進程 `kstack` 的虛擬地址都配置到固定的位置
```cpp
kernel/proc.c
// initialize the proc table at boot time.
void
procinit(void)
{
struct proc *p;
initlock(&pid_lock, "nextpid");
for(p = proc; p < &proc[NPROC]; p++) {
initlock(&p->lock, "proc");
// 原先為每個進程配置 kernel stack 並於 kernel_pagetable 新增映射關係
// char *pa = kalloc();
// if(pa == 0)
// panic("kalloc");
uint64 va = KSTACK((int) 0);
// kvmmap(va, (uint64)pa, PGSIZE, PTE_R | PTE_W);
p->kstack = va;
}
// kvminithart();
}
```
現在 `allocproc` 除了要為進程配置 user page table 外,還要配置 kernel page table 及 stack
```cpp
kernel/proc.c
static struct proc*
allocproc(void)
{
...
// kernel page table
p->k_pagetable = kptinit();
if(p->k_pagetable == 0){
freeproc(p);
release(&p->lock);
return 0;
}
// map kstack to physical address and store in process's kernel table
char *pa = kalloc();
if(pa == 0)
panic("kalloc");
mappages(p->k_pagetable, p->kstack, PGSIZE, (uint64)pa, PTE_R | PTE_W);
...
return p;
}
```
在調度器將進程交給 CPU 執行前,必需載入進程的 kernel page table,調度完成後再載回 kernel 自己的 page table
```cpp
kernel/proc.c
void
scheduler(void)
{
...
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;
// switch to user kernel pagetable
w_satp(MAKE_SATP(p->k_pagetable));
sfence_vma();
swtch(&c->context, &p->context);
// Process is done running for now.
// It should have changed its p->state before coming back.
kvminithart(); //switch back to kernel_pagetable
c->proc = 0;
found = 1;
}
release(&p->lock);
}
...
}
```
`kvmpa` 改用進程自己的 kernel page table,來轉換位於 kernel stack 的虛擬地址
```cpp
kernel/vm.c
// translate a kernel virtual address to
// a physical address. only needed for
// addresses on the stack.
// assumes va is page aligned.
uint64
kvmpa(uint64 va)
{
...
struct proc *p = myproc();
pte = walk(p->k_pagetable, va, 0);
...
return pa+off;
}
```
在釋放進程的時候,必需同時釋放進程的 kernel page table
```cpp
kernel/proc.c
static void
freeproc(struct proc *p)
{
if(p->trapframe)
kfree((void*)p->trapframe);
p->trapframe = 0;
if(p->pagetable)
proc_freepagetable(p->pagetable, p->sz);
if (p->k_pagetable)
proc_freekernelpagetable(p);
p->pagetable = 0;
p->sz = 0;
p->pid = 0;
p->parent = 0;
p->name[0] = 0;
p->chan = 0;
p->killed = 0;
p->xstate = 0;
p->state = UNUSED;
}
```
第一步先將 kernel stack 的映射取消,並釋放對應的物理地址。第二步則是將 kernel page table 本身釋放掉,要注意進程的 kernel page table 僅提供地址的映射關係,不該管理實際物理 page 的創建與釋放 (也避免將 I/O 設備等映射釋放掉)
```cpp
kernel/proc.c
// Free a process's kernel page table and
// physical memory for kstack,
// should not free physical memory for I/O devices
void
proc_freekernelpagetable(struct proc *p)
{
// free kstack
uvmunmap(p->k_pagetable, p->kstack, 1, 1);
// free page table
freeprockernelpage(p->k_pagetable);
}
void
freeprockernelpage(pagetable_t 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);
freeprockernelpage((pagetable_t)child);
pagetable[i] = 0;
} else if(pte & PTE_V){
pagetable[i] = 0;
}
}
kfree((void*)pagetable);
}
```
## Simplify copyin/copyinstr (hard)
現在每個進程有獨立的 kernel page table,為了在 kernel space 可以直接 dereference user space 的虛擬地址,我們需要在 kernel page table 維護 user page table 的映射關係
xv6 user space 的虛擬地址從 0 開始,而 kernel 在成功啟動後,最低的記憶體地址為 `PLIC (0xC000000)`,代表進程的能映射的空間為 0~PLIC,在配置記憶體時必需注意。以下出自 page tables 實驗頁面的說明
>This scheme relies on the user virtual address range not overlapping the range of virtual addresses that the kernel uses for its own instructions and data. Xv6 uses virtual addresses that start at zero for user address spaces, and luckily the kernel's memory starts at higher addresses. However, this scheme does limit the maximum size of a user process to be less than the kernel's lowest virtual address. After the kernel has booted, that address is 0xC000000 in xv6, the address of the PLIC registers; see kvminit() in kernel/vm.c, kernel/memlayout.h, and Figure 3-4 in the text. You'll need to modify xv6 to prevent user processes from growing larger than the PLIC address.
但 xv6 的記憶體架構中, `PLIC` 底下還有 `CLINT`(core-local interruptor),這個地址該如何處理?以下節自 xv6 book p.51[1]
>RISC-V requires that timer interrupts be taken in machine mode, not supervisor mode. RISCV machine mode executes without paging, and with a separate set of control registers, so it’s not practical to run ordinary xv6 kernel code in machine mode. As a result, xv6 handles timer interrupts completely separately from the trap mechanism laid out above.
>Code executed in machine mode in start.c, before main, sets up to receive timer interrupts (kernel/start.c:57). Part of the job is to program the CLINT hardware (core-local interruptor) to generate an interrupt after a certain delay. Another part is to set up a scratch area, analogous to the 51 trapframe, to help the timer interrupt handler save registers and the address of the CLINT registers. Finally, start sets mtvec to timervec and enables timer interrupts.
從 xv6 book CH.5 的說明可以知道,user 進程不會使用到 `CLINT`,初始化進程時不用映射這一段地址,我們修改 `kptinit` 的程式碼
```cpp
kernel/vm.c
pagetable_t
kptinit() {
...
// virtio mmio disk interface
ukvmmap(k_pagetable, VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);
// CLINT
// ukvmmap(k_pagetable, CLINT, CLINT, 0x10000, PTE_R | PTE_W);
// PLIC
ukvmmap(k_pagetable, PLIC, PLIC, 0x400000, PTE_R | PTE_W);
...
return k_pagetable;
}
```
進程動態配置記憶體時,要確保地址不得超過 `PLIC`
```cpp
kernel/vm.c
uint64
uvmalloc(pagetable_t pagetable, uint64 oldsz, uint64 newsz)
{
char *mem;
uint64 a;
if(newsz < oldsz)
return oldsz;
// user process's va should not be larger than PLIC (0x0c000000L)
if(newsz >= PLIC)
return 0;
...
}
```
現在我們確保 kernel 不會有映射衝突的問題,接下來就是複製 user 地址的映射到進程的 kernel page table,首先參考 `uvmcopy` 實作映射複製的函式,要特別注意兩點:
1. for 迴圈的起始地址必需為 page aligned,否則在某些情況下會多複製一頁,例如當 sz 大於一頁的大小,但 start + sz 只跨過一個頁,可參考下圖

2. kernel 不能訪問帶有 `PTE_U` 的地址,必需將之清除
```cpp
kernel/vm.c
int
kvmcopyuvm(pagetable_t u_pagetable, pagetable_t k_pagetable, uint64 start, uint64 sz)
{
pte_t *pte;
uint64 pa, i, aligned;
uint flags;
// make start page aligned to avoid remap
aligned = PGROUNDUP(start);
for(i = aligned; i < start + sz; i += PGSIZE){
if((pte = walk(u_pagetable, i, 0)) == 0)
panic("kvmcopyuvm: pte should exist");
if((*pte & PTE_V) == 0)
panic("kvmcopyuvm: page not present");
pa = PTE2PA(*pte);
flags = PTE_FLAGS(*pte);
if(mappages(k_pagetable, i, PGSIZE, pa, flags & ~PTE_U) != 0){
goto err;
}
}
return 0;
err:
uvmunmap(k_pagetable, start, (i-start) / PGSIZE, 0);
return -1;
}
```
第一個 user 進程初始化時,會配置一個 page 載入 `initcode`,進程的 kernel page table 需要保存這個映射關係
```cpp
kernel/proc.c
void
userinit(void)
{
...
// allocate one user page and copy init's instructions
// and data into it.
uvminit(p->pagetable, initcode, sizeof(initcode));
p->sz = PGSIZE;
// map first PGSIZE va to process's kernel page table
kvmcopyuvm(p->pagetable, p->k_pagetable, 0, PGSIZE);
...
}
```
`fork` 系統呼叫會配置子進程 `np`,並將父進程 page table 及暫存器的內容,複製給子進程
```cpp
kernel/proc.c
int
fork(void)
{
int i, pid;
struct proc *np;
struct proc *p = myproc();
// Allocate process.
if((np = allocproc()) == 0){
return -1;
}
// Copy user memory from parent to child.
if(uvmcopy(p->pagetable, np->pagetable, p->sz) < 0){
freeproc(np);
release(&np->lock);
return -1;
}
np->sz = p->sz;
// copy user page table mappings to kernel page table
if(kvmcopyuvm(np->pagetable, np->k_pagetable, 0, np->sz) < 0){
freeproc(np);
release(&np->lock);
return -1;
}
...
return pid;
}
```
`sbrk` 呼叫 `growproc`,而 `growproc` 透過 `uvmalloc` 及 `uvmdealloc` 控制進程的記憶體空間,kernel page table 只要隨著空間的擴張或縮小更新即可
```cpp
kernel/sysproc.c
uint64
sys_sbrk(void)
{
int addr;
int n;
if(argint(0, &n) < 0)
return -1;
addr = myproc()->sz;
if(growproc(n) < 0)
return -1;
return addr;
}
int
growproc(int n)
{
uint sz;
struct proc *p = myproc();
sz = p->sz;
if(n > 0){
if((sz = uvmalloc(p->pagetable, sz, sz + n)) == 0) {
return -1;
}
if(kvmcopyuvm(p->pagetable, p->k_pagetable, sz - n, n) < 0) {
return -1;
}
} else if(n < 0){
uvmdealloc(p->pagetable, sz, sz + n);
sz = kvmdealloc(p->k_pagetable, sz, sz + n);
}
p->sz = sz;
return 0;
}
```
釋放 kernel page table 的空間時,不應釋放實際的物理地址,所以增加新的函式 `kvmdealloc`,其功能與 `uvmdealloc` 相同,差別僅在 `uvmunmap` 的 `do_free` 輸入參數為 0
```cpp
kernel/vm.c
uint64
kvmdealloc(pagetable_t pagetable, uint64 oldsz, uint64 newsz)
{
if(newsz >= oldsz)
return oldsz;
if(PGROUNDUP(newsz) < PGROUNDUP(oldsz)){
int npages = (PGROUNDUP(oldsz) - PGROUNDUP(newsz)) / PGSIZE;
uvmunmap(pagetable, PGROUNDUP(newsz), npages, 0);
}
return newsz;
}
```
`exec` 的改動應該是本次實驗最困難的部份,一開始打算配置新的 kernel page table 並刪除舊的,但這樣的作法有一些缺點:
1. 此時進程運行在 kernel space,配置新的 page table 後,必需切換 `satp` 並刷新 `TLB`,確保 `satp` 使用新的 page table
2. 如果記憶體空間已經接近上限,此時我們想配置一個新 page table,但進程還使用著舊的 page table 不能直接刪掉,變成一定要先配置再刪除,導致空間不足
最終參考了其他人的方法才恍然大悟,我們可以續用原先的 page table,只要將原先的映射清除,再複製新的映射即可,節省記憶體空間又省去切換 page table 的麻煩
```cpp
kernel/exec.c
int
exec(char *path, char **argv)
{
...
// Commit to the user image.
oldpagetable = p->pagetable;
p->pagetable = pagetable;
p->sz = sz;
p->trapframe->epc = elf.entry; // initial program counter = main
p->trapframe->sp = sp; // initial stack pointer
proc_freepagetable(oldpagetable, oldsz);
// we need to unmap kernel page table first to avoid remap
uvmunmap(p->k_pagetable, 0, oldsz/PGSIZE, 0);
kvmcopyuvm(p->pagetable, p->k_pagetable, 0, p->sz);
// print page table structure if pid == 1 (init process)
if (p->pid == 1) vmprint(p->pagetable, 0);
return argc; // this ends up in a0, the first argument to main(argc, argv)
...
return -1;
}
```
:::success
== Test pte printout ==
$ make qemu-gdb
pte printout: OK (5.5s)
== Test answers-pgtbl.txt == answers-pgtbl.txt: OK
== Test count copyin ==
$ make qemu-gdb
count copyin: OK (1.2s)
== Test usertests ==
$ make qemu-gdb
(223.4s)
== Test usertests: copyin ==
usertests: copyin: OK
== Test usertests: copyinstr1 ==
usertests: copyinstr1: OK
== Test usertests: copyinstr2 ==
usertests: copyinstr2: OK
== Test usertests: copyinstr3 ==
usertests: copyinstr3: OK
== Test usertests: sbrkmuch ==
usertests: sbrkmuch: OK
== Test usertests: all tests ==
usertests: all tests: OK
== Test time ==
time: OK
Score: 66/66
:::
完整程式碼 [github](https://github.com/Chang-Chia-Chi/MIT6.s081/tree/main/Lab3:page_tables)
## Reference
1. [xv6: a simple, Unix-like teaching operating system](https://pdos.csail.mit.edu/6.828/2020/xv6/book-riscv-rev1.pdf)
2. [Lab: page tables](https://pdos.csail.mit.edu/6.S081/2020/labs/pgtbl.html)
3. [CS:APP](http://csapp.cs.cmu.edu/3e/home.html)
4. [Chapter 3: Page Tables](https://zhuanlan.zhihu.com/p/351646541)