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*類型的數組了嗎?


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM