mit jos lab2-4 綜述
本文主要講解了mit jos lab(2-4)中的內容,由於前輩們各種博客對題目解答已經非常詳細了,我就並不針對題目的解答做文章了,而是整體的對系統執行過程中,內存的情況作出概述,描述各個過程的虛擬地址的分配、使用情況。
其中也參考了各個前輩寫的博客,我分享在下面:
Lab2:https://www.cnblogs.com/fatsheep9146/p/5124921.html
和 https://www.jianshu.com/p/3be92c8228b6
Lab3:http://www.cnblogs.com/fatsheep9146/p/5341836.html
和:https://www.jianshu.com/p/f67034d0c3f2
Lab4:https://www.jianshu.com/p/10f822b3deda
接下來就是正文了~
-------------------------------------------------------------------------------------------------------------
從jos lab2開始,就進行內存管理內存管理的配置,由於jos采取了虛擬地址機制,所以逃不開分段和分頁,我們就先看看jos的虛擬地址和分段分頁具體是怎樣實現的:
Jos沒有具體實現分段機制,因此我們可以說虛擬地址和線性地址是等價的,在分頁時,我們也直接使用虛擬地址進行計算處理。
Jos的分頁機制是由一個二級頁表構成的,一級頁表os將其稱為頁目錄(page directory),二級頁表就叫頁表(page table)。
對於32位機器,共4G的地址空間來說,jos將每頁大小分為4k; 因此用一頁地址作為頁目錄,一個頁目錄存1024個頁目錄項; 通過頁目錄項,便可以找到對應的頁表,每個頁表有1024個頁,因此共4M。
32位虛擬地址,通過前10位,便可定位頁目錄,10-20位定位頁表項,20-32位是頁內偏移量。如圖:
這是理論,接下來講一下jos具體的實現,並談一談具體由32位虛擬地址如何找到對應的頁目錄,頁表,頁面,以及和物理地址的關系,並給出jos計算的具體實現。
首先給出一些預備知識,雖然並不難懂,但是大部分博客都沒有講到,主要是各個值之間的轉換關系和計算方法。
1.在jos中,物理地址和虛擬地址只是簡單的做了映射關系:
虛擬地址=物理地址+0xf0000000
2.在每一個頁目錄中,通過虛擬地址前10位尋找頁目錄項是由PDX實現的:
#define PDX(la) ((((uintptr_t) (la)) >> 22) & 0x3FF)
3.在頁表中,通過虛擬地址10-20位尋找頁表項是由PTX實現的:
#define PTX(la) ((((uintptr_t) (la)) >> 12) & 0x3FF)
4.下邊是頁內偏移:
#define PGOFF(la) (((uintptr_t) (la)) & 0xFFF)
5.PADDR:由虛擬地址轉換為物理地址
6.KADDR:由物理地址轉換為虛擬地址
7.ROUNDUP(n):向上按頁取整
例:n=1,返回4k
n=100,返回4k
n=4k+1,返回8k
8.page2pa: PageInfo 轉換成物理地址
(page[i]-pages)*4k
I386_init在最開始就調用了mem_init函數,整個jos地址管理也是主要從這部分開始,在這個函數中,奠定了整個地址管理的基礎,所以不得不說一下這裏邊各個函數的功能
避免顯得冗余,讓文章整體比較流暢,使概括性較強,我寫在了另外一篇文章裏:http://www.cnblogs.com/Not-a-Coder/articles/8178760.html
以下是整體的邏輯:
首先,通過boot_alloc申請了第一個頁,作為內核的頁目錄(kern_pgdir),在目錄的UVPT處,存放的是頁目錄首地址的物理地址(在後來多個進程,多個目錄時也是一樣)
然後,申請了npage個pageInfo大小的內存,pageInfo的數量和內存所有物理頁面的個數是一樣的(經過真實計算查看確實是這樣,jos只使用了系統的一部分內存,我的是64M),這點很重要,所以每個pageInfo都對應一個物理頁面,也才有後來的根據pageInfo來計算某個頁的虛擬地址(page2kva),根據page_free_list分配空閑頁。
申請了pages之後,便將其初始化,已經使用的頁將其被映射的次數置1,未使用的頁將其加入到page_free_list中。之後每次想申請空閑頁面時,從page_free_list中獲取一個PageInfo,然後經過計算既可以了(具體的計算在上文鏈接中)。當頁面再次處於空閑,再將其放回page_free_list中
接下來說一下權限(perm),是否在內存中(PTE_P)等條件是怎麽解決的:
主要是通過boot_map_region實現,這個函數除了映射虛擬地址和物理地址外,還將物理地址最後12位作為一些信息存儲位(具體我再函數介紹時說過了),通過傳入相應的物理地址和虛擬地址以及一些權限信息,就可以實現。因此通過
boot_map_region(kern_pgdir, KSTACKTOP - KSTKSIZE, KSTKSIZE, PADDR(bootstack), PTE_W);
boot_map_region(kern_pgdir, KERNBASE, 0xffffffff - KERNBASE, 0, PTE_W);
就可以將內核的堆棧和整個內核區域都設為只有內核可以讀寫了(kernbase之上為內核區域,之後會有說明)
對於之後判斷是否可以對某塊區域訪問,是在後面的實驗中又補充了一個函數,因為關系密切,我就一並寫在這了,就是user_mem_check函數,他可以根據某個進程的信息和虛擬地址,來判斷這個進程是不是可以訪問此塊內存區域。
在lab2完成虛擬地址的管理系統後,主要對外調用的有:
boot_map_region:用來對虛擬地址和物理地址映射,並分配權限等信息
page_alloc:申請新的頁面
page_insert:將新申請的頁面插入頁目錄,頁表項中
page_free:釋放頁
user_mem_check:檢查某進程是否有權限訪問某塊地址
至此,地址管理具體方法大致都說完了,在mem_init中,初始化完地址管理的一些東西,便開始初始化進程管理的東西,接下來會開始介紹,當有了進程時,具體的流程和lab2類似,一些具體的函數說明寫在了 http://www.cnblogs.com/Not-a-Coder/articles/8178760.html
在men_init中,同樣使用boot_alloc申請了NENV個Env結構體(這個結構體的屬性存儲了進程的狀態(正在運行,就緒,阻塞,還有“喪失進程”)、運行過程中的需要的各個寄存器的值等重要信息)。之後立即在kern_pgdir中為其增加了虛擬地址和物理地址的映射(boot_map_region),並設置了用戶和內核都可以讀取的權限,之後便調用了env_init函數,初始化整個進程的結構體鏈表,這個過程和pages的初始化類似,也維護了一個env_free_list,就不再多說。
當想要創建並啟用一個進程時,調用env_create,它會調用env_alloc,從env_free_list中取出一個env結構體,再通過 env_setup_vm為其初始化,申請新的頁目錄; 然後執行load_icode,這個函數加載elf文件(二進制文件),它會調用region_alloc為其分配頁,並將虛擬地址和物理地址作出映射,load_icon之後分配進程棧,以及,將env->env_tf.tf_eip指向將執行進程函數的入口(等待env_pop_tf的調用)。之後調用env_run啟動進程,通過env_pop_tf函數,改變env結構體中運行狀態等信息(防止後來的一個進程在多個內核中運行),將env結構體中保存的寄存器中的信息加在到真正的寄存器中,接下來便執行eip所指向的內容,一個進程便可以準備啟動了。
但真正啟動一個進程還需要在lib/libmain.c 文件中修改一下 libmain() 函數,使它能夠初始化全局指針 thisenv ,讓它指向當前用戶環境的 Env 結構體。libmain() 會調用 umain,即用戶進程的main函數。
當進程產生後,很重要的就是處理中斷和異常,因此,接下來我們開始討論中斷和異常的處理。
為了保護中斷和異常時切入內核是安全且受保護的。jos為了做到這一點,使用了中斷描述符表(IDT)和任務狀態段(TSS)。
首先來看中斷描述符表。處理器將確保從一些內核預先定義的條目才能進入內核,而不是由中斷或異常發生時運行的代碼決定。不同的中斷條目代表中斷來源:不同的設備以及錯誤類型,CPU 利用這些向量作為中斷描述符表的索引。從表中恰當的條目,處理器可以獲得需要加載到指令指針寄存器(EIP)的值(該值指向內核中處理這類異常的代碼),以及代碼段寄存器(CS)的值,其中最低兩位表示優先級。
而任務狀態段處則需要處理器保存中斷和異常出現時的自身狀態,例如 EIP 和 CS,以便處理完後能返回原函數繼續執行。但是存儲區域必須禁止用戶訪問,避免惡意代碼或 bug 的破壞。他也定義需要切換的內核棧(數據的壓入位置)
為了完成上述功能,我們要為每個處理中斷的函數聲明並註冊,在jos的實現裏,在trapentry.S中聲明處理函數,在trap.c中為其註冊。整個當系統在i386_init時,便調用此初始化程序了,之後中斷發生時,系統就可以捕獲中斷了。
處理器捕捉到中斷,然後切換到內核堆棧,將異常參數壓入堆棧(按順序 push SS, ESP, EFLAGS, CS, EIP
),查詢中斷向量表,找到對應的表項,把eip的值設為處理中斷執行代碼地址(_alltraps), 後來執行中斷處理函數trap,通過trap_dispatch選擇相應的執行代碼。執行完成後,恢復被中斷的進程的上下文,返回用戶態,繼續運行這個進程(trap_dispatch是根據傳入的保存著各個寄存器的結構體信息中的trapno來進行選擇執行哪一個函數,最終執行系統調用處理異常/中斷),接下來的一些調用的實現(斷點異常、系統調用),因為其他博客寫的都很清楚,我就不再描述了。
lab3雖然描寫了進程的創建以及中斷的處理,但是並不是特別的明朗,因為只有一個進程在運行,所以很多東西沒有體現出來。到lab4就開始了多核多進程的的處理,應該會使我們能更清晰的了解進程的運行,由於之前的鋪墊已經很多了,所以lab4的某些地方會比較簡略
多個cpu的初始化我就不多說了(因為我不是特別了解),但是需要註意的是,每個cpu都應該有自己的內核棧,防止中斷時壓棧導致錯誤。同時,由於每個進程都有自己的頁目錄,所以,不同的進程需要切換頁目錄,這個功能就是由lcr3函數實現的。
接下來我們直接看多個內核競爭內核資源的問題:如果同一時刻,不同cpu同時訪問內核區域,便會造成資源的競爭,因此,便要給內核上鎖,同時只允許一個cpu訪問。jos中是以大內核鎖實現的,1.在喚醒其他內核, 2.在用戶陷入內核態時,3初始化應用處理器之後獲得內核鎖,在切換回用戶模式前釋放內核鎖。
接下來是設置不同進程的調度問題:
jos采用的是輪詢的方式,就是循環遍歷envs數組(根據ENVX尋找每個進程),選擇"就緒"的進程,每次運行一個進程。(在lab3中我們已經提到了,每當一個進程運行前,就會將他存儲的屬性狀態設為runing,保證一個進程不會在多個cpu中運行)
這個時候我們需要添加新的系統調用,不然就無法切換進程了(系統調用在lab3中也交代過了,就不再多說)
一個常見的功能就是fork函數,進程拷貝,因為前一種拷貝所有信息的方式沒有被采用,所以這裏我們直接說 只拷貝頁面映射的方式。
為什麽可以只拷貝頁面映射調用了fork()
之後往往立即就會在子進程中用exec()
,將子進程的內存更換為新的程序。這樣,復制父進程的內存這個操作就完全浪費了,因此,讓父、子進程共享同一片物理內存,直到某個進程修改了內存。這樣可以大大的減少資源的浪費。
jos的具體實現:
主要是由duppage函數實現拷貝父子進程需要的頁面
pgdault處理頁面錯誤,頁面錯誤證明了此時父子進程的虛擬頁面應該指向不同的物理頁面。此時就需要重新分配空間,具體的流程是:先拷貝父進程的物理頁面,而後將子進程的虛擬地址映射到新的物理頁面,並消除原來的映射。之後再對內容更改。
接著便要實現多進程的搶占式處理,在之前,子進程開啟後就陷入死循環,此後 kernel 無法再獲得控制權。我們需要讓硬件周期性地產生時鐘中斷,強制將控制權交給 kernel,使得我們能夠切換到其他進程,因此我們在trapdistch中添加時鐘中斷。
之後看一下進程間通信:他有兩種方式,一種是只發送一個數字,一種是頁面共享。
jos具體的實現是ipc_recv函數和ipc_send函數。他的進程通訊還是比較簡陋的,當一個進程調用接受消息的函數後,會阻塞在那,知道某個進程向他發送了消息。發送消息要做多項檢測,而且,如果沒有收到返回消息,就會一直發送。共享頁面也是同樣的道理。
到此,lab4的就結束了。
可能會發現,lab3、lab4的介紹簡單了許多。這是因為,很多東西在其他博客上已經寫得很詳細了,沒有必要多說(可以去看我開始給出的一些鏈接)。
此外我只想給出每個實驗的概括性思想,而後邊的實驗很難一步一步連起來,我只能拆分開一個一個的講,便顯得有些散了。由於我並不熟悉匯編,所以題目中有些設計匯編的細節,被我忽略掉了。
最後我還要介紹一下整個4G空間的使用情況,如圖:
整個空間中的一些重要的信息都被我標註了一下,方便查看
如果文章有什麽錯誤,歡迎指正~
mit jos lab2-4 綜述