北航作業系統課程lab4實驗報告
實驗思考題
Thinking 4.1
思考並回答下面的問題:
-
核心在儲存現場的時候是如何避免破壞通用暫存器的?
-
系統陷入核心呼叫後可以直接從當時的$a0-$a3引數暫存器中得到使用者呼叫msyscall留下的資訊嗎?
-
我們是怎麼做到讓sys開頭的函式“認為”我們提供了和使用者呼叫msyscall時同樣的引數的?
-
核心處理系統呼叫的過程對Trapframe做了哪些更改?這種修改對應的使用者態的變化是?
-
呼叫syscall.S中的SAVE_ALL
-
能,在陷入核心時,一般不會有操作改變這四個暫存器,但一般情況下,還是從棧中取得這些引數更加保險。
-
在呼叫syscall前都將前四個引數按順序放入$a0-$a3暫存器,後兩個引數按順序存入核心棧中的相同位置(相對sp偏移相同),核心處理這些引數時也按照這個順序讀取。
-
將棧中儲存的EPC暫存器值增加4,這是因為系統呼叫後,將會返回下一條指令,而使用者程式會保證系統呼叫操作不在延遲槽內,所以直接加4得到下一條指令的地址。
Thinking 4.2
思考下面的問題,並對這個問題談談你的理解: 請回顧 lib/env.c 檔案中 mkenvid() 函式的實現,該函式不會返回 0,請結合系統呼叫和 IPC 部分的實現 與 envid2env() 函式的行為進行解釋。
我們可以看到該函式為:
u_int mkenvid(struct Env *e) {
u_int idx = e - envs;
u_int asid = asid_alloc();
return (asid << (1 + LOG2NENV)) | (1 << LOG2NENV) | idx;
}
顯然,無論如何第11位的值都為1,函式不會返回0,並且由此可得,根程序的程序號為0x400,這也與實驗結果相符合。
Thinking 4.3
思考下面的問題,並對這兩個問題談談你的理解:
-
子程序完全按照 fork() 之後父程序的程式碼執行,說明了什麼?
-
但是子程序卻沒有執行 fork() 之前父程序的程式碼,又說明了什麼?
-
子程序與父程序共享程式碼段空間。
-
子程序建立時,PC值設定到了fork的後一個指令,因此不會執行之前的程式碼,但由於空間的共享,之前的變數都可以使用。
Thinking 4.4
關於 fork 函式的兩個返回值,下面說法正確的是:
A、fork 在父程序中被呼叫兩次,產生兩個返回值
B、fork 在兩個程序中分別被呼叫一次,產生兩個不同的返回值
C、fork 只在父程序中被呼叫了一次,在兩個程序中各產生一個返回值
D、fork 只在子程序中被呼叫了一次,在兩個程序中各產生一個返回值
說法正確的是C
Thinking 4.5
我們並不應該對所有的使用者空間頁都使用duppage進行對映。那麼究竟哪些使用者空間頁應該對映,哪些不應該呢? 請結合本章的後續描述、mm/pmap.c 中 mips_vm_init 函式進行的頁面對映以及 include/mmu.h 裡的記憶體佈局圖進行思考。
在0 ~ USTACKTOP範圍的記憶體需要進行對映,其上範圍的記憶體要麼屬於核心,要麼是所有使用者程序共享的空間,使用者模式下只可以讀取。可寫但不共享的頁面都需要設定PTE_COW進行保護。
Thinking 4.6
在遍歷地址空間存取頁表項時你需要使用到vpd和vpt這兩個“指標的指標”,請參考 user/entry.S 和 include/mmu.h 中的相關實現,思考並回答這幾個問題:
-
vpt和vpd的作用是什麼?怎樣使用它們?
-
從實現的角度談一下為什麼程序能夠通過這種方式來存取自身的頁表?
-
它們是如何體現自對映設計的?
-
程序能夠通過這種方式來修改自己的頁表項嗎?
-
vpd是頁目錄首地址,以vpd為基地址,加上頁目錄項偏移數即可指向va對應頁目錄項,即
((Pde*)(*vpd)) + (va >> 22)
;vpt是頁表首地址,以vpt為基地址,加上頁表項偏移數即可指向va對應的頁表項,即((Pte*)(*vpt)) + (va >> 12)
。*vpd的值是可以來判斷當前va是否有對應頁表項的,若存在這個頁表項,才可以訪問對應的*vpt的值。 -
在user/entry.S中定義了vpt和vpd
-
.globl vpt vpt: .word UVPT .globl vpd vpd: .word (UVPT+(UVPT>>12)*4)
定義了使用者頁表和使用者頁目錄的虛擬地址,而且還是自對映的頁表。這使得每個程序的頁表都能在UVPT中儲存,切換程序時,頁表也會切換。
-
vpd的地址為
(UVPT+(UVPT>>12)*4)
,顯然這是自對映的特點。 -
不能。該區域對使用者只讀不寫,若想要增添頁表項,需要陷入核心進行操作。
Thinking 4.7
page_fault_handler 函式中,你可能注意到了一個向異常處理棧複製Trapframe執行現場的過程,請思考並回答這幾個問題:
-
這裡實現了一個支援類似於“中斷重入”的機制,而在什麼時候會出現這種“中斷重入”?
-
核心為什麼需要將異常的現場Trapframe複製到使用者空間?
-
缺頁中斷時再次響應外部中斷,在標誌有
COW
的頁面被修改時會出現。 -
該MOS作業系統按照微核心的設計理念, 儘可能地將功能實現在使用者空間中,因此主要的處理過程是在使用者態下完成的,所以需要將其複製到使用者空間。
Thinking 4.8
到這裡我們大概知道了這是一個由使用者程式處理並由使用者程式自身來恢復執行現場的過程,請思考並回答以下幾個問題:
-
在使用者態處理頁寫入異常,相比於在核心態處理有什麼優勢?
-
從通用暫存器的用途角度討論,在可能被中斷的使用者態下進行現場的恢復,要如何做到不破壞現場中的通用暫存器?
-
核心態處理失誤產生的影響較大,可能會使得作業系統崩潰。此外,使用者狀態下不能得到一些在核心狀態才有的許可權,避免改變不必要的記憶體空間。
-
通用暫存器可以通過巨集指令全部存入棧中,現場恢復時再取出來即可。(感覺好像又不是這意思 ̄ω ̄=)
Thinking 4.9
請思考並回答以下幾個問題:
-
為什麼需要將set_pgfault_handler的呼叫放置在syscall_env_alloc之前?
-
如果放置在寫時複製保護機制完成之後會有怎樣的效果?
-
子程序是否需要對在entry.S定義的字__pgfault_handler賦值?
-
子程序不會呼叫
set_pgfault_handler
,且父子程序共享空間,pgfault_handler函式都在UXSTACKTOP上,父子程序的缺頁中斷都可以被捕捉到。 -
寫時複製保護機制完成後發生缺頁中斷不能夠被捕捉到,導致無法進入缺頁中斷異常。
-
不需要,父程序已設定好,子程序共享即可。
實驗難點展示
系統呼叫
系統呼叫的函式十分簡單,以前寫了page_alloc,現在繼續寫mem_alloc,每次都是用一個if語句賦值給一個整形值,判斷是否小於零,是的話就返回這個值,這些負數值都是有巨集定義的,可以去觸發一些異常。
我想說的是syscall.S這個函式的編寫,往年貌似就考過這個。這個函式乾的事情很明顯的揭示了系統呼叫是如何將使用者態的資料跨越到核心態的。值得一提的是在Mars裡面程式設計的系統呼叫號暫存器為$v0,然後我就複製了這個暫存器,後面是一行一行對著別人的程式碼才看出來的,在這個作業系統中,系統呼叫號就是第一個引數對應的暫存器$a0,我們需要把這個暫存器讀出來,才能知道是什麼系統呼叫,才能執行對應的系統呼叫程式(通過jal t2實現,而t2又是由$a0決定的)。系統呼叫表就在下方sys_call_table處,和lab3處理多種中斷的方式一模一樣。
lw a0, TF_REG4(sp)
• lw a1, TF_REG5(sp)
• lw a2, TF_REG6(sp)
• lw a3, TF_REG7(sp)
• addiu sp, sp, -24
• sw t3, 16(sp)
• sw t4, 20(sp)
• jalr t2 // Invoke sys_* function
• nop
(圖源PPT)
使用者態與核心態函式
這個很容易把人搞暈的,我只能說在lib裡的都是核心態函式,在user裡的都是使用者態函式,核心態函式sys_*,使用者態函式是syscall_*。我們補充的幾乎都是核心態的系統呼叫函式(lib/syscall_all.c),而使用者態的系統呼叫函式全是直接呼叫msyscall(user/syscall_lib.c)。
在做課上實驗的時候,實現一個新的系統呼叫基本上是四步走:
-
在標頭檔案中先定義好新的系統呼叫號和系統呼叫名。
-
使用者態先定義這個syscall_*函式,呼叫msyscall
-
在syscall.S中新增上新的系統呼叫名
-
在核心空間實現這個系統呼叫sys_*,這才是實現該系統呼叫功能的主要步驟。
程序間通訊
在lab 4-1-Extra中實現了對傳送程序的阻塞,這避免了CPU的輪詢。最近幾次課上都考到了類似於PV操作的知識,而在我看來PV操作與訊號量相關的一部分知識是作業系統理論課最靈活的部分了。PV操作本質是對一個整形的訊號量進行加加減減的操作,實現起來也是如此。
一個程序能夠接收資訊之前,可以有多個程序準備傳送訊息,但他們必須處於阻塞狀態,因為要接受訊息的程序還沒有準備好接收。而若是出現這種情況,接收程序可以直接接收訊息,並不再過渡到NOT_RUNNABLE狀態。
實現的大致思路是需要建一個結構體來儲存要傳送程序的資訊,在傳送程序碰壁時,設定為NOT_RUNNABLE狀態,並直接yield,要注意,這個函式會直接導致該程序當前執行的函式return,所以只有在sys_ipc_recv函式中直接接收訊息。
建立子程序
這部分是我認為lab 4中最難的。
父程序要做的事:
-
設定缺頁中斷處理函式入口地址
-
使用syscall_env_alloc建立子程序
-
使用duppage設定PTE_COW標識,並將子程序的虛擬空間和父程序的虛擬空間聯合起來,指向相同的物理頁面
-
為子程序分配異常處理棧,為子程序設定狀態
子程序要做的事:
-
設定子程序所代表的env指標
-
子程序在缺頁中斷時陷入核心,再返回到設定好的缺頁中斷處理函式中使用者態下的pgfault函式,拷貝完頁面後,返回到使用者態的中斷現場。
缺頁中斷的處理:
-
陷入核心,執行page_fault_handler(lib/trap.c),儲存現場並設定異常處理棧,這裡面設定了EPC的值
tf->cp0_epc = curenv->env_pgfault_handler;
-
這個值其實就是__asm_pgfault_handler(user/entry.S)這個函式,
lw t1, __pgfault_handler jalr t1
,它呼叫了pgfault函式,缺頁處理,拷貝頁面。 -
然後返回到__asm_pgfault_handler函式中,通過使用lw指令將棧中儲存的值放回暫存器中,儲存現場,返回到中斷現場。
體會與感想
lab 4在思維要求上比前幾個實驗更難了,最主要做的就是兩個部分,核心函式和使用者函式的相結合、缺頁中斷的設定與處理。
一方面,腦子裡一定要清楚現在是在改核心還是改使用者,因為之前寫的都是核心,而核心函式是不能在使用者空間呼叫的。以前寫過的page_insert呀,envid2env呀都不能用在使用者空間了,只能用user_bcopy和user_bzero來代替bcopy和bzero了。現如今看來,從使用者空間到核心空間可以是系統呼叫或者中斷異常的形式,系統呼叫將環境上下文儲存在KERNEL_SP中,中斷異常將環境上下文儲存在TIMESTACK中。
另一方面,缺頁中斷是父子程序間實現的一個重要的功能,但我印象最深的是父子程序呼叫fork產生不同的返回值的實現,這是在int sys_env_alloc(void)(lib/syscall_all.c)中實現的。將父程序的epc設定為子程序的pc值,這讓子程序第一次執行時就能夠接著呼叫fork的後一條指令。父程序直接返回子程序程序號,子程序則將0這個返回值儲存在PCB裡,輪到子程序執行時直接把$v0暫存器設定為0。
寫的程式碼越多,bug也就越多,我課下遇到的一個bug是在fork時會建立兩個程序,加上父程序一共三個,試了很久才發現是env_run這個函式的鍋,十分的難受。
遺留難點
關於envid2env這個函式裡的checkperm引數,這個用於檢查是否是當前程序或當前程序子程序的引數顯得很莫名其妙,我大致看了一下,只有在刪除一個程序中會將這個引數置1 ,或許是為了確保一個程序只能由自己或自己的父程序結束。其他地方應該都是設定為0的。而經過實驗發現,如果在補全程式碼的時候沒有設定為0,很可能還過不了課下。