Part B Copy-on-Write Fork
Unix 提供 fork()
系統呼叫作為主要的程序建立基元。fork()系統呼叫複製呼叫程序(父程序)的地址空間,建立一個新程序(子程序)。
不過,在呼叫 fork()
之後,子程序往往會立即呼叫 exec()
,用新程式替換子程序的記憶體。例如,shell 通常就是這麼做的。在這種情況下,複製父程序地址空間所花費的時間基本上是白費的,因為子程序在呼叫 exec()
之前幾乎不會使用它的記憶體。
因此,後來的 Unix 版本利用虛擬記憶體硬體,允許父程序和子程序共享對映到各自地址空間的記憶體,直到其中一個程序實際修改了記憶體。這種技術被稱為 "寫時複製"(copy-on-write)。 為此,核心會在 fork()
時將父程序的地址空間對映覆制到子程序,而不是對映頁的內容,同時將現在共享的頁標記為只讀。 當兩個程序中的一個試圖寫入其中一個共享頁面時,該程序就會發生頁面錯誤。此時,Unix 核心會意識到該頁面實際上是一個 "虛擬 "或 "寫時複製 "副本,因此會為發生故障的程序建立一個新的、私有的、可寫的頁面副本。 這樣,單個頁面的內容在實際寫入之前不會被複制。這種最佳化使得在子程序中執行 fork()
之後執行 exec()
的成本大大降低:在呼叫 exec()
之前,子程序可能只需要複製一個頁面(堆疊的當前頁面)。
我們接下來的目標就是實現寫時複製的fork
使用者級頁面故障處理 User-level page fault handling
為了實現使用者級的寫時複製 fork(),exercise7做的syscall外,我們還需要實現一些基礎設施,即使用者級頁面故障處理。
注意啊,是使用者級的頁面故障處理,在 lab3 中,缺頁故障的處理函式使用的是自帶的簡易實現,它是由 trap() 呼叫的,這個過程顯然是在核心態完成的。手冊中描述如下:
核心需要跟蹤的資訊太多了。與傳統的 Unix 方法不同,你將在使用者空間中決定如何處理每個頁面故障,因為在使用者空間中,錯誤的破壞性較小。 這種設計的另一個好處是,允許程式非常靈活地定義其記憶體區域;稍後在對映和訪問基於磁碟的檔案系統上的檔案時,我們將使用使用者級頁面故障處理方法。
做到這裡一定有一堆疑問,所以可以看一下 part B後面的小標題,實際上 JOS 實現使用者級頁面故障的思路是:
- 增加一個系統呼叫,
sys_env_set_pgfault_upcall
,允許使用者程序指定自己的頁面故障程式 - 在 lab3 的
page_fault_handler
的基礎上修改,檢查異常來源是否是使用者態,如是,則呼叫上一步部指定的頁面故障程式
設定頁面故障處理程式
為了處理自己的頁面故障,使用者環境需要向 JOS 核心註冊一個頁面故障處理程式入口點。使用者環境透過新的 sys_env_set_pgfault_upcall
系統呼叫來註冊其頁面故障入口點。我們在 Env 結構中新增了一個新成員 env_pgfault_upcall
,以記錄這一資訊。
練習8
練習 8. 執行
sys_env_set_pgfault_upcall
系統呼叫。由於這是一個 "危險 "的系統呼叫,因此在查詢目標環境的環境 ID 時一定要啟用許可權檢查。
實現 sys_env_set_upcall
系統呼叫。
// 透過修改相應結構體 Env 的 “env_pgfault_upcall ”欄位,
// 為 “envid ”設定頁面故障上調。
// 當 “envid ”導致頁面故障時,
// 核心會將故障記錄推送到異常堆疊,然後分支到 “func”。
//
// 成功時返回 0,錯誤時返回 <0。 錯誤包括
// -E_BAD_ENV 如果環境 envid 當前不存在,或者呼叫者沒有許可權更改 envid。
static int
sys_env_set_pgfault_upcall(envid_t envid, void *func)
{
// LAB 4: Your code here.
// panic("sys_env_set_pgfault_upcall not implemented");
struct Env * e;
if(envid2env(envid, &e, 1)<0) // 檢查envid是否有誤
return -E_BAD_ENV ;
e->env_pgfault_upcall = func; // 設定該環境page fault的handler
return 0;
}
記得將這個系統呼叫假如 kern/syscal.c : syscall 的分發裡:
呼叫使用者頁面故障處理程式
現在我們終於要完善頁面故障處理程式 —— page_fault_handler
了。
我們知道目前的 page_fault_handler
僅僅是一個簡單實現,但他確是所有頁面故障處理的入口。如果我們希望實現使用者級頁面故障處理,那麼應該在這個地方呼叫上一步設定的處理程式。
但是, page_fault_handler 還不只是這麼簡單。想一想,如果是 page_fault_handler 自然是核心態的,但是使用者自己的處理程式肯定是使用者態的,然而我們目前中斷使用的棧卻是核心棧,使用者的處理程式肯定訪問不到。怎麼辦呢?
JOS的方法是,為每個使用者程序在各自的地址空間中劃分使用者異常棧,這個棧不可能由CPU來自動push值了,因此由我們的 page_fault_handler 來傳值。傳值的形式和 struct trapframe 類似,使用者異常棧使用 struct UTrapframe,從棧頂 UXSTACKTOP 開始,形如:
<-- 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
不過存在這種情況:進行使用者級頁面處理的過程中,又發生了頁面故障,這個時候應該在目前的使用者異常棧的基礎上,先push一個空32字,再繼續push資料。 那如何判斷某次處理究竟是不是遞迴的情況呢?
答案是:測試 tf->tf_esp 是否已經位於使用者異常棧之中。
最後,再呼叫使用者的處理程式。
所以說,我們要做的事情:
- 判斷curenv->env_pgfault_upcall 是否設定
- 修改esp,將其切換到異常棧
- 對於首次缺頁,是直接切換
- 對於遞迴缺頁,是在當前tf->tf_esp的下方。
- 再異常棧上壓入一個UTrapframe
- 將eip設定為env_pgfault_upcall
page_fault_handler
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_handler():page fault in kernel mode!\n");
// 我們已經處理過核心模式異常,所以如果我們到達這裡,頁面故障就發生在使用者模式下。
// 呼叫環境的頁面故障上調(如果有的話)。
// 在使用者異常堆疊(低於 UXSTACKTOP)上建立一個頁面故障堆疊框架,
// 然後分支到 curenv->env_pgfault_upcall。
//
// 頁面故障向上呼叫可能會導致另一個頁面故障,
// 在這種情況下,我們會遞迴分支到頁面故障向上呼叫,
// 在使用者異常堆疊頂部推送另一個頁面故障堆疊框架。
//
// 從頁面故障返回的程式碼(lib/pfentry.S)在陷阱時間棧的頂部有一個字的抓取空間,
// 這對我們來說很方便,可以更容易地恢復 eip/esp。
// 在非遞迴情況下,我們不必擔心這個問題,因為常規使用者棧的頂部是空閒的。
// 在遞迴情況下,這意味著我們必須在當前的異常棧頂和新的棧幀之間多留一個字,
// 因為異常棧 _ 就是陷阱時間棧。
//
// 如果沒有向上呼叫頁面故障,環境沒有為其異常堆疊分配頁面或無法寫入頁面,
// 或者異常堆疊溢位,則銷燬導致故障的環境。
// 請注意,本級指令碼假定您將首先檢查頁面故障上調,
// 如果沒有,則列印下面的 “使用者故障 va ”資訊。
// 其餘三個檢查可以合併為一個測試。
//
// 提示:
// user_mem_assert() 和 env_run() 在這裡很有用。
// 要改變使用者環境的執行方式,請修改'curenv->env_tf' // ('tf'變數的值為 0)。
// tf'變數指向'curenv->env_tf')。
// LAB 4: Your code here.
//檢查是否有處理頁錯誤的handler
if(curenv->env_pgfault_upcall)
{
uintptr_t stacktop = UXSTACKTOP;
//檢查是否在遞迴呼叫handler
if(tf->tf_esp > UXSTACKTOP-PGSIZE && tf->tf_esp < UXSTACKTOP)
stacktop = tf->tf_esp;
//預留32位字的scratch space
uint32_t size = sizeof(struct UTrapframe) + sizeof(uint32_t);
//檢查是否有許可權讀寫exception stack
user_mem_assert(curenv, (void *)(stacktop-size), size, PTE_U|PTE_W);
//填充UTrapframe
struct UTrapframe *utf = (struct UTrapframe *)(stacktop-size);
utf->utf_fault_va = fault_va;
utf->utf_err = tf->tf_err;
utf->utf_regs = tf->tf_regs;
utf->utf_eip = tf->tf_eip;
utf->utf_eflags = tf->tf_eflags;
utf->utf_esp = tf->tf_esp;
//設定eip和esp,執行handler
curenv->env_tf.tf_eip = (uintptr_t)curenv->env_pgfault_upcall;
curenv->env_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);
}
使用者模式頁面故障入口點
使用者級頁面故障管理還有一個問題,那就是誰負責初始化、維護使用者異常棧。我們知道,核心會幫使用者將trap-time時的狀態儲存到使用者異常棧上。
但實際上,這個使用者異常棧,從始至終都沒有被初始化過。
對於核心而言,每個使用者都有一個預設的頁面故障處理程式,那就是列印錯誤地址。然後退出。
使用者頁面故障是個自選的功能,JOS讓需要自定義處理的程序,自己初始化、維護使用者異常棧。核心至負責必要的傳值工作,即 page_fault_handler。而page_fault_handler 最後直接使用 env_run 將控制權歸還使用者了,這意味著,使用者需要自己銷燬核心傳到使用者異常棧上的資料。並且自己恢復到 trap-time 狀態。
實際上,這一步還挺不容易的,這裡存在的困難在於,我們要讓所有暫存器保持trap-time state,並跳轉回去。
- 我們不能呼叫 "jmp xxx",因為這要求我們將地址載入到某個暫存器中,而這會使得該暫存器無法保持trap-time state
- 我們也不能從異常堆疊呼叫 "ret",因為如果這樣做,%esp 就不是trap-time 的值。
因此,手冊給出的答題思路是:
- 從使用者異常棧上讀取 trap-time 的 sp
- 將 trap-time 的 eip 推送到 trap-time 的stack (即儲存到 trap-time 的 sp 所指位置)
- 從 使用者異常棧上的utrapframe,恢復暫存器狀態(跳過 eip)
- 恢復 esp (切換回 trap-time 的sp),由於第二步的操作,此時esp所指位置是 trap-time的eip
- ret,將 esp 所指的值彈給 PC。
接下來練習10 完成恢復 trap-time state,在練習11 完成使用者異常棧的初始化
Exercise 10
練習 10. 實現
lib/pfentry.S
中的_pgfault_upcall
例程。有趣的部分是返回到使用者程式碼中引起頁面故障的原始點。你將直接返回到那裡,而無需返回核心。困難的部分是同時切換堆疊和重新載入 EIP。
_pgfault_upcall
// 每當我們在使用者空間引發頁面故障時,
// 我們都會要求核心將我們重定向到這裡
//(參見 pgfault.c 中對 sys_set_pgfault_handler 的呼叫)。
//
// 當頁面故障實際發生時,如果我們尚未進入使用者異常堆疊,
// 核心會將我們的 ESP 切換到使用者異常堆疊,
// 然後將一個 UTrapframe 推入使用者異常堆疊:
//
// 陷阱時 esp
// 陷阱時 eflags
// 陷阱時 eip
// utf_regs.reg_eax
// ...
// utf_regs.reg_esi
// utf_regs.reg_edi
// utf_err(錯誤程式碼)
// utf_fault_va <-- %esp
//
// 如果這是一個遞迴故障,
// 核心將在陷阱時 esp 的上方為我們保留一個空白字,
// 以便在我們解除遞迴呼叫時進行從頭處理。
//
// 然後,我們在 C 程式碼中呼叫相應的頁面故障處理程式,
// 該處理程式由全域性變數“_pgfault_handler ”指向。
.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 // pop function argument
// Now the C page fault handler has returned and you must return
// to the trap time state.
// Push trap-time %eip onto the trap-time stack.
//
// Explanation:
// We must prepare the trap-time stack for our eventual return to
// re-execute the instruction that faulted.
// Unfortunately, we can't return directly from the exception stack:
// We can't call 'jmp', since that requires that we load the address
// into a register, and all registers must have their trap-time
// values after the return.
// We can't call 'ret' from the exception stack either, since if we
// did, %esp would have the wrong value.
// So instead, we push the trap-time %eip onto the *trap-time* stack!
// Below we'll switch to that stack and call 'ret', which will
// restore %eip to its pre-fault value.
//
// In the case of a recursive fault on the exception stack,
// note that the word we're pushing now will fit in the
// blank word that the kernel reserved for us.
//
// Throughout the remaining code, think carefully about what
// registers are available for intermediate calculations. You
// may find that you have to rearrange your code in non-obvious
// ways as registers become unavailable as scratch space.
//
// LAB 4: Your code here.
addl $8, %esp // 清除 fault_va 和 error code
movl 32(%esp), %eax // 取 trap-time-eip 到 eax
movl 40(%esp), %edx // 取 trap-time-esp 到 edx
subl $4, %edx // 在 trap-time的棧上開闢4位元組用於儲存 trap-time-eip
movl %eax, (%edx) // 將 trap-time-eip 儲存到 trap-time-esp
movl %edx, 40(%esp) // 將修改後的trap-time esp儲存回棧上
// Restore the trap-time registers. After you do this, you
// can no longer modify any general-purpose registers.
// LAB 4: Your code here.
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.
addl $4, %esp // 跳過 eip
popfl // 恢復 eflags
// Switch back to the adjusted trap-time stack.
// LAB 4: Your code here.
popl %esp // 切換會 trap-time棧
// Return to re-execute the instruction that faulted.
// LAB 4: Your code here.
ret // 回到 trap-time的指令
Exercise 11
練習 11. 完成 lib/pf
中的 set_pgfault_handler()
。
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");
// 為當前環境分配異常棧
if(sys_page_alloc(0, (void *)(UXSTACKTOP - PGSIZE), PTE_U|PTE_W | PTE_P)<0)
panic("set_pgfault_handler failed.");
sys_env_set_pgfault_upcall(0, _pgfault_upcall);
}
// Save handler pointer for assembly to call.
_pgfault_handler = handler;
}
小總結:頁面故障的流程
頁面錯誤時的控制流:
- 使用者程序首先呼叫 set_pgfault_handler,設定自定義的頁面故障處理過程。(綠色、黃色箭頭)
- 使用者程序正常執行,直至觸發頁面故障(紅色箭頭)
- 核心處理中斷,將控制權歸還給使用者自定義頁面故障處理(藍色箭頭)
有一點就是,set_pgfault_handler 這個函式,只會將 _pgfault_upcall
這個過程註冊到 env 結構體中。
使用者自定義的頁面故障處理被儲存在 _pgfault_handler
,由 _pgfault_upcall
呼叫。
也就是說_pgfault_upcall
相當於是個頁面故障處理模版,幫助使用者程序處理使用者異常棧的恢復過程。
發生頁錯誤後,_page_upcall
負責呼叫 _pgfault_handler
,並恢復上下文
測試
debug的時候發現,lib/pgfault.c:set_pgfault_handler 怎麼裝不上handler。
導致過不了很多測試。來回查了半天,發現 sys_env_set_pgfault_upcall 在做exercise 8的時候忘了給註冊到 syscall 裡去了。
user/faultread
這個程式沒有註冊handler,那就會在 page_fault_handler 檢查handler合規時失敗,列印trapframe後銷燬環境。
user/faultdie
這個使用者程式的handler列印了引發頁錯誤的地址和錯誤號。
user/faultalloc
#include <inc/lib.h>
void
handler(struct UTrapframe *utf)
{
int r;
void *addr = (void*)utf->utf_fault_va;
cprintf("fault %x\n", addr);
if ((r = sys_page_alloc(0, ROUNDDOWN(addr, PGSIZE),
PTE_P|PTE_U|PTE_W)) < 0)
panic("allocating at %x in page fault handler: %e", addr, r);
snprintf((char*) addr, 100, "this string was faulted in at %x", addr);
}
void
umain(int argc, char **argv)
{
set_pgfault_handler(handler);
cprintf("%s\n", (char*)0xDeadBeef);
cprintf("%s\n", (char*)0xCafeBffe);
}
faultalloc 嘗試訪問兩個地址,然後handler中透過 sys_page_alloc 申請這兩個地址再訪問。
deadbeef這個地址在發生頁錯誤後,透過 handler 申請記憶體頁後成功訪問了。
但是 cafebffe 在發生一次頁錯誤後,似乎又發生了一次頁錯誤,因為 cafebffe 在頁中正好處於 倒數第二個位元組(0xffe),handler又將一長串字元儲存到了cafebffe,所以引發了第二次頁錯誤。
第二次handler在申請玩cafec000的記憶體頁後,將一長串字串儲存到了cafec000,然後控制流回到第一次handler處理錯誤,繼續將字元儲存到cafebffe的位置,然後將控制流返回到umain的最後一句話,將第一次handler的字串打出來。
handler第二次儲存的字串應該是被第一次儲存的字串覆蓋,沒覆蓋的地方被尾巴'\0'切斷了。
user/faultallocbad
// test user-level fault handler -- alloc pages to fix faults
// doesn't work because we sys_cputs instead of cprintf (exercise: why?)
#include <inc/lib.h>
void
handler(struct UTrapframe *utf)
{
int r;
void *addr = (void*)utf->utf_fault_va;
cprintf("fault %x\n", addr);
if ((r = sys_page_alloc(0, ROUNDDOWN(addr, PGSIZE),
PTE_P|PTE_U|PTE_W)) < 0)
panic("allocating at %x in page fault handler: %e", addr, r);
snprintf((char*) addr, 100, "this string was faulted in at %x", addr);
}
void
umain(int argc, char **argv)
{
set_pgfault_handler(handler);
sys_cputs((char*)0xDEADBEEF, 4);
}
結果是沒有出發 handler ,反而是 user_mem_assert 輸出了。
faultallocbad 和 faultalloc 的 handler 是一樣的,區別在於使用 sys_cputs 列印。sys_cputs 第一件工作就是用 user_mem_assert 確認 0xDEADBEEF 是否使用,還沒有機會觸發頁錯誤。
by the way :user_mem_assert的檢查方式是查頁表,看PTE是否合規。這個過程是不會發生頁錯誤的。
實現寫時複製的fork
現在,我們終於完全擁有了完全在使用者空間實現寫時複製 fork() 的核心設施。
在 lib/fork.c
中 fork()
已經提供了一個骨架。與 dumbfork()
一樣,fork()
也會建立一個新環境,然後掃描父環境的整個地址空間,並在子環境中設定相應的頁面對映。
不同之處在於,dumbfork()
將每個頁面逐個位元組的複製。而 fork()
最初只會複製頁面對映。
只有當其中一個環境試圖寫入頁面時,fork()
才會複製每個頁面。
fork的基本框架如下:
- fork函式:負責複製自身,並呼叫duppage複製頁對映,設定頁面故障handler
- duppage:負責複製頁對映的具體工作
- pgfault:頁故障handler,當發生對寫時複製頁進行寫操作時,將頁面進行實際複製。
fork的具體流程:
- 父級程式會使用上面實現的 set_pgfault_handler() 函式安裝 pgfault() 作為使用者級頁面故障處理程式。
- 父環境呼叫 sys_exofork(),建立子環境。
- 父環境將[0~UTOP]的地址空間中所有“可寫PTE_W”、“寫時複製PTE_COW”頁面的對映,透過 duppage 複製到子環境中,然後將寫時複製頁面重新對映到自己的地址空間(為何?不太清楚)。
- 對於 [UXSTACKTOP-PGSIZE, UXSTACKTOP] 的部分則是申請新的頁面。
- 對於只讀頁面直接保持原許可權複製即可。
發生頁錯誤時,就會觸發 pgfault() 然後將PTE_COW的頁面用新頁替換。
練習12 完成fork, duppage, pgfault
練習 12. 在 `lib/fork.c` 中實現 `fork`、`duppage` 和 `pgfault`。
用 `forktree` 程式測試你的程式碼。它應該會產生以下資訊,其中夾雜著 "new env"、"free env "和 "exiting gracefully "資訊。這些資訊可能不會按此順序出現,環境 ID 也可能不同。
1000: I am ''
1001: I am '0'
2000: I am '00'
2001: I am '000'
1002: I am '1'
3000: I am '11'
3001: I am '10'
4000: I am '100'
1003: I am '01'
5000: I am '010'
4001: I am '011'
2002: I am '110'
1004: I am '001'
1005: I am '111'
1006: I am '101'
fork
注意理解 uvpt 和 uvpd
uvpt 就是 UVPT,是虛擬地址,範圍是PTSIZE,4mb,使用PGNUM宏搜尋,取出pte的虛擬地址。
uvpd 是 pgdir 所在的虛擬地址,範圍是一個記憶體頁,4kb,使用PDE宏搜尋,取出pde的虛擬地址。。
然後就是關於 PFTEMP,前文中有測試過使用者的dupapge,我們需要這個區域實現程序間的頁面複製。
// implement fork from user space
#include <inc/string.h>
#include <inc/lib.h>
// PTE_COW 標記寫時複製頁表項。
// 它是明確分配給使用者程序的位之一(PTE_AVAIL)。
#define PTE_COW 0x800
//
// 自定義頁面故障處理程式 - 如果故障頁面是寫時複製、
// 對映到我們自己的私有可寫副本中。
//
static void
pgfault(struct UTrapframe *utf)
{
void *addr = (void *) utf->utf_fault_va;
uint32_t err = utf->utf_err;
int r;
// 檢查故障訪問是否 (1) 可寫;(2) 是寫時複製頁。 如果不是,則 panic。
// 提示:
// 在 uvpt 中使用只讀頁表對映(參見 <inc/memlayout.h>)。
// LAB 4: Your code here.
//只在對“寫時複製頁面”進行“寫操作”才處理
if(!(err & FEC_WR))
{
panic("trapno is not FEC_WR.");
}
if(!(uvpt[PGNUM(addr)] & PTE_COW))
{
panic("fault addr is not COW");
}
// 分配一個新頁面,將其對映到臨時位置 (PFTEMP),
// 將舊頁面的資料複製到新頁面,然後將新頁面移動到舊頁面的地址。
// 提示:
// 你應該呼叫三次系統呼叫。
// LAB 4: Your code here.
// panic("pgfault not implemented");
addr = ROUNDDOWN(addr, PGSIZE);
//將當前程序PFTEMP也對映到當前程序addr指向的物理頁
if ((r = sys_page_map(0, addr, 0, PFTEMP, PTE_U|PTE_P)) < 0)
panic("sys_page_map: %e", r);
//令當前程序addr指向新分配的物理頁
if ((r = sys_page_alloc(0, addr, PTE_P|PTE_U|PTE_W)) < 0)
panic("sys_page_alloc: %e", r);
//將PFTEMP指向的物理頁複製到addr指向的物理頁
memmove(addr, PFTEMP, PGSIZE);
//解除當前程序PFTEMP對映
if ((r = sys_page_unmap(0, PFTEMP)) < 0)
panic("sys_page_unmap: %e", r);
}
//
// 將我們的虛擬頁面 pn(地址 pn*PGSIZE)對映到相同虛擬地址的目標 envid 中。
// 如果頁面是可寫或寫時複製的,則必須建立寫時複製的新對映,
// 然後我們的對映也必須標記為寫時複製。
// (練習: 如果我們的對映在本函式開始時已經是寫時複製,為什麼還需要再次標記寫時複製?)
//
// Returns: 0 on success, < 0 on error.
// It is also OK to panic on error.
//
static int
duppage(envid_t envid, unsigned pn)
{
int r;
// LAB 4: Your code here.
// panic("duppage not implemented");
void *addr = (void *)(pn * PGSIZE);
if(uvpt[pn] & PTE_SHARE)
{
sys_page_map(0, addr, envid, addr, PTE_SYSCALL);
}
else if ((uvpt[pn]&PTE_W)|| (uvpt[pn] & PTE_COW))
{
if ((r = sys_page_map(0, addr, envid, addr, PTE_COW|PTE_U|PTE_P)) < 0)
panic("sys_page_map:%e", r);
if ((r = sys_page_map(0, addr, 0, addr, PTE_COW|PTE_U|PTE_P)) < 0)
panic("sys_page_map:%e", r);
}
else
{
sys_page_map(0, addr, envid, addr, PTE_U|PTE_P); //對於只讀的頁,只需要複製對映關係即可
}
return 0;
}
//
// 使用寫時複製的使用者級 fork。
// 適當設定頁面故障處理程式。
// 建立一個子程序。
// 將我們的地址空間和頁面故障處理程式設定複製到子執行程式中。
// 然後將子程序標記為可執行並返回。
//
// 返回:子代的 envid 返回給父代,0 返回給子代,< 0 表示出錯。
// 出錯時也可以 panic。
//
// 提示
// 使用 uvpd、uvpt 和 duppage。
// 記住在子程序中固定 “thisenv”。
// 使用者異常堆疊都不應該標記為寫時複製、
// 因此必須為子程序的使用者異常堆疊分配一個新頁面。
//
envid_t
fork(void)
{
// LAB 4: Your code here.
// panic("fork not implemented");
extern void _pgfault_upcall(void);
set_pgfault_handler(pgfault);
envid_t eid = sys_exofork(); // 建立子程序
if(eid < 0){
panic("sys_exofork Failed, envid: %e", eid);
}
if(eid == 0){ // 子程序進入該分支
thisenv = &envs[ENVX(sys_getenvid())];
return 0;
}
for(uint32_t addr = 0; addr < USTACKTOP; addr += PGSIZE){
if((uvpd[PDX(addr)] & PTE_P) &&
(uvpt[PGNUM(addr)]&PTE_P) &&
(uvpt[PGNUM(addr)] &PTE_U)){
duppage(eid, PGNUM(addr));
}
}
//為子環境的異常棧申請記憶體頁
int r = sys_page_alloc(eid, (void *)(UXSTACKTOP-PGSIZE), PTE_P|PTE_W|PTE_U);
if( r < 0)
panic("sys_page_alloc: %e", r);
//為子環境設定pgfault_upcall
r= sys_env_set_pgfault_upcall(eid, _pgfault_upcall);
if( r < 0 )
panic("sys_env_set_pgfault_upcall: %e",r);
//設定子環境的執行狀態
r = sys_env_set_status(eid, ENV_RUNNABLE);
if (r < 0)
panic("sys_env_set_status: %e", r);
return eid;
}
// Challenge!
int
sfork(void)
{
panic("sfork not implemented");
return -E_INVAL;
}
Part B 結束