1. 程式人生 > 實用技巧 >lab4——系統呼叫與fork

lab4——系統呼叫與fork

思考題

Thinking 4.1 思考並回答下面的問題:

  • 核心在儲存現場的時候是如何避免破壞通用暫存器的?

通過SAVE_ALL將所有通用暫存器的值存入sp中

  • 系統陷入核心呼叫後可以直接從當時的$a0-$a3 引數暫存器中得到使用者呼叫msyscall 留下的資訊嗎?

可以

  • 我們是怎麼做到讓sys 開頭的函式“認為”我們提供了和使用者呼叫msyscall 時同樣的引數的?

前四個引數放在對應的暫存器上,後兩個引數存在棧上的相同位置

  • 核心處理系統呼叫的過程對Trapframe 做了哪些更改?這種修改對應的使用者態的變化是?

修改了EPC的值,使用者態返回的時候可以繼續執行下一條指令

Thinking 4.2 思考下面的問題,並對這兩個問題談談你的理解:

子程序完全按照fork() 之後父程序的程式碼執行,說明了什麼?
但是子程序卻沒有執行fork() 之前父程序的程式碼,又說明了什麼?

說明子程序的程式碼段和父程序是一樣的

說明子程序恢復到的上下文位置是fork函式

Thinking 4.3 關於fork 函式的兩個返回值,下面說法正確的是: C

A. fork 在父程序中被呼叫兩次,產生兩個返回值
B. fork 在兩個程序中分別被呼叫一次,產生兩個不同的返回值
C. fork 只在父程序中被呼叫了一次,在兩個程序中各產生一個返回值
D. fork 只在子程序中被呼叫了一次,在兩個程序中各產生一個返回值

當然只完成子程序部分,子程序還不能正常跑起來,父親在兒子醒來之前則需要做更多的準備,而這些準備中最重要的一步是遍歷程序的大部分使用者空間頁,對於所有可以寫入的頁面的頁表項,在父程序和子程序都加以PTE_COW 標誌位保護起來。這裡需要實現duppage 函式來完成這個過程。

Thinking 4.4 如果仔細閱讀上述這一段話, 你應該可以發現, 我們並不是對所有的使用者空間頁都使用duppage 進行了保護。那麼究竟哪些使用者空間頁可以保護,哪些不可以呢,請結合include/mmu.h 裡的記憶體佈局圖談談你的看法。

從0到USERSTACKTOP的地址空間裡,對於不是共享的和只讀的頁面可以保護起來。

Thinking 4.5 在遍歷地址空間存取頁表項時你需要使用到vpd 和vpt 這兩個“指標的指標”,請思考並回答這幾個問題:

  • vpt 和vpd 的作用是什麼?怎樣使用它們?
  • 從實現的角度談一下為什麼能夠通過這種方式來存取程序自身頁表?
  • 它們是如何體現自對映設計的?
  • 程序能夠通過這種存取的方式來修改自己的頁表項嗎?

vpt和vpd分別指向頁表項和頁目錄所在的虛擬地址,可以通過取他們的內容來獲得相應的指標

因為vpt和vpd通過巨集定義對應了記憶體中頁表所在的虛擬地址

vpd:(UVPT+(UVPT>>12)*4)

可以

Thinking 4.6 page_fault_handler 函式中,你可能注意到了一個向異常處理棧複製Trapframe 執行現場的過程,請思考並回答這幾個問題:

  • 這裡實現了一個支援類似於“中斷重入”的機制,而在什麼時候會出現這種“中斷重入”?
  • 核心為什麼需要將異常的現場Trapframe 複製到使用者空間?

當有COW的頁面被修改的時候

因為需要在使用者態處理異常

Thinking 4.7 到這裡我們大概知道了這是一個由使用者程式處理並由使用者程式自身來恢復執行現場的過程,請思考並回答以下幾個問題:

  • 使用者處理相比於在核心處理寫時複製的缺頁中斷有什麼優勢?
  • 從通用暫存器的用途角度討論使用者空間下進行現場的恢復是如何做到不破壞通用暫存器的?

體現了微核心的思想,讓使用者程序實現核心的功能,就算核心出了問題作業系統也能執行

通過把所有通用暫存器壓入棧中,在使用時取出

難點

Exercise 4.1 填寫user/syscall_wrap.S 中的msyscall 函式,使得使用者部分的系統呼叫機制可以正常工作。

通過特權指令syscall陷入核心態,之後jr返回即可(注意延遲槽)

Exercise 4.2 按照lib/syscall.S 中的提示,完成handle_sys 函式,使得核心部分的系統呼叫機制可以正常工作。

第一步:從TF中取出EPC的值,加4之後放回TF中,使得異常處理結束後可以繼續執行下一條指令

第二步:從TF中讀取a0的值(系統呼叫號)

第三步:在核心棧上分配儲存六個引數的空間,並且把六個引數放到正確的位置

注意在mips中按照規定,前四個引數並不放在棧上,而是直接留在暫存器中

第四步:恢復核心棧的位置

Exercise 4.3 實現lib/syscall_all.c 中的int sys_mem_alloc(int sysno,u_int envid,u_int va, u_int perm) 函式

先判斷錯誤情況:va超出了使用者空間(涉及了核心空間),試圖用COW作為perm,或者perm中沒有V,此時返回-E_INVAL

然後通過envid2env得到對應的程序,用page_alloc和page_insert分配和插入頁面

注意每一步都需要檢測是否出現返回值不正常的情況,如果有要繼續傳遞出去

Exercise 4.4 實現lib/syscall_all.c 中的int sys_mem_map(int sysno,u_int srcid,u_int srcva, u_int dstid, u_dstva, u_int perm) 函式

先判斷錯誤情況:srcva和dstva是否超出了使用者空間

通過envid2env得到對應程序,page__lookup查詢src中的頁面,page_insert插入到dst中

Exercise 4.5 實現lib/syscall_all.c 中的int sys_mem_unmap(int sysno,u_int envid,u_int va) 函式

先判斷錯誤情況:va是否超出了使用者空間

通過envid2env得到對應程序,page_remove解除對映關係

Exercise 4.6 實現lib/syscall_all.c 中的void sys_yield(void) 函式

該函式用於當前程序主動放棄cpu,因為lab3中編寫的env_run中切換程序是從TIMESTACK儲存的通用暫存器等資訊,所以在呼叫排程演算法切換程序之前,需要從核心棧KERNEL_SP中把Trapframe拷貝到TIMESTACK的相應位置。

Exercise 4.7 實現lib/syscall_all.c 中的void sys_ipc_recv(int sysno,u_int dstva)函式和int sys_ipc_can_send(int sysno,u_int envid, u_int value, u_int srcva,u_int perm) 函式。

sys_ipc_recv負責將當前程序設定成準備好接受ipc的狀態,寫入接受的va地址,並且改變env狀態使其暫停執行,然後呼叫sys_yield主動放棄cpu

sys_ipc_can_sen首先檢測目標env是否處於準備好接受ipc資訊的狀態,如果不是返回對應錯誤

只有srcva不是0的情況下才呼叫page__lookup和page_insert傳遞資訊

最後把接受資訊的程序env資訊修改即可 perm要賦值!!!

Exercise 4.8 填寫lib/syscall_all.c 中的sys_env_alloc 函式

這個函式用於建立子程序。需要把父程序中的資訊(Trapframe pri)傳遞給子程序。

因為子程序還不能執行,狀態要設定為不能執行;同時為了結束異常後能夠返回到正確的上下文位置,需要把tf中pc更新為epc的值。

注意tf中reg2(代表返回值的暫存器)要設定為0,用於實現父子程序呼叫sys_env_alloc返回不同的值,以便fork進行區分。

Exercise 4.9 填寫user/fork.c 中的fork 函式中關於sys_env_alloc 的部分和“子程序”執行的部分

通過sys_env_alloc得到兩種返回值:父程序返回子程序的id,子程序返回0

當返回值為0時,子程序通過syscall_getenvid()得到自己的id,並且將env設定為對應的程序指標

Exercise 4.10 結合註釋,填寫user/fork.c 中的duppage 函式

這一函式主要實現了子程序頁表的“複製”。說是複製,但是實際上只是讓父子程序共享相同的物理頁面而已。

這裡的難點一個在於怎麼獲取頁表的perm資訊,一個在於怎麼根據perm資訊對子程序的perm進行賦值。

perm = ((Pte*)(*vpt))[pn] &0xfff其實就是取得頁表vpt中第pn項,然後看它低位的值

子程序賦值大概分為以下幾種情況:

無效——報錯

只讀——直接給相同perm

共享——給相同的perm

COW——給相同的perm

可寫——父子程序都追加COW

修改perm通過syscall_mem_map即可

Exercise 4.11 完成lib/traps.c 中的page_fault_handler 函式,設定好異常處理棧以及epc暫存器的值。

把epc設定為對應程序page_fault_handler 的指標即可,這樣從異常返回後就會進入異常處理函式

Exercise 4.12 完成lib/syscall_all.c 中的sys_set_pgfault_handler 函式

把對應env的page_fault_handler 和xstack設定成對應值即可

Exercise 4.13 填寫user/fork.c 中的pgfault 函式

這個函式用於處理COW處罰的中斷

va對頁大小取整,在USTACKTOP上面invalid的區域分配一塊臨時區域,用於拷貝va處的資訊

之後把這段資訊插入到子程序的對應va上

最後取消tmp在頁表中的對映關係

Exercise 4.14 填寫lib/syscall_all.c 中的sys_set_env_status 函式

判斷status是否合法,否則報錯

設定env為對應的status,根據情況插入或者移出就緒佇列

Exercise 4.15 填寫user/fork.c 中的fork 函式中關於“父程序”執行的部分

難點中的難點

首先需要通過set_pgfault_handler設定異常處理函式,然後syscall_env_alloc建立子程序

之後根據返回值的不同分別執行父子程序的操作

父程序要在迴圈中遍歷使用者空間頁表中的每一項,對於頁目錄和頁表都有效的,複製給子程序

然後分配空間給異常處理棧,設定__sam_pgfault_handler,最後改變子程序狀態讓它執行起來

感悟

這次實驗上來就把我難住了。對於在使用者態和核心態之間的切換,還有各種暫存器、堆疊、資料的設定都弄得不是很清楚。導致很長一段時間裡程式連正常執行都做不到,debug也無從下手。琢磨了很長時間才把前兩個練習填對。

系統呼叫和ipc部分倒是還算比較快的做完了,接下來的fork又是一大難點。

現在回過頭再去看填的這些函式,我感覺當時的理解其實也沒有錯。問題就在於,理解該怎麼做,和知道具體怎麼實現是兩回事。我知道需要獲取頁表的資訊來判斷是不是需要新增COW,需不需要通過duppage進行復制,但是我並不太清楚從哪得到頁表的資訊。

通過grep檢視定義知道了vpt和vpd的作用,但是實際使用的過程中怎麼做都不對。最後請教同學才發現,取完vpt和vpd之後加上對應指標的強制型別轉換才行。但是定義的時候vpt已經是Pte*型別的了,我以為不需要轉換也應該是對的,最後也不知道為什麼。

這次實驗卡在最後的時間才做完實在驚險,六天前就寫完了所有內容,沒想到debug居然就用了整整5天,幾乎都沒做其他事情。下一個實驗一定要早點開始做,避免重蹈覆轍。

殘留難點

((Pte*)(*vpt))[n](*vpt)[n]為什麼會結果不同?vpt不是已經定義了是Pte*型別的陣列了嗎?