Lab 2: Memory Management
lab2中多出來的幾個文件:
inc/memlayout.h
kern/pmap.c
kern/pmap.h
kern/kclock.h
kern/kclock.c
memlayout.h描述了虛擬地址空間的結構,我們需要通過修改pmap.c文件來實現這個結構。memlayout.h和pmap.h文件定義了一個PageInfo結構,利用這個結構可以記錄有哪些物理頁是空閑的。kclock.c和kclock.h文件中操作的是用電池充電的時鍾,以及CMOS RAM設備。在這個設備中記錄着PC機擁有的物理內存的數量。在pmap.c中的代碼必須讀取這個設備中的信息才能弄清楚到底有多少內存。
Part 1:Physical Page Management
操作系統必須要追蹤記錄哪些內存區域是空閑的,哪些是被占用的。JOS內核是以頁(page)為最小粒度來管理內存的,它使用MMU來映射,保護每一塊被分配出去的內存。
在這里你要具體編寫一下物理內存頁的分配子函數。它利用一個結構體PageInfo的鏈表來記錄哪些頁是空閑的,鏈表中每一個結點對應一個物理頁。
Exercise 1. 在文件 kern/pmap.c 中,你必須要完成以下幾個子函數的代碼
boot_alloc(); mem_init(); page_init(); page_alloc(); page_free();
check_page_free_list()和check_page_alloc()兩個函數將會檢測你寫的頁分配器代碼是否正確。
答:
我們觀察一下pmap.c中的代碼,其中最重要的函數就是mem_init()了,在內核剛開始運行時就會調用這個子函數,對整個操作系統的內存管理系統進行一些初始化的設置,比如設定頁表等等操作。
下面進入這個函數,首先這個函數調用 i386_detect_memory 子函數,這個子函數的功能就是檢測現在系統中有多少可用的內存空間。
之前我們介紹過,jos把整個物理內存空間划分成三個部分:
一個是從0x00000~0xA0000,這部分也叫basemem,是可用的。
緊接着是0xA0000~0x100000,這部分叫做IO hole,是不可用的,主要被用來分配給外部設備了。
再緊接着就是0x100000~0x,這部分叫做extmem,是可用的,這是最重要的內存區域。
這個子函數中包括三個變量,其中npages記錄整個內存的頁數,npages_basemem記錄basemem的頁數,npages_extmem記錄extmem的頁數。
執行完這個函數,下一條指令為:
kern_pgdir = (pde_t *) boot_alloc(PGSIZE);
memset(kern_pgdir, 0, PGSIZE);
其中kern_pgdir是一個指針,pde_t *kern_pgdir,它是指向操作系統的頁目錄表的指針,操作系統之后工作在虛擬內存模式下時,就需要這個頁目錄表進行地址轉換。我們為這個頁目錄表分配的內存大小空間為PGSIZE,即一個頁的大小。並且首先把這部分內存清0。
這里調用了boot_alloc函數,這個函數使我們要首先實現的函數:
這個函數就像在注釋中說的那樣,它只是被用來暫時當做頁分配器,之后我們使用的真實頁分配器是page_alloc()函數。而這個函數的核心思想就是維護一個靜態變量nextfree,里面存放着下一個可以使用的空閑內存空間的虛擬地址,所以每次當我們想要分配n個字節的內存時,我們都需要修改這個變量的值。
所以添加的代碼為:
result = nextfree; nextfree = ROUNDUP(nextfree+n, PGSIZE); if((uint32_t)nextfree - KERNBASE > (npages*PGSIZE)) panic("Out of memory!\n"); return result;
所以這條kern_pgdir = (pde_t *) boot_alloc(PGSIZE);指令就會分配一個頁的內存,並且這個頁就是緊跟着操作系統內核之后。
再看下一條命令:
kern_pgdir[PDX(UVPT)] = PADDR(kern_pgdir) | PTE_U | PTE_P;
這一條指令就是再為頁目錄表添加第一個頁目錄表項。通過查看memlayout.h文件,我們可以看到,UVPT的定義是一段虛擬地址的起始地址,0xef400000,從這個虛擬地址開始,存放的就是這個操作系統的頁表kern_pgdir,所以我們必須把它和頁表kern_pgdir的物理地址映射起來,PADDR(kern_pgdir)就是在計算kern_pgdir所對應的真實物理地址。
下一條命令需要我們去補充,這條命令要完成的功能是分配一塊內存,用來存放一個struct PageInfo的數組,數組中的每一個PageInfo代表內存當中的一頁。操作系統內核就是通過這個數組來追蹤所有內存頁的使用情況的。我寫的代碼如下:
pages = (struct PageInfo *) boot_alloc(npages * sizeof(struct PageInfo)); memset(pages, 0, npages * sizeof(struct PageInfo));
下一條指令我們將運行一個子函數,page_init(),這個子函數的功能包括:
1. 初始化pages數組 2.初始化pages_free_list鏈表,這個數組中存放着所有空閑頁的信息
我們可以到這個函數的定義處具體查看,整個函數是由一個for循環構成,它會遍歷所有內存頁所對應的在npages數組中的PageInfo結構體,並且根據這個頁當前的狀態來修改這個結構體的狀態,如果頁已被占用,那么要把PageInfo結構體中的pp_ref屬性置一;如果是空閑頁,則要把這個頁送入pages_free_list鏈表中。根據注釋中的提示,第0頁已被占用,io hole部分已被占用,還有在extmem區域還有一部分已經被占用,所以我們的代碼如下:
size_t i; page_free_list = NULL; //num_alloc:在extmem區域已經被占用的頁的個數 int num_alloc = ((uint32_t)boot_alloc(0) - KERNBASE) / PGSIZE; //num_iohole:在io hole區域占用的頁數 int num_iohole = 96; for(i=0; i<npages; i++) { if(i==0) { pages[i].pp_ref = 1; } else if(i >= npages_basemem && i < npages_basemem + num_iohole + num_alloc) { pages[i].pp_ref = 1; } else { pages[i].pp_ref = 0; pages[i].pp_link = page_free_list; page_free_list = &pages[i]; } }
初始化關於所有物理內存頁的相關數據結構后,進入check_page_free_list(1)子函數,這個函數的功能就是檢查page_free_list鏈表的所謂空閑頁,是否真的都是合法的,空閑的。當輸入參數為1時,這個函數要在檢查前先進行一步額外的操作,對空閑頁鏈表free_page_list進行修改,經過page_init,free_page_list中已經存放了所有的空閑頁表,但是他們的順序是按照頁表的編號從大到小排列的。當前操作系統所采用的頁目錄表entry_pgdir(不是kern_pgdir)中,並沒有對大編號的頁表進行映射,所以這部分頁表我們還不能操作。但是小編號的頁表,即從0號頁表開始到1023號頁表,已經映射過了,所以可以對這部分頁表進行操作。那么check_page_free_list(1)要完成的就是把這部分頁表對應的PageInfo結構體移動到free_page_list的前端,供操作系統現在使用。
剩下的操作就是對你的free_page_list進行檢查了。
check_page_free_list(1)執行完成,我們將進入下一個檢查函數check_page_alloc(),這個函數的功能是檢查page_alloc(),page_free()兩個子函數是否能夠正確運行。所以我們首先要實現這兩個子函數。
先實現page_alloc()函數,通過注釋我們可以知道這個函數的功能就是分配一個物理頁。而函數的返回值就是這個物理頁所對應的PageInfo結構體。
所以這個函數的大致步驟應該是:
1. 從free_page_list中取出一個空閑頁的PageInfo結構體
2. 修改free_page_list相關信息,比如修改鏈表表頭
3. 修改取出的空閑頁的PageInfo結構體信息,初始化該頁的內存
代碼如下:
struct PageInfo * page_alloc(int alloc_flags) { struct PageInfo *result; if (page_free_list == NULL) return NULL; result= page_free_list; page_free_list = result->pp_link; result->pp_link = NULL; if (alloc_flags & ALLOC_ZERO) memset(page2kva(result), 0, PGSIZE); return result; }
然后實現page_free()方法,根據注釋可知,這個方法的功能就是把一個頁的PageInfo結構體再返回給page_free_list空閑頁鏈表,代表回收了這個頁。
主要完成以下幾個操作:
1. 修改被回收的頁的PageInfo結構體的相應信息。
2. 把該結構體插入回page_free_list空閑頁鏈表。
代碼如下:
void page_free(struct PageInfo *pp) { // Fill this function in // Hint: You may want to panic if pp->pp_ref is nonzero or // pp->pp_link is not NULL. assert(pp->pp_ref == 0); assert(pp->pp_link == NULL); pp->pp_link = page_free_list; page_free_list = pp; }
至此,我們已經完成了這個Exercise為我們布置的任務,但是mem_init()函數的完善沒有完成,這個將在下面的練習中繼續完善。
Part 2: Virtual Memory
Virtual, Linear, and Physical Addresses
在x86體系中,一個虛擬地址(Virtual Address)是由兩部分組成,一個是段選擇子(segment selector),另一個是段內偏移(segment offset)。一個線性地址(Linear Address)指的是通過段地址轉換機構把虛擬地址進行轉換之后得到的地址。一個物理地址(Physical Addresses)是分頁地址轉換機構把線性地址進行轉換之后得到的真實的內存地址,這個地址將會最終送到你的內存芯片的地址總線上。

我們所編寫的C語言程序中的指針的值是虛擬地址中段內偏移部分的值。在boot/boot.S文件中,我們引入了一個全局描述符表,這個表通過把所有的段的基址設置為0,界限設置為0xffffffff的方式,關閉了分段管理的功能。因此虛擬地址中的段選擇子字段的內容已經沒有任何意義,線性地址的值總是等於虛擬地址中段內偏移的值。
回顧一下lab1中的part 3,我們引入了一個簡單的頁表,使得內核可以運行與0xf0100000的虛擬地址空間,盡管它所在的真實位置是物理地址0x00100000處,剛剛好在ROM BIOS之上。這個頁表僅僅映射了4MB的內存空間。在我們這個JOS操作系統中,我們希望把這種映射擴展到物理內存的頭256MB空間上,並且把這部分物理空間映射到從0xf0000000開始的虛擬空間中,以及一些其他的虛擬地址空間中。
Exercise 3
通過GDB,我們只能通過虛擬地址來查看內存所存放的內容,但是如果我們能夠訪問物理內存的話,肯定會更有幫助的。我們可以看一下QEMU中的一些常用指令,特別是xp指令,可以允許我們去訪問物理內存地址。
“QEMU中有一個內置的監控器(moniter),首先通過在運行着QEMU軟件的terminal里面輸入 ctrl-a c,可以讓我們切換到這個監控器。” 這個是官方給出的做法,但是在我的機器上並不好使,所以通過查詢,發現在lab目錄下面輸入如下指令,一樣可以打開moniter:
qemu-system-i386 -hda obj/kern/kernel.img -monitor stdio -gdb tcp::26000 -D qemu.log
打開monitor后,我們可以輸入如下比較常見的指令:
xp/Nx paddr -- 查看paddr物理地址處開始的,N個字的16進制的表示結果。
info registers -- 展示所有內部寄存器的狀態。
info mem -- 展示所有已經被頁表映射的虛擬地址空間,以及它們的訪問優先級。
info pg -- 展示當前頁表的結構。
一旦進入保護模式,我們就不能直接使用線性地址或者物理地址了。所有代碼中的地址引用都是虛擬地址的形式,然后被MMU系統所轉換,所以C語言中的指針其實都是虛擬地址。
JOS內核通常需要把地址按照以一種模糊的值或者整數值的形式來操縱,而不是直接解析引用,比如物理內存分配器。有時使用虛擬地址,有時使用物理地址。為了能夠幫助我們記錄代碼,JOS源文件中的地址被區分為兩種情況:
uintptr_t -- 表示虛擬地址
physaddr_t -- 表示物理地址
這兩種類型其實都是32位的整型數(uint32_t),所以如果你把一個類型的變量的值賦給另一個類型變量,編譯器不會報錯。但是由於他們都是整型數,所以如果你打算解引用(deference)他們,編譯器會報錯。
JOS內核可以先對uintptr_t類型的值進行強制類型轉換,然后再解析引用。但是對於physaddr_t的值,我們不能這么做,因為內核是需要MMU(內存管理單元)來首先對你輸入的地址進行轉化的,如果你對physaddr_t進行強制類型裝換再解引用,最終你得到的你要訪問的地址,可能不是你要找的真實物理地址。
總結以下:

問題:
假設下述JOS內核代碼是正確的,那么變量x應該是uintptr_t類型呢,還是physaddr_t呢?
mystery_t x; char* value = return_a_pointer(); *value = 10; x = (mystery_t) value;
答:
由於這里使用了 * 操作符解析地址,所以變量x應該是uintptr_t類型。
JOS內核有時需要讀取或者修改內存,但是這時有可能他只知道這個要被修改的內存的物理地址。舉個例子,當我們想要加入一個新的頁表項時,我們需要分配一塊物理內存來存放頁目錄項,然后初始化這塊內存。然而,內核,它是不能繞過 虛擬地址轉換 這一步的,因而它也不能直接加載或者存儲物理地址。那么我們如何把物理地址轉換為虛擬地址,我們可以采用KADDR(pa)指令來獲取。其中pa指的是物理地址。
同樣的,如果想通過虛擬地址的值求得物理地址的值,我們可以采用PADDR(va)指令。
Reference counting
在之后的實驗中,你將會經常遇到一種情況,多個不同的虛擬地址被同時映射到相同的物理頁上面。這時我們需要記錄一下每一個物理頁上存在着多少不同的虛擬地址來引用它,這個值存放在這個物理頁的PageInfo結構體的pp_ref成員變量中。當這個值變為0時,這個物理頁才可以被釋放。通常來說,任意一個物理頁p的pp_ref值等於它在所有的頁表項中,被位於虛擬地址UTOP之下的虛擬頁所映射的次數(UTOP之上的地址范圍在啟動的時候已經被映射完成了,之后不會被改動)。
當我們使用page_alloc函數的時候需要注意。它所返回的頁的引用計數值總是0,所以pp_ref應該被馬上加一。
Page Table Management
現在你應該可以着手開始編寫管理頁表的程序了:包括插入和刪除線性地址到物理地址的映射關系,以及創建頁表等操作。
Exercise 4. 完成kern/pmap.c中的下面幾個子函數的編碼
pgdir_walk() boot_map_region() page_lookup() page_remove() page_insert()
check_page()子函數將會被用來檢查你所編寫的這些程序是否正確。
答:
首先完成pgdir_walk函數,函數原型 pgdir_walk(pde_t *pgdir, const void *va, int create),該函數的功能在注釋中解釋道:
給定一個頁目錄表指針 pgdir ,該函數應該返回線性地址va所對應的頁表項指針。
所以在這里我們應該完成以下幾個步驟:
1. 通過頁目錄表求得這個虛擬地址所在的頁表頁對於與頁目錄中的頁目錄項地址 dic_entry_ptr。(7-8)
2. 判斷這個頁目錄項對應的頁表頁是否已經在內存中。 (10)
3. 如果在,計算這個頁表頁的基地址page_base,然后返回va所對應頁表項的地址 &page_base[page_off] (23-25)
4. 如果不在則,且create為true則分配新的頁,並且把這個頁的信息添加到頁目錄項dic_entry_ptr中。(11-18)
5. 如果create為false,則返回NULL。(19-20)
代碼
1 pte_t * pgdir_walk(pde_t *pgdir, const void * va, int create) 2 { 3 unsigned int page_off; 4 pte_t * page_base = NULL; 5 struct PageInfo* new_page = NULL; 6 7 unsigned int dic_off = PDX(va); 8 pde_t * dic_entry_ptr = pgdir + dic_off; 9 10 if(!(*dic_entry_ptr & PTE_P)) 11 { 12 if(create) 13 { 14 new_page = page_alloc(1); 15 if(new_page == NULL) return NULL; 16 new_page->pp_ref++; 17 *dic_entry_ptr = (page2pa(new_page) | PTE_P | PTE_W | PTE_U); 18 } 19 else 20 return NULL; 21 } 22 23 page_off = PTX(va); 24 page_base = KADDR(PTE_ADDR(*dic_entry_ptr)); 25 return &page_base[page_off]; 26 }
接下來完成boot_map_region函數,函數原型 static void boot_map_region(pde_t *pgdir, uintptr_t va, size_t size, physaddr_t pa, int perm),這個函數的功能在注釋中被這樣解釋:
把虛擬地址空間范圍[va, va+size)映射到物理空間[pa, pa+size)的映射關系加入到頁表pgdir中。這個函數主要的目的是為了設置虛擬地址UTOP之上的地址范圍,這一部分的地址映射是靜態的,在操作系統的運行過程中不會改變,所以這個頁的PageInfo結構體中的pp_ref域的值不會發生改變。
這個函數要完成的步驟如下:
1. 需要完成一個循環,在每一輪中,把一個虛擬頁和物理頁的映射關系存放到響應的頁表項中。直到把size個字節的內存都分配完。
1 static void 2 boot_map_region(pde_t *pgdir, uintptr_t va, size_t size, physaddr_t pa, int perm) 3 { 4 int nadd; 5 pte_t *entry = NULL; 6 for(nadd = 0; nadd < size; nadd += PGSIZE) 7 { 8 entry = pgdir_walk(pgdir,(void *)va, 1); //Get the table entry of this page. 9 *entry = (pa | perm | PTE_P); 10 11 12 pa += PGSIZE; 13 va += PGSIZE; 14 15 } 16 }
接下來再繼續查看page_insert(),函數原型如下 page_insert(pde_t *pgdir, struct PageInfo *pp, void *va, int perm),功能上是完成:把一個物理內存中頁pp與虛擬地址va建立映射關系。
這個函數的主要步驟如下:
1. 首先通過pgdir_walk函數求出虛擬地址va所對應的頁表項。(4)
2. 修改pp_ref的值。(8)
3. 查看這個頁表項,確定va是否已經被映射,如果被映射,則刪除這個映射。(9-13)
4. 把va和pp之間的映射關系加入到頁表項中。(14-15)
代碼:
1 int 2 page_insert(pde_t *pgdir, struct PageInfo *pp, void *va, int perm) 3 { 4 pte_t *entry = NULL; 5 entry = pgdir_walk(pgdir, va, 1); //Get the mapping page of this address va. 6 if(entry == NULL) return -E_NO_MEM; 7 8 pp->pp_ref++; 9 if((*entry) & PTE_P) //If this virtual address is already mapped. 10 { 11 tlb_invalidate(pgdir, va); 12 page_remove(pgdir, va); 13 } 14 *entry = (page2pa(pp) | perm | PTE_P); 15 pgdir[PDX(va)] |= perm; //Remember this step! 16 17 return 0; 18 }
這里要注意,pp->pp_ref++這條語句,一定要放在page_remove之前,這是為了處理一種特殊情況:pp已經映射到va上了。至於為什么要這么做,大家可以思考一下。
接下來繼續完成page_lookup()函數,函數原型:struct PageInfo * page_lookup(pde_t *pgdir, void *va, pte_t **pte_store), 函數的功能為:
返回虛擬地址va所映射的物理頁的PageInfo結構體的指針,如果pte_store參數不為0,則把這個物理頁的頁表項地址存放在pte_store中。
這個函數的功能就很容易實現了,我們只需要調用pgdir_walk函數獲取這個va對應的頁表項,然后判斷這個頁是否已經在內存中,如果在則返回這個頁的PageInfo結構體指針。並且把這個頁表項的內容存放到pte_store中。
代碼:
1 struct PageInfo * 2 page_lookup(pde_t *pgdir, void *va, pte_t **pte_store) 3 { 4 pte_t *entry = NULL; 5 struct PageInfo *ret = NULL; 6 7 entry = pgdir_walk(pgdir, va, 0); 8 if(entry == NULL) 9 return NULL; 10 if(!(*entry & PTE_P)) 11 return NULL; 12 13 ret = pa2page(PTE_ADDR(*entry)); 14 if(pte_store != NULL) 15 { 16 *pte_store = entry; 17 } 18 return ret; 19 }
最后一個就是page_remove函數,它的原型是:void page_remove(pde_t *pgdir, void *va),功能就是把虛擬地址va和物理頁的映射關系刪除。
注釋里面還提示了要注意的幾個細節:
1. pp_ref值要減一
2. 如果pp_ref減為0,要把這個頁回收
3. 這個頁對應的頁表項應該被置0
代碼:
1 void 2 page_remove(pde_t *pgdir, void *va) 3 { 4 pte_t *pte = NULL; 5 struct PageInfo *page = page_lookup(pgdir, va, &pte); 6 if(page == NULL) return ; 7 8 page_decref(page); 9 tlb_invalidate(pgdir, va); 10 *pte = 0; 11 }
以上就是Lab2 Part1和Part2的分析。
歡迎大家的建議與提問~
zzqwf12345@163.com
