操做系統的實驗課實在使人頭禿。咱們須要在兩週時間內學習相關知識、讀懂指導書、讀懂代碼、補全代碼、處理玄學bug和祖傳bug,以及回答使人窒息的思考題。能夠說,這門課的要求很是高,就我的感受,遠比計算機組成實驗課要難受。算法
一方面,想要達到細緻理解操做系統每一個實現細節,很是困難,須要大量時間和經歷的投入;但另外一方面,若是咱們可以理解了操做系統實現的每一個細節,咱們的水平也會有大幅度的提高。在這裏,我記錄下本次實驗課下個人學習經歷,若是有不對的地方,但願可以指出,以求共同進步。函數
在前面三個Lab的實驗中,咱們成功的搭建起了操做系統的內核,創建了內存管理機制和進程調度機制。通常來講,進程是給用戶使用的,而用戶沒法直接對系統內核進行存取。另外一方面,進程與進程之間的虛擬地址互相獨立,這使得兩個進程之間的互相通訊變得困難。可是,用戶會在有些狀況下須要使用只有內核才能進行的操做。爲了解決這個問題,操做系統設計了系統調用。學習
指導書上已有的知識,我在此再也不贅述。在進行實驗以前,咱們須要稍微補習一點知識,主要是關於彙編函數方面的東西。這些知識,指導書或者其餘地方都有,只不過比較零碎。我稍微彙集了一下這些知識,若是想要了解的更詳細,能夠深刻了解。操作系統
爲了方便的像C語言同樣構造函數,咱們的操做系統事先爲咱們提供了函數的宏,咱們能夠直接使用。這個宏的代碼並不是由本校人員開發,應當是較爲通用的定義方式。文件中爲咱們提供了兩種函數的宏,即葉函數(LEAF)和嵌套函數(NESTED)。設計
咱們把函數體中沒有函數調用語句的函數稱爲葉函數,天然若是有函數調用語句的函數稱爲非葉函數。在MIPS 的調用規範中,進入函數體時會經過對棧指針作減法的方式爲自身的局部變量、返回地址、調用函數的參數分配存儲空間(葉函數沒有後二者),在函數調用結束以後會對棧指針作加法來釋放這部分空間,咱們把這部分空間稱爲棧幀(Stack Frame)。3d
——OS指導書指針
下面是宏的具體實現定義。能夠看到,函數定義無非是聲明一個全局符號,給定一個標籤用於跳轉和返回。調試
下面是文件中部分代碼的引用。有些代碼後面我沒有寫註釋,是由於我本身也弄不太清楚,不敢亂講,怕引發誤會。若是有同窗明白,但願能夠給我講講。code
#define LEAF(symbol) \ .globl symbol; \聲明"symbol"爲全局變量 .align 2; \下一個數據的地址空間按字對齊 .type symbol,@function; \ .ent symbol,0; \告訴彙編器"symbol"函數的起始點,用於調試 symbol: .frame sp,0,ra 提供一個名爲"symbol"的標籤,將跳轉到此處 #define NESTED(symbol, framesize, rpc) \ .globl symbol; \ .align 2; \ .type symbol,@function; \ .ent symbol,0; \ symbol: .frame sp, framesize, rpc 肯定棧幀大小以及結束時的返回地址 #define END(function) \ .end function; \指出函數結尾,用於調試 .size function,.-function 在符號表中列出函數名和函數指令字節數
有時候,咱們會不可避免的在C語言中調用匯編函數,也會在彙編語言中調用C函數。根據MIPS軟件標準(ABI)的定義,函數的參數傳遞按照以下原則:blog
而關於函數的返回值,MIPS ABI規定,返回值存在$v0寄存器中。某些特殊的狀況下也會用到$v1寄存器,但不常見。想了解更多關於返回值的知識,請查閱書籍See MIPS Run Linux。
咱們在進行用戶態和內核態之間的切換,或者進程之間的切換時,須要保存現場。所謂現場,就是include/trap.h中所定義的trap結構體,其中包含的信息有:
可是這個文件中只有結構體的定義,沒有將數據存入結構體的操做。將寄存器中的值存入內存,顯然要用匯編語言去完成。stackframe.h中定義了一些彙編函數的宏,方便咱們對現場進行存取操做。下面摘錄了其中的宏,並做出相應的解釋。
//TF_SIZE是Trapframe寄存器的字節大小 .macro STI //Set Interrupt,打開全局中斷使能(容許中斷) .macro CLI //Close Interrupt,關閉全局中斷使能(屏蔽中斷) .macro SAVE_ALL //保存全部現場,將數據以Trapframe結構體形式存在sp爲開頭的空間中 .macro RESTORE_SOME //恢復部分現場,此處的「部分」僅不包括sp的值 .macro RESTORE_ALL //恢復全部現場,包括棧頂的位置 .macro RESTORE_ALL_AND_RET //恢復現場並從內核態中返回 .macro get_sp //獲取棧頂位置,此函數會判斷當前的狀態是異常仍是中斷, //從而決定棧頂是TIMESTACK仍是KERNEL_SP。 //系統調用是編號爲8的異常,進程切換是時鐘中斷信號。
按照指導書上的思路,咱們來梳理一下系統調用的流程:
須要填寫的文件:
user/syscall_wrap.S
只須要念一句咒語:syscall就好。固然,考慮到MIPS的習慣,能夠move v0, a0,這樣後面取出系統調用號也能夠在v0中取。
lib/syscall.S
TODO項有三:
注:第2、3、四個參數的值沒有改變過,於是也不須要修改。系統調用號寄存器a0雖然用於計算相對位置,可是此後的調用函數根本沒有用到,只是起到一個佔位的做用(指導書所言),於是也能夠不用修改a0的值,將錯就錯,不會影響。
lib/syscall_all.c
此處須要實現四個函數,按照文件中的函數順序來介紹。
/* Overview: * 這個函數容許當前進程釋放CPU。 * Post-Condition: * 取消運行當前進程。這個函數永遠也不會返回。(?) */ void sys_yield(void) { // your code here /* 直接使用咱們以前寫的sched_yield函數便可。 * 不過,須要在KERNEL_SP和TIMESTACK上作一點準備工做, * 由於當前進程處於內核態,保存的現場在KERNEL_SP - sizeof(struct Trapframe), * 可是env_run中所使用的進程切換機制中, * bcopy從TIMESTACK - sizeof(struct Trapframe)的位置進行復制 * 於是咱們要把現場複製到TIMESTACK棧區。 */ }
/* Overview: * 分配一頁內存,並映射到進程envid空間中的虛擬地址va,加上權限perm。 * 可能的反作用是,若是va已經和一個頁面p構建了映射,那麼頁面p就會被解除映射。 * Pre-Condition: * perm的PTE_V(有效)位必須爲1,而PTE_COW(寫時複製)位必須爲0。其餘位隨意。 * Post-Condition: * 返回值0是成功映射,返回值小於0便是出錯。 * 注意va必須小於UTOP,以及env可能會調整本身和子進程的地址空間。 */ int sys_mem_alloc(int sysno, u_int envid, u_int va, u_int perm) { // Your code here. struct Env *env; struct Page *ppage; int ret; ret = 0; /* 首先將上方註釋裏的全部須要判斷的狀況所有判斷完。 * 包括va的範圍,perm的部分位,envid是否合法。 * 進行頁面分配(page_alloc)和頁面插入(page_insert)的時候也會報錯,注意返回值。 * 各類負數返回值的意義在include/mmu.h中,此後再也不贅述調用函數的返回值。 * / }
/* Overview: * 將源進程地址空間中的相應內存映射到目標進程的相應地址空間的相應虛擬內存中去, * 而且附加保護位perm。perm的限制和sys_mem_alloc中同樣。 * (也許咱們應該加上只讀頁面不可映射爲可寫頁面的判斷?) * Post-Condition: * 返回值0表明成功,小於0表明報錯。 * Note: * 不能對UTOP以上的內存進行操做。 */ int sys_mem_map(int sysno, u_int srcid, u_int srcva, u_int dstid, u_int dstva, u_int perm) { int ret; u_int round_srcva, round_dstva; struct Env *srcenv; struct Env *dstenv; struct Page *ppage; Pte *ppte; ppage = NULL; ret = 0; round_srcva = ROUNDDOWN(srcva, BY2PG); round_dstva = ROUNDDOWN(dstva, BY2PG); //此處將兩個虛擬地址按頁進行對齊,映射時應當使用以上兩個地址。 // your code here /* 首先判斷srcva,dstva,perm,srcid,dstid是否合法, * 而後在源進程的地址空間中找到所需的頁面,插入到目標進程的地址空間中。 * 主要是使用page_lookup和page_insert兩個函數不能出錯。 */ return ret; }
/* Overview: * 解除envid進程空間中虛擬地址va所綁定的頁面。 * (若是va自己就沒綁定頁面,函數不做任何操做,算做成功) * Post-Condition: * 返回值0表明成功,小於0表明出錯。 * 不能解除UTOP地址以上空間的映射。 */ int sys_mem_unmap(int sysno, u_int envid, u_int va) { // Your code here. int ret = 0; struct Env *env; /* 首先判斷va,envid是否合法,而後page_remove便可,沒有技術含量。 * 注意page_remove自己具備判斷地址是否綁定的功能,因此無需畫蛇添足。 */ return ret; }
IPC 是微內核最重要的機制之一,目的是使得兩個進程之間能夠通信,須要經過系統調用來實現。通信最直觀的一種理解就是交換數據。
兩個進程之間之因此無法相互交換數據,是由於各個進程的地址空間相互獨立。咱們在以前寫的函數,正是爲了實現地址空間之間的溝通。而溝通兩個進程,天然須要一個權限凌駕兩個進程之上的存在來進行操做,即內核態。
在Lab3使用的進程控制塊(struct Env)中,有部分值用於本次實驗的進程間通訊,代碼以下:
// Lab 4 IPC u_int env_ipc_value; // 傳遞的數據值 u_int env_ipc_from; // 發送者的進程id u_int env_ipc_recving; // 進程是否阻塞,從而可以接收。0爲不能接收,1爲能夠接收。 u_int env_ipc_dstva; // 接收物理頁面的虛擬地址 u_int env_ipc_perm; // 接收頁面的保護位
IPC的操做,本質是在內核態中對這些部分進行賦值。咱們須要填的兩個函數位於lib/syscall_all.c中。
/* Overview: * 這個函數使得調用進程能夠接收其餘進程發送的信息。更準確地說, * 這個函數能夠標記當前進程,使得其餘進程能夠向其發送信息。 * Pre-Condition: * dstva必須合法(NULL也是合法的)。 * Post-Condition: * 這個系統調用函數會將當前進程狀態置爲NOT RUNNABLE,並釋放CPU。 */ void sys_ipc_recv(int sysno, u_int dstva) { /* 首先判斷dstva是否合法。而後,置recving位爲1,給dstva賦值, * 設置進程狀態爲阻塞,而且從新調用sys_yield。 * 因爲咱們的算法採用了兩個鏈表,因此當進程爲阻塞時,應當從就緒鏈表中移出。 * 不過若是你採用了這種寫法,就必須得另想辦法終止當前進程。 * 由於哪怕進程不在sched_list裏面,只要時間片沒用光,依然可能繼續運行。 * 這樣程序就會出錯。能夠選擇不刪除不插入,yield函數遇到NOT RUNNABLE就跳過。 */ } /* Overview: * Try to send 'value' to the target env 'envid'. * 將value傳給目標進程envid。 * 若是目標進程還沒有處於可接收狀態,返回值應當爲-E_IPC_NOT_RECV。 * 其餘狀況下,發送成功後,目標進程的IPC部分數據應當按照以下規則更新: * env_ipc_recving設置爲0,防止多餘的接收。 * env_ipc_from設置爲發送進程的id。 * env_ipc_value設置爲函數參數value。 * 目標進程須要標記爲RUNNABLE,以便從新運行。 * Post-Condition: * 返回值0表明成功,小於0表明出錯。 * * Hint: 你惟一須要調用的函數只有envid2env()。 */ int sys_ipc_can_send(int sysno, u_int envid, u_int value, u_int srcva, u_int perm) { int r; struct Env *e; struct Page *p; Pte *ppte; /* 判斷envid是否合法,目標進程是否處於可接收狀態。 * 這個函數貌似是殘缺的,srcva和perm沒有使用,也沒有映射物理頁面。 * 只是單純的傳遞一個值value而已。很迷。 * 一樣須要注意,設置爲就緒後是否加入就緒狀態鏈表。取決於我的程序。 */ return 0; }
此處只是分享個人見解,不保證答案的正確性和完備性。
Thinking 4.1 思考並回答下面的問題:
內核在保存現場的時候是如何避免破壞通用寄存器的?
內核保存現場的方法,是將全部通用寄存器、CP0寄存器、當前PC值保存到棧裏。可是,通用寄存器的值卻非一成不變、徹底保存。k0、k1兩個寄存器由中斷/自陷程序保留,這兩個寄存器的值得不到保證。內核使用k0、k1兩個寄存器保存用戶棧、取出內核棧,再進行保存,從而維護了大多數通用寄存器的值。
系統陷入內核調用後能夠直接從當時的a0-a3參數寄存器中獲得用戶調用msyscall留下的信息嗎?
能夠。內核保存現場的過程當中沒有破壞a0-a3參數寄存器的值,只改變過k0, k1, v0的值。
咱們是怎麼作到讓sys開頭的函數「認爲」咱們提供了和用戶調用msyscall時一樣的參數的?
參數的傳遞依賴於a0-a3參數寄存器和棧。只要咱們保證a0-a3參數寄存器不變,棧可以以本來的樣子複製到內核棧空間中,就可以讓sys開頭的函數認爲參數相同。
內核處理系統調用的過程對Trapframe作了哪些更改?這種修改對應的用戶態的變化是?
處理過程當中,內核改變了Trapframe中寄存器v0的值,用於在用戶態中傳遞系統調用函數的返回值。此外,內核改變了EPC的值,使得程序返回用戶態後可以從正確的位置繼續執行。
系統調用號 對於系統調用syscall_cgetc,它傳入msyscall函數的系統調用號的數字值應該是?
打開文件user/syscall_lib.h,能夠看到系統調用號的數值是常量SYS_cgetc。
打開文件include/unistd.h,能夠讀到__SYSCALL_BASE = 9527,SYS_cgetc = 9527+14 = 9541。
因此係統調用號的數字值應當是9541。