Lab2
Lab2 是關於操作系統存儲管理的細節。主要是建立內存模型,頁表,物理地址映射等。
在Lab2之前,請復習好前序知識:
Part1 物理內存管理
在開始做題之前,需要了解一下一些常用的函數,宏以及內存布局,建議復習一下LAB1中的簡單內存模型,LAB2預備知識中的相關。這里有幾個很有用的地址變換工具,具體實現可以查看mmu.h
和pmap.h
,提前掌握這些小工具對於理解地址變換和后續的程序編寫有很大幫助。
名稱 | 參數 | 作用 |
---|---|---|
PADDR | 內核虛擬地址kva | 將內核虛擬地址kva轉成對應的物理地址 |
KADDR | 物理地址pa | 將物理地址pa轉化為內核虛擬地址 |
page2pa | 頁信息結構struct PageInfo | 通過空閑頁結構得到這一頁起始位置的物理地址 |
pa2page | 物理地址pa | 通過物理地址pa獲取這一頁對應的頁結構體struct PageInfo |
page2kva | 頁信息結構struct PageInfo | 通過空閑頁結構得到這一頁起始位置的虛擬地址 |
PDX | 線性地址la | 獲得該線性地址la對應的頁目錄項索引 |
PTX | 線性地址la | 獲得該線性地址la在二級頁表中對應的頁表項索引 |
PTE_ADDR(pte) | 頁表項或頁目錄項的值 | 獲得對應的頁表基址或者物理地址基址(低12位為0) |
- 首先關於第一個函數
boot_alloc()
這是在內存管理機制還沒建立起來時,系統內存的分配函數。在page等建立以后當使用page_alloc()
而不該再使用該函數。根據函數的注釋,先記錄當前的free指針,然后將free指針偏移n單元即可,注意內存對齊(使用ROUNDUP函數)。
static void *
boot_alloc(uint32_t n)
{
static char *nextfree; // virtual address of next byte of free memory
char *result;
if (!nextfree) {
extern char end[];
nextfree = ROUNDUP((char *) end, PGSIZE);
}
// Allocate a chunk large enough to hold 'n' bytes, then update
// nextfree. Make sure nextfree is kept aligned
// to a multiple of PGSIZE.
// LAB 2: Your code here.
result = nextfree;
nextfree = ROUNDUP(result + n, PGSIZE);
return result;
}
-
第二個函數是初始化內存管理了,只需要做到check_page_free_list(1)之前即可。
首先使用
i386_detect_memory
獲取物理內存大小;之后創建內核的頁目錄,使用的是boot_alloc()
,大小是1頁(4KB);然后將內核頁目錄安裝到一個頁目錄項中;之后創建空閑物理頁數組pages。void mem_init(void) { uint32_t cr0; size_t n; i386_detect_memory(); ////////////////////////////////////////////////////////////////////// // create initial page directory. kern_pgdir = (pde_t *) boot_alloc(PGSIZE); memset(kern_pgdir, 0, PGSIZE); ////////////////////////////////////////////////////////////////////// // Permissions: kernel R, user R // UVPT是 User read-only virtual page table的虛擬地址 // PDX獲得頁目錄項索引 // 將內核頁目錄安裝到內核頁目錄中(參考前一篇文章中類似的搞法) /* ULIM, MMIOBASE-->+------------------------------+ 0xef800000 | Cur. Page Table (User R-) | R-/R- PTSIZE UVPT ---->+------------------------------+ 0xef400000 此處PTSIZE=4096*1024 =4MB 為一個頁目錄項能映射的內存大小 */ kern_pgdir[PDX(UVPT)] = PADDR(kern_pgdir) | PTE_U | PTE_P; // 分配pages數組,一共有npages個物理頁,每個頁使用struct PageInfo結構記錄,並填充0 // Your code goes here: size_t sizes = sizeof(struct PageInfo) * npages; pages = (struct PageInfo*)boot_alloc(sizes); memset(pages, 0, sizes); page_init(); check_page_free_list(1);
-
第三個函數,建立page相關的數據結構。首先哪些物理內存是free的?根據注釋,首先物理內存的第0頁需要被標記為已使用;IO-hole需要被標記為已使用,不能被分配出去;擴展地址包含內核地方不能被分配出去,剩下的空間就可標記為free並后續可以分配出去。
void page_init(void) { // npages_basemem :Amount of base memory (in pages) //第0頁不能被后續分配出去 pages[0].pp_ref = 1; pages[0].pp_link = NULL; size_t i; //內核的尾端所在的頁索引號(那物理地址進行計算) size_t kernel_end_page = PADDR(boot_alloc(0)) / PGSIZE; for (i = 1; i < npages; i++) { //IO-hole和內核部分不能被分配出去 if (i >= npages_basemem && i < kernel_end_page) { pages[i].pp_ref = 1; pages[i].pp_link = NULL; } else { //建立free物理頁鏈表 pages[i].pp_ref = 0; pages[i].pp_link = page_free_list; page_free_list = &pages[i]; } } }
-
第四個函數,是后續應該使用的內存分配函數
page_alloc
,根據前面我們知道,page_free_list
指着第一個空閑頁,因此只需要從這個鏈表上摘取一個下來即可。這里通過前面的幾個函數或者宏,可以將struct PageInfo
輕松地對應到物理地址或者虛擬地址。// 分配一個物理頁 // If (alloc_flags & ALLOC_ZERO) 用0填充該頁 // 不要增加頁引用數 // 鏈接域要設為NULL // 如果內存不夠了,返回NULL // Hint: use page2kva and memset struct PageInfo * page_alloc(int alloc_flags) { //page_free_list=NULL 說明沒有內存可供分配 if (page_free_list == NULL) { cprintf("page_alloc: out of free memory\n"); return NULL; } //摘下那一頁 struct PageInfo *addr = page_free_list; page_free_list = page_free_list->pp_link; addr->pp_link = NULL; if (alloc_flags & ALLOC_ZERO) { //得到這個info結構描述的那個物理頁的虛擬地址,才能使用memset memset(page2kva(addr), 0, PGSIZE); } //返回這個空閑頁的info結構 return addr; }
-
第五個函數,作用是釋放一個頁。也就是將一個
struct PageInfo
結構,重新掛回page_free_list
。注意不能釋放一個引用值不為0的頁,或者鏈接值不為空的頁。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. if (pp->pp_ref != 0 || pp->pp_link != NULL) { panic("page_free: can not free the memory"); return; } //掛入鏈表 pp->pp_link = page_free_list; page_free_list = pp; }
Part2 虛擬內存
這一部分的前序知識,可以看上一篇文章Lab2內存管理准備知識。於是開始建立頁表管理。
-
第一個函數,用於給定一個頁目錄和虛擬地址,返回對於的頁表項指針。就是一個訪問二級頁表找值的過程,上一篇文章中詳細地寫到了。
/* 給定一個指向頁目錄的指針,這個函數返回 指向線性地址va的頁表項的指針 這需要訪問二級頁表 對應的頁表不一定存在,如果create參數為false則直接返回NULL否則,該函數申請新的一頁來做頁表,並增 加頁的引用計數值。 */ pte_t * pgdir_walk(pde_t *pgdir, const void *va, int create) { // Fill this function in // 得到頁目錄索引對應的頁目錄項 pde_t *dir = pgdir + PDX(va); //檢查這一頁表是否存在 if (!(*dir & PTE_P)) { if (!create) return NULL; //申請新的一頁 struct PageInfo* pp = page_alloc(1); if (pp == NULL) return NULL; pp->pp_ref++; //得到這一頁的起始物理地址,並安裝到頁目錄中 *dir = page2pa(pp) | PTE_P | PTE_U | PTE_W; } // 頁表的起始物理地址轉為虛擬地址+在頁表項中的索引---->一個指向頁表項的指針 return (pte_t *) KADDR(PTE_ADDR(*dir)) + PTX(va); }
-
第二個函數,建立起一段虛擬地址空間和物理地址空間的映射關系,也就是填充頁表的值。
/* 將虛擬地址空間[va, va+size),映射到物理地址空間[pa, pa+size) 物理地址和虛擬地址都是頁對齊的。 映射的過程就是填頁表的過程。 */ static void boot_map_region(pde_t *pgdir, uintptr_t va, size_t size, physaddr_t pa, int perm) { // Fill this function in // 空間大小為多少頁(對齊) size_t pieces = ROUNDUP(size, PGSIZE) / PGSIZE; for (size_t i = 0; i < pieces; i++) { //得到這個虛地址對於的頁表項 pte_t *pte = pgdir_walk(pgdir, (void *) va, 1); if (pte == NULL) { panic("boot_map_region: out of memory!\n"); } //頁表項放上物理地址(頁的起始地址) *pte = pa | PTE_P | perm; //下一頁 va += PGSIZE; pa += PGSIZE; } }
-
第三個函數,查找一個虛擬地址對應的頁。
/* 得到虛擬地址va對應的頁結構,如果pte_store不為空,就存入這一頁的地址 va還沒有對應到某個頁,就返回NULL */ struct PageInfo * page_lookup(pde_t *pgdir, void *va, pte_t **pte_store) { // Fill this function in // 查找頁表項 pte_t *pte = pgdir_walk(pgdir, va, 0); // 沒有這個項 if (!pte || !(*pte & PTE_P)) { cprintf("page_lookup: can not find out the page.\n"); return NULL; } // 存儲記錄 if (pte_store) { *pte_store = pte; } // 得到頁的物理地址對應的PageInfo結構 return pa2page(PTE_ADDR(*pte)); }
-
第四個函數,取消一個映射關系
/* 取消虛擬地址va映射到的物理頁 物理頁的引用計數應該減少(為0是釋放) 這個地址對應的頁表項(如果有)應該清空 TLB失效 */ void page_remove(pde_t *pgdir, void *va) { // Fill this function in // pte_store會存入頁表項 pte_t *pte_store; struct PageInfo *pp = page_lookup(pgdir, va, &pte_store); // pp不為空說明有這一項 if (pp) { page_decref(pp); // 頁表項清空 *pte_store = 0; tlb_invalidate(pgdir, va); } }
-
第五個函數
/* 將物理地址pp映射到虛擬地址va 權限設置為 perm|PTE_P 如果va以及和一個物理地址關聯了,那么應該使用page_remove()並刷TLB pp所在的物理頁的引用計數增加 */ int page_insert(pde_t *pgdir, struct PageInfo *pp, void *va, int perm) { // Fill this function in // 對應的頁表項,申請新的頁如果需要 pte_t *pte = pgdir_walk(pgdir, va, 1); if (!pte) { return -E_NO_MEM; } pp->pp_ref++; //已經存在映射關系 if (*pte & PTE_P) { page_remove(pgdir, va); tlb_invalidate(pgdir, va); } //得到該頁的物理地址,並安裝進頁表 *pte = page2pa(pp) | PTE_P | perm; return 0; }
繼續完善```mem_init()``
void
mem_init(void)
{
/* ... ... */
check_page_free_list(1);
check_page_alloc();
check_page();
// pa:PADDR(pages)---->va:UPAGES size=PTSIZE
boot_map_region(kern_pgdir, UPAGES, PTSIZE, PADDR(pages), PTE_U);
// pa:PADDR(bootstack)---->va:KSTACKTOP - KSTKSIZE size=KSTKSIZE
boot_map_region(kern_pgdir, KSTACKTOP - KSTKSIZE, KSTKSIZE, PADDR(bootstack), PTE_W);
// pa:0---->va:KERNBASE size=0xffffffff - KERNBASE
boot_map_region(kern_pgdir, KERNBASE, 0xffffffff - KERNBASE, 0, PTE_W);
// Check that the initial page directory has been set up correctly.
check_kern_pgdir();
/* ... ... */
}
現在可以來一段總結了
這便是JOS目前建立起來的內存映射了,左側是物理地址空間,右邊是虛擬地址空間。比如說UVPT,在代碼中有這樣一段
kern_pgdir[PDX(UVPT)] = PADDR(kern_pgdir) | PTE_U | PTE_P;
而PDX(UVPT)=1110 1111 01
因此地址區間0xef400000~0xef7fffff
共計4MB被映射到PADDR(kern_pgdir)
處。而正如JOS一開始所說,只會使用256MB的內存,映射關系也滿足。
總結
- 內存映射這塊,需要好好地閱讀代碼,文章中沒有詳細地列出JOS內存布局,虛擬內存的布局在
memlayout.h
中 - 為了更好地理解這部分,需要熟悉保護模式分頁模式下地尋址Lab2內存管理准備知識
- 要區分好物理地址和虛擬地址,頁表,頁目錄這些里面裝地內容是什么
- 要有一個內存模型總體上的概念