# 五、
來源:[JOS學習筆記(五)](http://blog.csdn.net/roger__wong/article/details/8656339)
> 神說、內存之間要有映射、將地址空間分為虛實。
> 神就造出兩級頁表、將變換前的地址、變換后的地址分開了.事就這樣成了。
> 有晚上、有早晨、是第二日。
來到了lab2,內存管理,該實驗分為兩部分,第一部分為物理內存管理,第二部分為虛擬內存管理,本篇先描述lab1。
做本章實驗一定頭腦中要時刻清晰的記住兩個內存分布圖:物理內存分布圖以及虛擬內存分布圖。
物理內存的分布在前面的筆記中有介紹,這里拷貝過來:
```
+------------------+ <- 0xFFFFFFFF (4GB)
| 32-bit |
| memory mapped |
| devices |
| |
/\/\/\/\/\/\/\/\/\/\
/\/\/\/\/\/\/\/\/\/\
| |
| Unused |
| |
+------------------+ <- depends on amount of RAM
| |
| |
| Extended Memory |
|------------------|
| kernnel |
+------------------+ <- 0x00100000 (1MB)
| BIOS ROM |
+------------------+ <- 0x000F0000 (960KB)
| 16-bit devices, |
| expansion ROMs |
+------------------+ <- 0x000C0000 (768KB)
| VGA Display |
+------------------+ <- 0x000A0000 (640KB)
| |
| Low Memory |
|------------------| <-0x00010000 (elf herader here!)
|------------------| <-0x00007c00 (boot loader here!)
| bootmain stack |
+------------------+ <- 0x00000000
```
虛擬內存分布參見memlayout.h文件,這里也拷貝過來:

好,記住這兩個圖然后開始做實驗。
## 1、實驗內容
實驗內容很簡單,只是讓你完成下列幾個函數
```
boot_alloc()
mem_init()
page_init()
page_alloc()
page_free()
```
mem_init()會調用你寫的函數,然后再調用check_page_free_list和check_page_alloc兩個函數來判斷你完成的函數的邏輯對不對,基本上通過驗證就沒問題了。
## 2、原理。
JOS使用Page數據結構來管理內存,一個Page代表一個PGSIZE(4K)大小的物理頁面,Page數據結構的定義在memlayout.h中,共有兩個域,第一個域是pp_link,指向下一個空閑Page結構的指針,第二個域是一個short整形,代表當前此物理頁面的引用次數,若為0則是沒有被引用也就是空閑頁面。
其次使用free_page_list維護一個空閑物理內存的鏈表,free_page_list本身就是一個Page指針,然后通過Page結構體里面的pp_link域構成空閑鏈表。
再次一個Page代表4K,在pmap.c中的i386_memory_detect函數中檢測內存后,使用總物理內存/4k得到所需要的Page數量,賦值給npages,換句話說npages代表所需Page結構體的數量。
最后所有Page在內存(物理內存)中的存放是連續的,存放于pages處,可以通過數組的形式訪問各個Page,而pages緊接于end[]符號之上,end符號是編譯器導出符號,其值約為kernel的bss段在內存(虛擬內存)中的地址+bss段的段長,對應物理和虛擬內存布局也就是在kernel向上的緊接著的高地址部分連續分布著pages數組。
除此之外JOS提供page2pa,pa2page等函數可以進行Page數據結構的指針向物理地址的轉換,或反轉換等。
## 3、具體函數實現
首先被調用的是boot_alloc(),它要在什么都沒做好的情況下開辟出n字節的空閑空間,并返回其首地址。
做法很簡單,end符號向上均為未使用空間,只要返回這些空間就行。
首先將end符號向上和4K字節對齊(JOS已經幫我們完成),然后將這個地址(也就是nextfree)加上你要分配的空間并依然4K字節對齊,接著返回原先的nextfree即可。
```
char* result;
result=nextfree;
nextfree+=ROUNDUP(n,PGSIZE);
return result;
```
然后完成mem_init()中的部分代碼,可以看到mem_init()中首先檢查可用內存大小,然后調用boot_alloc()分配了一個頁面給kern_pgdir,這個在part2中會用,現在沒啥用。
接下來需要我們給pages分配空間。通過以上原理分析,很容易得出代碼:
```
pages=(struct Page*)boot_alloc(npages*sizeof(struct Page);
```
接著完成page_init()。
在page_init()里系統首先給我們初始化了pages數組以及page_free_list,可以看到這個page_free_list指向了所有的Page結構,換句話說此時認為所有的頁面都是空閑可分配的,這當然是不對的,所以就要從中把一些我們已經用的內存頁面從中剔除出去,這包括0地址向上的第一個頁面(包括IDT等),IO hole(0xA0000--0x100000,包括vga display ,bios等),kernel地址之上的部分(kernel本身+kern_pgdir+pages)。巧合的是,IO hole,和kernel之上部分是連續的地址,因為kernel就加載在0x100000處,所以其實只需要剔除兩塊地址,第一塊是0地址開始的第一個頁面,第二塊就是io hole開始的向上的一組連續的頁面。
分析一下page_free_list的代碼邏輯,不難發現這個鏈表是從pages數組的末尾開始從高地址指向低地址,所以我們先計算出要剔除的Page的地址,然后通過指針操作剔除即可。

首先剔除第一個頁面,只需讓第二個頁面(下標為1)的pp_link域指向空,因為原本其指向的是第一個頁面,而第一個頁面的pp_link域指向空,也等價與pages[1].pp_link=pages[0].pp_link
其次剔除一組連續頁面,使用pgstart和pgend代表這組連續地址空間的首尾所在的Page結構(所謂首是低地址,所謂尾是高地址)。
首地址也就是IOPHYSMEM所在地址,注意IOPHYSMEM已經是物理地址了,所以只需要使用pa2page得到Page結構即可。
pgend是較高位的地址,首先將end符號地址轉化成物理地址(-KERNBASE),然后再加上剛才分配的kern_pgdir(一個PGSIZE)和pages數組所占用的空間即可。當然更嚴謹點應該4K字節對齊的,不過不對其也能落在正確的4K范圍內,不影響程序正確性。
其次注意到pgstart和pgend這兩個Page也是要剔除的,所以需要找到pgend的上一個Page,和pgstart的下一個Page,這兩個Page應該是空閑的。因為所有Page的組織是按數組進行組織,所以只需要進行+1和-1的地址操作即可。
接著改變pp_link域,跳過中間的區域即可。
接下來是很簡單的page_alloc()。
代碼邏輯很簡單,從page_free_list頭剔除一個Page,然后改變page_free_list使其為其pp_link即可。

接著是Page_free,直接上圖:

至此part1結束:

一點感想:
在用戶態編程反而覺得內存的分配是理所當然的,然而在OS內核中寫相關代碼時,產生的一種很奇妙的感覺就是這個過程是由程序員自己掌控的,并且分配的結果會反而影響后面的代碼邏輯。
What an amazing experience !