Part B: 寫時(shí)拷貝的 Fork
在 Part A 中,我們通過把父進(jìn)程的所有內(nèi)存數(shù)據(jù)拷貝到子進(jìn)程實(shí)現(xiàn)了 fork(),這也是 Unix 系統(tǒng)早期的實(shí)現(xiàn)。這個(gè)拷貝到過程是 fork() 時(shí)最昂貴的操作。
然而,調(diào)用了 fork() 之后往往立即就會(huì)在子進(jìn)程中調(diào)用 exec() ,將子進(jìn)程的內(nèi)存更換為新的程序,例如 shell 經(jīng)常干的(HW:Shell)。這樣,復(fù)制父進(jìn)程的內(nèi)存這個(gè)操作就完全浪費(fèi)了。
因此,后來的 Unix 系統(tǒng)讓父、子進(jìn)程共享同一片物理內(nèi)存,直到某個(gè)進(jìn)程修改了內(nèi)存。這被稱作 copy-on-write。為了實(shí)現(xiàn)它,fork()時(shí)內(nèi)核只拷貝頁(yè)面的映射關(guān)系,而不拷貝其內(nèi)容,同時(shí)將共享的頁(yè)面標(biāo)記為只讀 (read-only)。當(dāng)父子進(jìn)程中任一方向內(nèi)存中寫入數(shù)據(jù)時(shí),就會(huì)觸發(fā) page fault。此時(shí),Unix 就知道應(yīng)該分配一個(gè)私有的可寫內(nèi)存給這個(gè)進(jìn)程。這個(gè)優(yōu)化使得 fork() + exec() 連續(xù)操作變得非常廉價(jià)。在執(zhí)行 exec() 之前,只需要拷貝一個(gè)頁(yè)面,即當(dāng)前的棧。
在 Part B 中,我們將實(shí)現(xiàn)上述更佳實(shí)現(xiàn)方式的 fork()。
用戶級(jí)別的頁(yè)錯(cuò)誤處理
內(nèi)核必須要記錄進(jìn)程不同區(qū)域出現(xiàn)頁(yè)面錯(cuò)誤時(shí)的處理方法。例如,一個(gè)棧區(qū)域的 page fault 會(huì)分配并映射一個(gè)新的頁(yè)。一個(gè) BSS 區(qū)域(用于存放程序中未初始化的全局變量、靜態(tài)變量)的頁(yè)錯(cuò)誤會(huì)分配一個(gè)新的頁(yè)面,初始化為0,再映射。
用戶級(jí)別的頁(yè)錯(cuò)誤處理流程為:
- 頁(yè)錯(cuò)誤異常,陷入內(nèi)核
- 內(nèi)核修改
%esp切換到進(jìn)程的異常棧,修改%eip讓進(jìn)程運(yùn)行 _pgfault_upcall - _pgfault_upcall 將運(yùn)行 page fault handler,此后不通過內(nèi)核切換回正常棧
設(shè)置頁(yè)錯(cuò)誤處理函數(shù)
為處理自己的頁(yè)錯(cuò)誤,進(jìn)程需要在 JOS 注冊(cè)一個(gè) page fault handler entrypoint。進(jìn)程通過 sys_env_set_pgfault_upcall 注冊(cè)自己的 entrypoint,并在 Env 結(jié)構(gòu)體中新增 env_pgfault_upcall 來記錄該信息。
Exercise 8.
Implement thesys_env_set_pgfault_upcallsystem call. Be sure to enable permission checking when looking up the environment ID of the target environment, since this is a "dangerous" system call.
進(jìn)程的正常棧和異常棧
正常運(yùn)行時(shí),JOS 的進(jìn)程會(huì)運(yùn)行在正常棧上,ESP 從USTACKTOP開始往下生長(zhǎng),棧上的數(shù)據(jù)存放在 [USTACKTOP-PGSIZE, USTACKTOP-1] 上。當(dāng)出現(xiàn)頁(yè)錯(cuò)誤時(shí),內(nèi)核會(huì)把進(jìn)程在一個(gè)新的棧(異常棧)上面重啟,運(yùn)行指定的用戶級(jí)別頁(yè)錯(cuò)誤處理函數(shù)。也就是說完成了一次進(jìn)程內(nèi)的棧切換。這個(gè)過程與 trap 的過程很相似。
JOS 的異常棧也只有一個(gè)物理頁(yè)大小,并且它的棧頂定義在虛擬內(nèi)存 UXSTACKTOP 處。當(dāng)運(yùn)行在這個(gè)棧上時(shí),用戶級(jí)別頁(yè)錯(cuò)誤處理函數(shù)可以使用 JOS 的系統(tǒng)調(diào)用來映射新的頁(yè),以修復(fù)頁(yè)錯(cuò)誤。
每個(gè)需要支持用戶級(jí)頁(yè)錯(cuò)誤處理的函數(shù)都需要分配自己的異常棧。可以使用 sys_page_alloc() 這個(gè)系統(tǒng)調(diào)用來實(shí)現(xiàn)。
用戶頁(yè)錯(cuò)誤處理函數(shù)
現(xiàn)在我們需要修改 kern/trap.c 以支持用戶級(jí)別的頁(yè)錯(cuò)誤處理。
如果沒有注冊(cè) page fault handler,JOS內(nèi)核就直接銷毀進(jìn)程。否則,內(nèi)核就會(huì)初始化一個(gè) trap frame 記錄寄存器狀態(tài),在異常棧上處理頁(yè)錯(cuò)誤,恢復(fù)進(jìn)程的執(zhí)行。UTrapframe 在異常棧棧上如下所示。
<-- UXSTACKTOP
trap-time esp
trap-time eflags
trap-time eip
trap-time eax start of struct PushRegs
trap-time ecx
trap-time edx
trap-time ebx
trap-time esp
trap-time ebp
trap-time esi
trap-time edi end of struct PushRegs
tf_err (error code)
fault_va <-- %esp when handler is run
相比 trap 時(shí)使用的 Trapframe,多了記錄錯(cuò)誤位置的 fault_va,少了段選擇器%cs, %ds, %ss。這反映了兩者最大的不同:是否發(fā)生了進(jìn)程的切換。
如果異常發(fā)生時(shí),進(jìn)程已經(jīng)在異常棧上運(yùn)行了,這就說明 page fault handler 本身出現(xiàn)了問題。這時(shí),我們就應(yīng)該在 tf->tf_esp 處分配新的棧,而不是在 UXSTACKTOP。首先需要 push 一個(gè)空的 32bit word 作為占位符,然后是一個(gè) UTrapframe 結(jié)構(gòu)體。
為檢查 tf->tf_esp 是否已經(jīng)在異常棧上了,只要檢查它是否在區(qū)間 [UXSTACKTOP-PGSIZE, UXSTACKTOP-1] 上即可。
以下9,10,11三個(gè)練習(xí),建議按照調(diào)用順序來看,即 11(設(shè)置handler)->9(切換到異常棧)->10(運(yùn)行handler,切換回正常棧)。
Exercise 9.
Implement the code inpage_fault_handlerinkern/trap.crequired to dispatch page faults to the user-mode handler. Be sure to take appropriate precautions when writing into the exception stack. (What happens if the user environment runs out of space on the exception stack?)
可參考 Exercise 10 的 lib/pfentry.S 中的注釋
較有難度的一個(gè)練習(xí)。首先需要理解用戶級(jí)別的頁(yè)錯(cuò)誤處理的步驟是:
進(jìn)程A(正常棧) -> 內(nèi)核 -> 進(jìn)程A(異常棧) -> 進(jìn)程A(正常棧)
那么內(nèi)核的工作就是修改進(jìn)程 A 的某些寄存器,并初始化異常棧,確保能順利切換到異常棧運(yùn)行。需要注意的是,由于修改了eip, env_run() 是不會(huì)返回的,因此不會(huì)繼續(xù)運(yùn)行后面銷毀進(jìn)程的代碼。
值得注意的是,如果是嵌套的頁(yè)錯(cuò)誤,為了能實(shí)現(xiàn)遞歸處理,棧留出 32bit 的空位,直接向下生長(zhǎng)。
void
page_fault_handler(struct Trapframe *tf)
{
uint32_t fault_va;
// Read processor's CR2 register to find the faulting address
fault_va = rcr2();
// Handle kernel-mode page faults.
// LAB 3: Your code here.
if ((tf->tf_cs & 3) == 0) panic("Page fault in kernel-mode");
// LAB 4: Your code here.
if (curenv->env_pgfault_upcall) {
// 初始化異常棧
struct UTrapframe *utf;
if (tf->tf_esp >= UXSTACKTOP-PGSIZE && tf->tf_esp < UXSTACKTOP) {
// from exception stack
utf = (struct UTrapframe *)(tf->tf_esp - 4 - sizeof(struct UTrapframe));
} else {
utf = (struct UTrapframe *)(UXSTACKTOP - sizeof(struct UTrapframe));
}
user_mem_assert(curenv, (void *)utf, sizeof(struct UTrapframe), PTE_U | PTE_W | PTE_P);
utf->utf_fault_va = fault_va;
utf->utf_err = tf->tf_trapno;
utf->utf_regs = tf->tf_regs;
utf->utf_eip = tf->tf_eip;
utf->utf_eflags = tf->tf_eflags;
utf->utf_esp = tf->tf_esp;
// 修改 esp 完成棧切換,修改 eip 運(yùn)行 handler
tf->tf_eip = (uintptr_t)curenv->env_pgfault_upcall;
// tf->esp = (uintptr_t)utf - 1; 不需要減1
tf->tf_esp = (uintptr_t)utf;
env_run(curenv);
}
// Destroy the environment that caused the fault.
cprintf("[%08x] user fault va %08x ip %08x\n",
curenv->env_id, fault_va, tf->tf_eip);
print_trapframe(tf);
env_destroy(curenv);
}
Question
What happens if the user environment runs out of space on the exception stack?
在 inc/memlayout.h 中可以找到:
#define UXSTACKTOP UTOP
// Next page left invalid to guard against exception stack overflow;

下面一頁(yè)是空頁(yè),內(nèi)核和用戶訪問都會(huì)報(bào)錯(cuò)。
用戶模式頁(yè)錯(cuò)誤入口
在處理完頁(yè)錯(cuò)誤之后,現(xiàn)在我們需要編寫匯編語(yǔ)句實(shí)現(xiàn)從異常棧到正常棧的切換。
Exercise 10.
Implement the_pgfault_upcallroutine inlib/pfentry.S. The interesting part is returning to the original point in the user code that caused the page fault. You'll return directly there, without going back through the kernel. The hard part is simultaneously switching stacks and re-loading the EIP.
匯編苦手,寫的很艱難,最終還是參考了別人的答案。
.text
.globl _pgfault_upcall
_pgfault_upcall:
// 調(diào)用用戶定義的頁(yè)錯(cuò)誤處理函數(shù)
// Call the C page fault handler.
pushl %esp // function argument: pointer to UTF
movl _pgfault_handler, %eax
call *%eax
addl $4, %esp // pop function argument
// LAB 4: Your code here.
movl 48(%esp), %ebp
subl $4, %ebp
movl %ebp, 48(%esp)
movl 40(%esp), %eax
movl %eax, (%ebp)
// Restore the trap-time registers. After you do this, you
// can no longer modify any general-purpose registers.
// LAB 4: Your code here.
// 跳過 utf_err 以及 utf_fault_va
addl $8, %esp
// popal 同時(shí) esp 會(huì)增加,執(zhí)行結(jié)束后 %esp 指向 utf_eip
popal
// Restore eflags from the stack. After you do this, you can
// no longer use arithmetic operations or anything else that
// modifies eflags.
// LAB 4: Your code here.
// 跳過 utf_eip
addl $4, %esp
// 恢復(fù) eflags
popfl
// Switch back to the adjusted trap-time stack.
// LAB 4: Your code here.
// 恢復(fù) trap-time 的棧頂
popl %esp
// Return to re-execute the instruction that faulted.
// LAB 4: Your code here.
// ret 指令相當(dāng)于 popl %eip
ret
首先必須要理解異常棧的結(jié)構(gòu),下圖所示的是嵌套異常時(shí)的情況。其中左邊表示內(nèi)容,右邊表示地址。需要注意的是,上一次異常的棧頂之下間隔 4byte,就是一個(gè)新的異常。

最難理解的是這一部分:
movl 48(%esp), %ebp // 使 %ebp 指向 utf_esp
subl $4, %ebp
movl %ebp, 48(%esp) // 更新 utf_esp 值為 utf_esp-4
movl 40(%esp), %eax
movl %eax, (%ebp) // 將 utf_esp-4 地址的內(nèi)容改為 utf_eip
經(jīng)過這一部分的修改,異常棧更新為(紅字標(biāo)出):

此后就是恢復(fù)各寄存器,最后的 ret 指令相當(dāng)于 popl %eip,指令寄存器的值修改為 utf_eip,達(dá)到了返回的效果。
Exercise 11.
Finishset_pgfault_handler()inlib/pgfault.c.
該練習(xí)是用戶用來指定缺頁(yè)異常處理方式的函數(shù)。代碼比較簡(jiǎn)單,但是需要區(qū)分清楚 handler,_pgfault_handler,_pgfault_upcall 三個(gè)變量。
-
handler是傳入的用戶自定義頁(yè)錯(cuò)誤處理函數(shù)指針。 -
_pgfault_upcall是一個(gè)全局變量,在lib/pfentry.S中完成的初始化。它是頁(yè)錯(cuò)誤處理的總?cè)肟?,?yè)錯(cuò)誤除了運(yùn)行 page fault handler,還需要切換回正常棧。 -
_pgfault_handler被賦值為handler,會(huì)在_pgfault_upcall中被調(diào)用,是頁(yè)錯(cuò)誤處理的一部分。具體代碼是:
.text
.globl _pgfault_upcall
_pgfault_upcall:
// Call the C page fault handler.
pushl %esp // function argument: pointer to UTF
movl _pgfault_handler, %eax
call *%eax
addl $4, %esp
void
set_pgfault_handler(void (*handler)(struct UTrapframe *utf))
{
int r;
if (_pgfault_handler == 0) {
// First time through!
// LAB 4: Your code here.
// panic("set_pgfault_handler not implemented");
envid_t e_id = sys_getenvid();
r = sys_page_alloc(e_id, (void *)(UXSTACKTOP-PGSIZE), PTE_U | PTE_W | PTE_P);
if (r < 0) {
panic("pgfault_handler: %e", r);
}
// r = sys_env_set_pgfault_upcall(e_id, handler);
r = sys_env_set_pgfault_upcall(e_id, _pgfault_upcall);
if (r < 0) {
panic("pgfault_handler: %e", r);
}
}
// Save handler pointer for assembly to call.
_pgfault_handler = handler;
}
若是第一次調(diào)用,需要首先分配一個(gè)頁(yè)面作為異常棧,并且將該進(jìn)程的 upcall 設(shè)置為 Exercise 10 中的程序。此后如果需要改變handler,不需要再重復(fù)這個(gè)工作。
最后直接通過 make grade 測(cè)試,滿足要求。
Question
Whyuser/faultallocanduser/faultallocbadbehave differently?
兩者的 page fault handler 一樣,但是一個(gè)使用 cprintf() 輸出,另一個(gè)使用 sys_cput() 輸出。
sys_cput()直接通過 lib/syscall.c 發(fā)起系統(tǒng)調(diào)用,其實(shí)現(xiàn)在 kern/syscall.c 中:
static void
sys_cputs(const char *s, size_t len)
{
// Check that the user has permission to read memory [s, s+len).
// Destroy the environment if not.
// LAB 3: Your code here.
user_mem_assert(curenv, s, len, PTE_U);
// Print the string supplied by the user.
cprintf("%.*s", len, s);
}
它檢查了內(nèi)存,因此在這里 panic 了。中途沒有觸發(fā)過頁(yè)錯(cuò)誤。
而 cprintf() 的實(shí)現(xiàn)可以在 lib/printf.c 中找到:
int
vcprintf(const char *fmt, va_list ap)
{
struct printbuf b;
b.idx = 0;
b.cnt = 0;
vprintfmt((void*)putch, &b, fmt, ap);
sys_cputs(b.buf, b.idx);
return b.cnt;
}
int
cprintf(const char *fmt, ...)
{
va_list ap;
int cnt;
va_start(ap, fmt);
cnt = vcprintf(fmt, ap);
va_end(ap);
return cnt;
}
它在調(diào)用 sys_cputs() 之前,首先在用戶態(tài)執(zhí)行了 vprintfmt() 將要輸出的字符串存入結(jié)構(gòu)體 b 中。在此過程中試圖訪問 0xdeadbeef 地址,觸發(fā)并處理了頁(yè)錯(cuò)誤(其處理方式是在錯(cuò)誤位置處分配一個(gè)字符串,內(nèi)容是 "this string was faulted in at ..."),因此在繼續(xù)調(diào)用 sys_cputs() 時(shí)不會(huì)出現(xiàn) panic。
實(shí)現(xiàn) Copy-on-Write Fork
現(xiàn)在我們已經(jīng)具備了在用戶空間實(shí)現(xiàn) copy-on-write fork() 的條件。
如同 dumbfork() 一樣,fork() 也要?jiǎng)?chuàng)建一個(gè)新進(jìn)程,并且在新進(jìn)程中建立與父進(jìn)程同樣的內(nèi)存映射。關(guān)鍵的不同點(diǎn)是,dumbfork() 拷貝了物理頁(yè)的內(nèi)容,而 fork() 僅拷貝了映射關(guān)系,僅在某個(gè)進(jìn)程需要改寫某一頁(yè)的內(nèi)容時(shí),才拷貝這一頁(yè)的內(nèi)容。其基本流程如下:
- 父進(jìn)程使用
set_pgfault_handler將pgfault()設(shè)為 page fault handler - 父進(jìn)程使用
sys_exofork()建立一個(gè)子進(jìn)程 - 對(duì)每個(gè)在
UTOP之下可寫頁(yè)面以及 COW 頁(yè)面(用PTE_COW標(biāo)識(shí)),父進(jìn)程調(diào)用duppage將其“映射”到子進(jìn)程,同時(shí)將其權(quán)限改為只讀,并用PTE_COW位來與一般只讀頁(yè)面區(qū)別
異常棧的分配方式與此不同,需要在子進(jìn)程中分配一個(gè)新頁(yè)面。因?yàn)?page fault handler 會(huì)實(shí)實(shí)在在地向異常棧寫入內(nèi)容,并在異常棧上運(yùn)行。如果異常棧頁(yè)面都用 COW 機(jī)制,那就沒有能夠執(zhí)行拷貝這個(gè)過程的載體了 - 父進(jìn)程會(huì)為子進(jìn)程設(shè)置 user page fault entrypoint
- 子進(jìn)程已經(jīng)就緒,父進(jìn)程將其設(shè)為 runnable
進(jìn)程第一次往一個(gè) COW page 寫入內(nèi)容時(shí),會(huì)發(fā)生 page fault,其流程為:
- 內(nèi)核將 page fault 傳遞至
_pgfault_upcall,它會(huì)調(diào)用pgfault()handler -
pgfault()檢查錯(cuò)誤類型,以及頁(yè)面是否標(biāo)記為PTE_COW -
pgfault()分配一個(gè)新的頁(yè)面并將 fault page 的內(nèi)容拷貝進(jìn)去,然后將舊的映射覆蓋,使其映射到該新頁(yè)面。
Exercise 12.
Implementfork,duppageandpgfaultinlib/fork.c.
Test your code with theforktreeprogram.
非常難的一個(gè)練習(xí)。
fork() 函數(shù)
首先從主函數(shù) fork() 入手,其大體結(jié)構(gòu)可以仿造 user/dumbfork.c 寫,但是有關(guān)鍵幾處不同:
設(shè)置 page fault handler,即 page fault upcall 調(diào)用的函數(shù)
duppage 的范圍不同,
fork()不需要復(fù)制內(nèi)核區(qū)域的映射為子進(jìn)程設(shè)置 page fault upcall,之所以這么做,是因?yàn)?
sys_exofork()并不會(huì)復(fù)制父進(jìn)程的e->env_pgfault_upcall給子進(jìn)程。
envid_t
fork(void)
{
// LAB 4: Your code here.
// panic("fork not implemented");
set_pgfault_handler(pgfault);
envid_t e_id = sys_exofork();
if (e_id < 0) panic("fork: %e", e_id);
if (e_id == 0) {
// child
thisenv = &envs[ENVX(sys_getenvid())];
return 0;
}
// parent
// extern unsigned char end[];
// for ((uint8_t *) addr = UTEXT; addr < end; addr += PGSIZE)
for (uintptr_t addr = UTEXT; addr < USTACKTOP; addr += PGSIZE) {
if ( (uvpd[PDX(addr)] & PTE_P) && (uvpt[PGNUM(addr)] & PTE_P) ) {
// dup page to child
duppage(e_id, PGNUM(addr));
}
}
// alloc page for exception stack
int r = sys_page_alloc(e_id, (void *)(UXSTACKTOP-PGSIZE), PTE_U | PTE_W | PTE_P);
if (r < 0) panic("fork: %e",r);
// DO NOT FORGET
extern void _pgfault_upcall();
r = sys_env_set_pgfault_upcall(e_id, _pgfault_upcall);
if (r < 0) panic("fork: set upcall for child fail, %e", r);
// mark the child environment runnable
if ((r = sys_env_set_status(e_id, ENV_RUNNABLE)) < 0)
panic("sys_env_set_status: %e", r);
return e_id;
}
duppage() 函數(shù)
該函數(shù)的作用是復(fù)制父、子進(jìn)程的頁(yè)面映射。尤其注意一個(gè)權(quán)限問題。由于 sys_page_map() 頁(yè)面的權(quán)限有硬性要求,因此必須要修正一下權(quán)限。之前沒有修正導(dǎo)致一直報(bào)錯(cuò),后來發(fā)現(xiàn)頁(yè)面權(quán)限為 0x865,不符合 sys_page_map() 要求。
static int
duppage(envid_t envid, unsigned pn)
{
int r;
// LAB 4: Your code here.
// panic("duppage not implemented");
envid_t this_env_id = sys_getenvid();
void * va = (void *)(pn * PGSIZE);
int perm = uvpt[pn] & 0xFFF;
if ( (perm & PTE_W) || (perm & PTE_COW) ) {
// marked as COW and read-only
perm |= PTE_COW;
perm &= ~PTE_W;
}
// IMPORTANT: adjust permission to the syscall
perm &= PTE_SYSCALL;
// cprintf("fromenvid = %x, toenvid = %x, dup page %d, addr = %08p, perm = %03x\n",this_env_id, envid, pn, va, perm);
if((r = sys_page_map(this_env_id, va, envid, va, perm)) < 0)
panic("duppage: %e",r);
if((r = sys_page_map(this_env_id, va, this_env_id, va, perm)) < 0)
panic("duppage: %e",r);
return 0;
}
pgfault() 函數(shù)
這是 _pgfault_upcall 中調(diào)用的頁(yè)錯(cuò)誤處理函數(shù)。在調(diào)用之前,父子進(jìn)程的頁(yè)錯(cuò)誤地址都引用同一頁(yè)物理內(nèi)存,該函數(shù)作用是分配一個(gè)物理頁(yè)面使得兩者獨(dú)立。
首先,它分配一個(gè)頁(yè)面,映射到了交換區(qū) PFTEMP 這個(gè)虛擬地址,然后通過 memmove() 函數(shù)將 addr 所在頁(yè)面拷貝至 PFTEMP,此時(shí)有兩個(gè)物理頁(yè)保存了同樣的內(nèi)容。再將 addr 也映射到 PFTEMP 對(duì)應(yīng)的物理頁(yè),最后解除了 PFTEMP 的映射,此時(shí)就只有 addr 指向新分配的物理頁(yè)了,如此就完成了錯(cuò)誤處理。
static void
pgfault(struct UTrapframe *utf)
{
void *addr = (void *) utf->utf_fault_va;
uint32_t err = utf->utf_err;
int r;
// Check that the faulting access was (1) a write, and (2) to a
// copy-on-write page. If not, panic.
// Hint:
// Use the read-only page table mappings at uvpt
// (see <inc/memlayout.h>).
// LAB 4: Your code here.
if ((err & FEC_WR)==0 || (uvpt[PGNUM(addr)] & PTE_COW)==0) {
panic("pgfault: invalid user trap frame");
}
// Allocate a new page, map it at a temporary location (PFTEMP),
// copy the data from the old page to the new page, then move the new
// page to the old page's address.
// Hint:
// You should make three system calls.
// LAB 4: Your code here.
// panic("pgfault not implemented");
envid_t envid = sys_getenvid();
if ((r = sys_page_alloc(envid, (void *)PFTEMP, PTE_P | PTE_W | PTE_U)) < 0)
panic("pgfault: page allocation failed %e", r);
addr = ROUNDDOWN(addr, PGSIZE);
memmove(PFTEMP, addr, PGSIZE);
if ((r = sys_page_unmap(envid, addr)) < 0)
panic("pgfault: page unmap failed (%e)", r);
if ((r = sys_page_map(envid, PFTEMP, envid, addr, PTE_P | PTE_W |PTE_U)) < 0)
panic("pgfault: page map failed (%e)", r);
if ((r = sys_page_unmap(envid, PFTEMP)) < 0)
panic("pgfault: page unmap failed (%e)", r);
}
可以通過 make run-forktree 驗(yàn)證結(jié)果。