mit6.828筆記 - lab4 Part B:寫時複製Fork

来源:https://www.cnblogs.com/toso/p/18202341
-Advertisement-
Play Games

Part B Copy-on-Write Fork Unix 提供 fork() 系統調用作為主要的進程創建基元。fork()系統調用複製調用進程(父進程)的地址空間,創建一個新進程(子進程)。 不過,在調用 fork() 之後,子進程往往會立即調用 exec(),用新程式替換子進程的記憶體。例如,s ...


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 實現用戶級頁面故障的思路是:

  1. 增加一個系統調用,sys_env_set_pgfault_upcall ,允許用戶進程指定自己的頁面故障程式
  2. 在 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 的分發里:

image.png


調用用戶頁面故障處理程式

現在我們終於要完善頁面故障處理程式 —— 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 是否已經位於用戶異常棧之中。
最後,再調用用戶的處理程式。

所以說,我們要做的事情:

  1. 判斷curenv->env_pgfault_upcall 是否設置
  2. 修改esp,將其切換到異常棧
    1. 對於首次缺頁,是直接切換
    2. 對於遞歸缺頁,是在當前tf->tf_esp的下方。
  3. 再異常棧上壓入一個UTrapframe
  4. 將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 的值。

因此,手冊給出的答題思路是:

  1. 從用戶異常棧上讀取 trap-time 的 sp
  2. 將 trap-time 的 eip 推送到 trap-time 的stack (即保存到 trap-time 的 sp 所指位置)
  3. 從 用戶異常棧上的utrapframe,恢復寄存器狀態(跳過 eip)
  4. 恢復 esp (切換回 trap-time 的sp),由於第二步的操作,此時esp所指位置是 trap-time的eip
  5. 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;
}

小總結:頁面故障的流程

頁面錯誤時的控制流:

  1. 用戶進程首先調用 set_pgfault_handler,設置自定義的頁面故障處理過程。(綠色、黃色箭頭)
  2. 用戶進程正常執行,直至觸發頁面故障(紅色箭頭)
  3. 內核處理中斷,將控制權歸還給用戶自定義頁面故障處理(藍色箭頭)

image.png

有一點就是,set_pgfault_handler 這個函數,只會將 _pgfault_upcall 這個過程註冊到 env 結構體中。
用戶自定義的頁面故障處理被保存在 _pgfault_handler,由 _pgfault_upcall調用。
也就是說_pgfault_upcall 相當於是個頁面故障處理模版,幫助用戶進程處理用戶異常棧的恢復過程。

發生頁錯誤後,_page_upcall 負責調用 _pgfault_handler,並恢覆上下文

image.png

測試

debug的時候發現,lib/pgfault.c:set_pgfault_handler 怎麼裝不上handler。
導致過不了很多測試。來回查了半天,發現 sys_env_set_pgfault_upcall 在做exercise 8的時候忘了給註冊到 syscall 里去了。

user/faultread


這個程式沒有註冊handler,那就會在 page_fault_handler 檢查handler合規時失敗,列印trapframe後銷毀環境。
image.png
image.png

user/faultdie

image.png
這個用戶程式的handler列印了引發頁錯誤的地址和錯誤號。

image.png

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 申請這兩個地址再訪問。

image.png

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 輸出了。
image.png

faultallocbad 和 faultalloc 的 handler 是一樣的,區別在於使用 sys_cputs 列印。sys_cputs 第一件工作就是用 user_mem_assert 確認 0xDEADBEEF 是否使用,還沒有機會觸發頁錯誤。

image.png

by the way :user_mem_assert的檢查方式是查頁表,看PTE是否合規。這個過程是不會發生頁錯誤的。


實現寫時複製的fork

現在,我們終於完全擁有了完全在用戶空間實現寫時複製 fork() 的內核設施。
lib/fork.cfork() 已經提供了一個骨架。與 dumbfork() 一樣,fork() 也會創建一個新環境,然後掃描父環境的整個地址空間,併在子環境中設置相應的頁面映射。
不同之處在於,dumbfork() 將每個頁面逐個位元組的複製。而 fork() 最初只會複製頁面映射。
只有當其中一個環境試圖寫入頁面時,fork() 才會複製每個頁面。

fork的基本框架如下:

  1. fork函數:負責複製自身,並調用duppage複製頁映射,設置頁面故障handler
  2. duppage:負責複製頁映射的具體工作
  3. pgfault:頁故障handler,當發生對寫時複製頁進行寫操作時,將頁面進行實際複製。

fork的具體流程:

  1. 父級程式會使用上面實現的 set_pgfault_handler() 函數安裝 pgfault() 作為用戶級頁面故障處理程式。
  2. 父環境調用 sys_exofork(),創建子環境。
  3. 父環境將[0~UTOP]的地址空間中所有“可寫PTE_W”、“寫時複製PTE_COW”頁面的映射,通過 duppage 複製到子環境中,然後將寫時複製頁面重新映射到自己的地址空間(為何?不太清楚)。
  4. 對於 [UXSTACKTOP-PGSIZE, UXSTACKTOP] 的部分則是申請新的頁面。
  5. 對於只讀頁面直接保持原許可權複製即可。

發生頁錯誤時,就會觸發 pgfault() 然後將PTE_COW的頁面用新頁替換。
image.png

練習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 結束



您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 對某個遠程伺服器啟用和設置NTP服務(Windows系統) 打開註冊表 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\W32Time\TimeProviders\NtpServer 將 Enabled 的值設置為 1,這將啟用NTP伺服器功 ...
  • 我在隨筆《EAV模型(實體-屬性-值)的設計和低代碼的處理方案(1)》中介紹了一些基本的EAV模型設計知識和基於Winform場景下低代碼(或者說無代碼)的一些實現思路,在本篇隨筆中,我們來分析一下這種針對通用業務,且只需定義就能構建業務模塊存儲和界面的解決方案,其中的數據查詢處理的操作。 ...
  • C#TMS系統代碼-聯表報表學習 領導被裁了之後很快就有人上任了,幾乎是無縫銜接,很難讓我不想到這早就決定好了。我的職責沒有任何變化。感受下來這個系統封裝程度很高,我只要會調用方法就行。這個系統交付之後不會有太多問題,更多應該是做小需求,有大的開發任務應該也是第二期的事,嗯?怎麼感覺我變成運維了?而 ...
  • 目錄前言PostgreSql安裝測試額外Nuget安裝Person.cs模擬運行Navicate連postgresql解決方案Garnet為什麼要選擇Garnet而不是RedisRedis不再開源Windows版的Redis是由微軟維護的Windows Redis版本老舊,後續可能不再更新Garne ...
  • .Net8.0 Blazor Hybird 桌面端 (WPF/Winform) 實測可以完整運行在 win7sp1/win10/win11. 如果用其他工具打包,還可以運行在mac/linux下, 傳送門BlazorHybrid 發佈為無依賴包方式 安裝 WebView2Runtime 1.57 M ...
  • 大家好,我是痞子衡,是正經搞技術的痞子。今天痞子衡給大家分享的是JLink 7.62優化了手動增加新MCU型號支持方法。 JLink 工具可以說是搞單片機開發的必備神器,JLink 包括一個硬體模擬器(分不同用途的 EDU/BASE/PLUS/WIFI/ULTRA+/PRO)和 PC 機上的驅動軟體 ...
  • Part C:搶占式多任務和進程間通信(IPC lab4到目前為止,我們能夠啟動多個CPU,讓多個CPU同時處理多個進程。實現了中斷處理,並且實現了用戶級頁面故障機制以及寫時複製fork。 但是,我們的進程調度不是搶占式的,現在每個進程只有在發生中斷的時候,才會被調度(調用shed_yeild),這 ...
  • 哈嘍大家好,我是鹹魚。 最近寫的一個 Python 項目用到了 jwcrypto 這個庫,這個庫是專門用來處理 JWT 的,JWT 全稱是 JSON Web Token ,JSON 格式的 Token。 今天就來簡單入門一下 JWT。 官方介紹:https://jwt.io/introduction ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...