MIT JOS LAB1&2學習筆記
lab1和2概述:
本次作業系統實驗,我們對計算機的作業系統進行了初步的探究,通過完成作業和問題,我們對作業系統的啟動、核心載入、一些系統函式、堆疊的使用、記憶體管理有了更加深刻的瞭解,並且在完成作業的同時,深刻了解了計算機記憶體的結構以及每一塊兒對應的作用。從實踐的角度出發,很好的理解了一個作業系統的底層功能的實現。具體來說:
在啟動計算機的部分中,通過gdb 的單補除錯和斷點控制,我們看到計算機執行一條條機器指令的過程,並且初步的理解了一些指令的作用以及總體瞭解了計算機啟動和核心載入的過程。之後,我們對於Kernal 中的格式化輸出函式進行了探究,其中著重觀察了其對於進位制的控制以及輸出時頁面的控制。此外,我們還自主探究了一些較為底層的與I/O 有密切關係的函式,從而更加深刻的對I/O 功能有了一定的瞭解。之後,我們還探究了計算機堆疊的使用,理解了指標在堆疊中的作用,並且在作業中通過對於指標的操作改造了原有的堆疊顯示函式。在記憶體管理的部分,我們的一些對於計算機記憶體的瞭解的不足使我們曾一度停滯不前。之後,我們通過學習《深入瞭解Linux 核心》以及menlayout.h 檔案中的一些提示資訊逐漸瞭解了計算機對於記憶體管理的方法。在物理頁管理的部分,實現了boot 的分配、page 的初始化、分配以及釋放等。在虛擬記憶體部分,我們深入瞭解了計算機實體地址、線性地址、邏輯地址的相互轉化,實現了虛擬記憶體中頁表的管理,實現了頁目錄的初始化、boot 頁的對映、也得查詢刪除及插入等。之後,我們將men_init()中的程式碼補充完全,實現了對於核心部分線性空間的初始化,並最終通過了所有的check。
此外,在完成這兩大塊內容的同時,對於問題的完成也使我們對於一些基礎概念的理解,以及程式碼部分知識的掌握有了進一步的提升。
介紹工具和環境的配置
進入我們的具體問題
問題1:
問題2.1:說明兩個函式之間的聯絡
問題2.2解釋題目的程式碼
作業1:改成8進位制輸出
作業2
(一) 作業描述:
The above exercise should give you theinformation you need to implement a stack backtrace
function, which you should callmon_backtrace(). A prototype for this function is already
waiting for you in kern/monitor.c. You cando it entirely in C, but you may find the read_ebp()
function in inc/x86.h useful. You'll alsohave to hook this new function into the kernel
monitor's command list so that it can beinvoked interactively by the user.
The backtrace function should display alisting of function call frames in the following format:
Stack backtrace:
ebp f0109e58 eip f0100a62 args 00000001f0109e80 f0109e98 f0100ed2 00000031
ebp f0109ed8 eip f01000d6 args 0000000000000000 f0100058 f0109f28 00000061
...
The first line printed reflects thecurrently executing function, namely mon_backtrace itself,
the second line reflects the function thatcalled mon_backtrace, the third line reflects the
function that called that one, and so on.You should print all the outstanding stack frames. By
studying kern/entry.S you'll find thatthere is an easy way to tell when to stop.
(二) 解答:
1.堆疊是一種簡單的資料結構,是一種只允許在其一端進行插入或刪除的線性表。允許插入或刪除操作的一端稱為棧頂,另一端稱為棧底,對堆疊的插入和刪除操作被稱入棧和出棧。
2.有一組CPU 指令可以實現對程序的記憶體實現堆疊訪問。其中,POP 指令實現出棧操作,PUSH 指令實現入棧操作。CPU 的ESP 暫存器存放當前執行緒的棧頂指標,EBP 暫存器中儲存當前執行緒的棧底指標。CPU 的EIP 暫存器存放下一個CPU 指令存放的記憶體地址,當CPU 執行完當前的指令後,從EIP暫存器中讀取下一條指令的記憶體地址,然後繼續執行。
3.實現原理:C 函式呼叫時,首先將引數push入棧,然後push 返回地址,接著將原來的EBP push 入棧,然後將ESP 的值賦給EBP,令ESP 指向新的棧頂。而函式返回時,會將EBP 的值賦予ESP,然後pop 出原來的EBP 的值賦予EBP指標。
int
mon_backtrace(int argc, char **argv, structTrapframe *tf)
{
// Your code here.
unsigned int ebp;
unsigned int eip;
unsigned int args[5];
unsigned int i;
ebp = read_ebp();
cprintf("Stack backtrace:\n");
do {
eip = *((unsigned int*)(ebp + 4));
for(i=0; i<5; i++)
args[i] = *((unsigned int*) (ebp + 8 +4*i));
cprintf(" ebp %08x eip %08x args %08x%08x %08x %08x %08x\n",
ebp, eip, args[0], args[1], args[2],args[3], args[4]);
ebp = *((unsigned int *)ebp);
} while(ebp != 0);
Return 0;
}
作業 3
作業描述
在檔案 kern/pmap.c 中,你需要實現以下函式的程式碼(如下,按序給出):
boot_alloc()
mem_init() //(在呼叫check_page_free_list(1)之前的部分)
page_init()
page_alloc()
page_free()
解答
首先,我們要先來理解一下記憶體管理的機制。
1. 三個重要地址:
1) 邏輯地址為使用者程式所使用的地址;
2) 線性地址是作業系統根據x86 段式地址轉換將邏輯地址轉換後的地址,具體來說:線性地址 = 邏輯地址 -KERNBASE
3) 實體地址是作業系統地址轉換系統將線性地址通過頁表地址轉換後得到的資料真實儲存地址邏輯地址;
2. 實體記憶體分頁:
一個物理頁的大小為4K 位元組,第0 個物理頁從實體地址 0x00000000 處開始。由於頁的大小為4KB,就是0x1000 位元組,所以第1 頁從實體地址0x00001000處開始。第2 頁從實體地址 0x00002000 處開始。可以看到由於頁的大小是4KB,所以只需要32bit 的地址中高20bit 來定址物理頁。頁表:一個頁表的大小為4K 位元組(32bit),放在一個物理頁中。由1024 個4位元組的頁表項組成。頁表中的每一項的內容(每項4 個位元組,32bit)高20bit 用來放一個物理頁的實體地址,低12bit 放著一些標誌。
頁目錄:一個頁目錄大小為4K 位元組(32bit),放在一個物理頁中。由1024 個4 位元組的頁目錄項組成。頁目錄中的每一項的內容(每項4 個位元組)高20bit 用來放一個頁表的實體地址,低12bit 放著一些標誌。
3. 虛擬地址:
如果CPU 暫存器中的分頁標誌位被設定,那麼執行記憶體操作的機器指令時,CPU 會自動根據頁目錄和頁表中的資訊,把虛擬地址轉換成實體地址,完成該指令。比如 mov eax,004227b8h ,這是把地址004227b8h 處的值賦給暫存器的彙編程式碼,004227b8 這個地址就是虛擬址。CPU 在執行這行程式碼時,發現暫存器中的分頁標誌位已經被設定,就自動完成虛擬地址到實體地址的轉換,使用實體地址取出值,完成指令。對於Intel CPU 來說,分頁標誌位是暫存器CR0 的第31位,為1 表示使用分頁,為0 表示不使用分頁。對於初始化之後的 Win2k 我們觀察 CR0 ,發現第31 位為1。表明Win2k 是使用分頁的。使用了分頁機制之後,4G 的地址空間被分成了固定大小的頁,每一頁或者被對映到實體記憶體,或者被對映到硬碟上的交換檔案中,或者沒有對映任何東西。對於一般程式來說,4G 的地址空間,只有一小部分映射了實體記憶體,大片大片的部分是沒有對映任何東西。實體記憶體也被分頁,來對映地址空間。對於32bit的Win2k,頁的大小是4K 位元組。CPU 用來把虛擬地址轉換成實體地址的資訊存放在叫做頁目錄和頁表的結構裡。
4. 然後我們可以再inc/mmu.h 看到對於線性地址定義的闡述以及對於頁表、頁
目錄的值的巨集定義。
5.在inc/memlayout.h 中看到虛擬記憶體的層次結構。
6.然後開啟kern/pmap.c,首先來看boot——allocated()函式。bootmain的最後一句跳轉到0x10000c 處,開始執行 entry.S 的程式碼.kernel 結束之後就是freememory 了,而在free memory 的最開始存放的是pgdir,這塊記憶體就是由boot_alloc 申請。
於是,boot_alloc(unit32_t n)的功能就是申請n 個位元組的地址空間,返回申請空間的首地址。如果n 是0,則返回nextfree(未分配空間的首地址)。其中,分配的地址是頁對齊的,即4K 對齊。值得注意的是,未初始化的全域性變數和靜態變數會被自動初始化為0。函式以首先定義靜態區域性變數nextfree,它指向空閒記憶體空間的首地址。由於未初始化,所以變數自動初始化為0,所以首次呼叫boot_alloc()函式的時候,nextfree 的值是0,會執行下面的語句:
if (!nextfree) {
extern char end[];
nextfree = ROUNDUP((char *) end, PGSIZE);
}
還有一個細節就是需要4k 頁對齊,使用了ROUNDUP 巨集定義。接下來,在你n>0時,我們將nextfree+n 記憶體加入,並返回當前分配完的這塊記憶體的頭指標。
if(n>0){
result = nextfree;
nextfree = (char *)((uint32_t)nextfree+n);
nextfree = ROUNDUP(nextfree,PGSIZE);
return result;
}
else{
return nextfree;
}
7.接下來是mem_init()函式
這個函式只需要補充一部分就可以了。主要是要為struct Page 的結構體的指標pages 申請一定的地址空間。首先來看structPage 的定義:
struct Page
{ //Next page on the free list.
struct Page *pp_link;
uint16_t pp_ref;
}
結構體裡主要有兩個變數:
1) pp_link 表示下一個空閒頁,如果pp_link=0,則表示這個頁面被分配了,否則,這個頁面未被分配,是空閒頁面。
2) pp_ref 表示頁面被引用數,如果為0,表示是空閒頁。(這個變數類似於智慧指標中指標的引用計數)。補充的程式碼比較簡單,就是位pages 申請足夠的空間(npages 的頁面),來存放這些結構體,並且用memset 來初始化:
pages = (struct Page*)boot_alloc(sizeof(structPage)*npages);
memset(pages, 0, sizeof(struct Page)*npages);
8. page_init(void)函式,按照提示的記憶體劃分分別初始化page 就可以了。尤其注意提示4 要參考虛擬記憶體的層次圖,注意劃分Empty Memory。對於保留不能讓其他程式使用的page 均設定pp_ref=1 即可,對於可以使用的page 做成一個連結串列,在初始化可用page 時實際上就是做了一個連結串列的頭插入操作。
page_init(void)
{
// The example code here marks all physicalpages as free.
// However this is not truly the case. Whatmemory is free?
// Change the code to reflect this.
// NB: DO NOT actually touch the physicalmemory corresponding to
// free pages!
//以下按照提示的記憶體劃分分別初始化page
size_t i;
for (i = 0; i < npages; i++) {
if(i == 0)
{
// 1) Mark physical page 0 as in use.
// This way we preserve the real-mode IDTand BIOS structures
// in case we ever need them. (Currently wedon't, but...)
pages[i].pp_ref = 1;
pages[i].pp_link = NULL;
}
else if(i>=1 &&i<npages_basemem)
{
// 2) The rest of base memory, [PGSIZE,npages_basemem * PGSIZE)
// is free.
pages[i].pp_ref = 0;
pages[i].pp_link = page_free_list;
page_free_list = &pages[i];
}
else if(i>=IOPHYSMEM/PGSIZE &&i< EXTPHYSMEM/PGSIZE )
{
// 3) Then comes the IO hole [IOPHYSMEM,EXTPHYSMEM), which must
// never be allocated.
pages[i].pp_ref = 1;
pages[i].pp_link = NULL;
}
else if( i >= EXTPHYSMEM / PGSIZE&&
i < ( (int)(boot_alloc(0)) -KERNBASE)/PGSIZE)
{
// 4) Then extended memory [EXTPHYSMEM,...).
// Some of it is in use, some is free.Where is the kernel
// in physical memory? Which pages arealready in use for
// page tables and other data structures?
//
pages[i].pp_ref = 1;
pages[i].pp_link =NULL;
}
else
{
pages[i].pp_ref = 0;
pages[i].pp_link = page_free_list;
page_free_list = &pages[i];
}
}
}
9. page_alloc() 和 page_free()的函式思路比較簡單,一個是page申請,一個是page 釋放,主要也就是連結串列操作,和pp_ref 的賦值。
struct Page *
page_alloc(int alloc_flags)
{
// Fill this function in
//如page_free_list 為空,則不能正確分配空閒記憶體,所以返回空指標
if(page_free_list==NULL)
return NULL;
struct Page *result = page_free_list;
//將物理空閒列表的頭指標賦值給result,相當於分配了一個空閒記憶體
page_free_list = result->pp_link;
result->pp_link = NULL;
if(alloc_flags & ALLOC_ZERO)
memset(page2kva(result),0,PGSIZE);
return result;
}
void
page_free(struct Page *pp)
{
// Fill this function in 通過修改列表的表頭,將pp加入到佇列中
//驗證pp_ref!=0 的情況,檢驗要釋放的page 沒有被任何程式使用
if(pp->pp_ref!=0){
panic("pp_ref!=0");
}
pp->pp_link = page_free_list;
page_free_list = pp;
}
問題三:
先給出答案:虛擬地址。對於這個問題,首先pdf 的上面一頁教學內容已經基本把知識講到了,下面的截圖就是響應的關鍵內容:
然後搜尋資料還發現一下很重要的地方:
From code executing on the CPU, once we'rein protected mode (which we
entered first thing inboot/boot.S),there'sno way to directly use a linear or physical
address. All memory references areinterpreted as virtual addresses and translated
by the MMU, which means all pointers in Care virtual addresses.
這句話就告訴我們,在程式裡面的所有地址,都是虛擬地址,系統會通過MMU來翻譯得到他的實體地址。也就是說,程式裡面的一切的地址都是虛擬地址。即使是實體地址,程式在呼叫的時候也會把他當成是虛擬地址,會轉化為實體地址。
綜上所述,這道題就是要分清楚在JOS 系統裡面,什麼是實體地址,什麼是虛擬地址。由於系統會經常要進行地址的相關運算,所以經常要進行強制轉化,把一個unsigned int 型變數轉化為一個地址,或者相反,所以在程式裡面,需要對兩者進行區分。x 應該是uintptr_t,在程式裡面,任何指標都是虛擬地址(段偏移)。
作業四:
Now we set up virtual memory
看到這句話,進入我們的第四問。往下看一下mem_init的幾個註釋,接下來我們大致要做的事是將虛擬記憶體空間[UPAGES,UPAGES+ROUNDUP((sizeof(structPage) * npages)對映到物理空間
[PADDR(pages),PADDR(pages)+ROUNDUP((sizeof(struct Page) *npages)中這個對映關係加入到頁目錄pagedir中去;然後又是對bootstack和KERNBASE的相關地址做相同相似的對映並加入頁目錄,然後就是一大堆check。我們需要寫下的相關程式就是上邊操作對映的page_insert和boot_map_region,以及在實現這兩個函式要用到的pgdir_walk、page_lookup和page_remove。
我們從最基本的也是最重要的操作pgdir_walk 說起。這個函式做的事情就是: 給定一個頁目錄表指標pgdir ,該函式應該返回線性地址va 所對應的頁表項指標。兩個引數pde_t *pgdir, const void *va, int create,根據註釋可知具體要實現的東西是:pgdir 是一個指向page dictionary 的指標,然後va 是虛擬地址,這個函式要返回這個虛擬地址指向的頁表項(PTE)。如果這個PTE 存在,那麼返回這個PTE 即可,如果不存在,引數create 是指這個頁表項是否需要被建立,若需要就建立一個頁表項,不需要就返回NULL。建立失敗返回NULL,成功就為這個頁表項的引用計數+1。
所以我們的程式碼如下:
1.首先得到這個虛擬地址所在的頁目錄偏移:
int pdeIndex = (unsigned int)va >>22;
2.通過頁目錄表pgdir+頁目錄偏移求得這頁在pagedirectory 的地址,按照註釋的幾種情況分別操作:
3.最後計算這個頁目錄項對應的頁表頁的基地址,返回對應的頁表項指標
然後再來看看mem_init主要用到的boot_map_region函式。提示告訴我們要將虛擬記憶體空間[va, va+size)對映到物理空間[pa, pa+size)這個對映關係加入到頁目錄pagedir中。這個函式主要的目的是為了設定虛擬地址UTOP之上的地址範圍,這一部分的地址對映是靜態的,在作業系統的執行過程中不會改變,所以,這個頁的Page結構體中的pp_ref域的值不會發生改變。pde_t *pgdir,uintptr_t va, size_t size, physaddr_t pa,int perm,幾個引數意義也很明顯,一個頁目錄表指標pgdir,虛擬地址va和要對映的size,和va對映的實體地址pa,最後許可權標誌位perm。所以我們的程式碼也很簡單:
接下來的insert函式也是在mem_init進行呼叫,其目的和boot_map_region很像,把一個物理頁pp與虛擬地址va建立對映,主要是操作的空間物件不同。pde_t *pgdir, struct Page *pp, void *va,int perm,幾個引數和前面也差不多。根據註釋需要注意的是:
如果虛擬記憶體va 處已經有一個物理頁與它對映,那麼應該呼叫page_removed()。如果必要的話,應該分配一個頁表並把它插入頁目錄中。pp->ref 應該在插入成功後+1。如果一頁原來就在va 處,那麼TLB 一定是無效的。若成功則返回0,沒有成功分配頁表的話就返回E_NO_MEM。
根據這些情況寫出我們的程式碼:
來看看insert 要用到的page_remove 函式。取消虛擬地址va 處的物理頁對映,如果這個地址上本來就沒有物理頁,那麼就不用取消。注意細節就是:這處物理頁的引用計數要減1,然後要將它free,如果這個地址的頁表項存在,那麼頁表項要置0。頁表中remove 一個表項時要將TLB 置為無效。根據註釋資訊程式碼也很明瞭:
最後,在remove 中我們又用了一個函式,也是本題的最後一個函式page_lookup。結合註釋顧名思義,這個函式將會返回虛擬地址va 對映的頁page 結構體的指標。然後如果引數pte_store!=0,那麼我們將頁表項pte 的地址儲存到pte_store 中。如果va 處沒有物理頁,那麼返回NULL。所以程式碼:
作業五:
part3 主要是使用者空間和核心空間的一些東西。主要區分就是ULIM,在ULIM上面的就是核心空間,在下面的部分就是使用者空間
根據提示需要對映使用者僅可讀的頁,所以我們定義許可權為PTE_U|PTE_P。之後,我們通過ROUNDUP 計算出pages 結構體的大小,並使用已經定義好的page_insert()進行對映。
此處根據要求我們把虛擬地址[KSTACKTOP-KSTKSIZE, KSTACKTOP)對映到以bootstack 為起點的實體地址(bootstack 實際儲存的是其虛擬地址,需要轉換位實體地址),並且在許可權上為核心可讀寫,使用者不可見,所以在設定許可權後,使用boot_map_region()完成對映。
這段程式碼要求把地址從[KERNBASE, 2^32)對映到[0, 2^32 - KERNBASE)。但是我們沒有2^32-KERNNASE byte 的記憶體,所以我們需要通過ROUNDUP 來得到size。
問題4:
根據menlayout.h 中的記憶體表我們可以補充表格如下
我們已經將核心和使用者環境放在了相同的地址空間,為什麼使用者程式不能讀或者寫核心記憶體?什麼樣的具體機制保護核心記憶體?
主要看低3 位,即U,W,P 三個標誌位。
p:代表頁面是否有效,若為1,表示頁面有效。否則,表示頁面無效,不能對映頁面,否則會發生錯誤。
W:表示頁面是否可寫。若為1,則頁面可以進行寫操作,否則,頁面是隻讀頁面,不能進行修改。
U:表示使用者程式是否可以使用該頁面。若位1,表示此頁面是使用者頁面,使用者程式可以使用並且訪問該頁面。若為0,則表示使用者程式不能訪問該頁面,只有核心才能訪問頁面。
上面的頁面標誌位,可以有效的保護系統的安全。由於作業系統執行在核心空間(微核心除外,其部分系統功能在使用者態下進行)中執行,而一般的使用者程式都是在使用者空間上執行的。所以使用者程式的奔潰,不會影響到作業系統,因為使用者程式無權對核心地址中的內容進行修改。這就有效的對作業系統和使用者程式進行了隔離,加強了系統的穩定性。
3. 這個作業系統最大能支援多大的實體記憶體?為什麼?
2GB
原因:作業系統使用4MB 的UPAGES 存放頁的結構體Page,每個頁佔據8B 的空間,而每個頁對映4KB 的記憶體,所以總共對映的記憶體為:4MB/8B*4KB = 2GB.
4. 管理記憶體有多大的空間開銷,如果我們擁有最強大的實體記憶體?這個空間開
銷如何減小?
(1)存放所有的Page,需要1024*4B=4MB
(2)存放頁目錄表 kern_pgdir 4KB
(3)存放當前的頁表4MB。
總的開銷:8196 KB。
我們可以將每一頁的空間定為4MB 這樣空間利用率就會有很大的提升