在開始做題之前,需要了解一下一些常用的函數,宏以及記憶體布局,建議複習一下LAB1中的簡單記憶體模型,LAB2預備知識中的相關。這裡有幾個很有用的位址變換工具,具體實作可以檢視<code>mmu.h</code>和<code>pmap.h</code>,提前掌握這些小工具對于了解位址變換和後續的程式編寫有很大幫助。
<col>
名稱
參數
作用
PADDR
核心虛拟位址kva
将核心虛拟位址kva轉成對應的實體位址
KADDR
實體位址pa
将實體位址pa轉化為核心虛拟位址
page2pa
頁資訊結構struct PageInfo
通過空閑頁結構得到這一頁起始位置的實體位址
pa2page
通過實體位址pa擷取這一頁對應的頁結構體struct PageInfo
page2kva
通過空閑頁結構得到這一頁起始位置的虛拟位址
PDX
線性位址la
獲得該線性位址la對應的頁目錄項索引
PTX
獲得該線性位址la在二級頁表中對應的頁表項索引
PTE_ADDR(pte)
頁表項或頁目錄項的值
獲得對應的頁表基址或者實體位址基址(低12位為0)
首先關于第一個函數<code>boot_alloc()</code>這是在記憶體管理機制還沒建立起來時,系統記憶體的配置設定函數。在page等建立以後當使用<code>page_alloc()</code>而不該再使用該函數。根據函數的注釋,先記錄目前的free指針,然後将free指針偏移n單元即可,注意記憶體對齊(使用ROUNDUP函數)。
第二個函數是初始化記憶體管理了,隻需要做到check_page_free_list(1)之前即可。
首先使用<code>i386_detect_memory</code>擷取實體記憶體大小;之後建立核心的頁目錄,使用的是<code>boot_alloc()</code>,大小是1頁(4KB);然後将核心頁目錄安裝到一個頁目錄項中;之後建立空閑實體頁數組pages。
<code>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);</code>
第三個函數,建立page相關的資料結構。首先哪些實體記憶體是free的?根據注釋,首先實體記憶體的第0頁需要被标記為已使用;IO-hole需要被标記為已使用,不能被配置設定出去;擴充位址包含核心地方不能被配置設定出去,剩下的空間就可标記為free并後續可以配置設定出去。
<code>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]; } } }</code>
第四個函數,是後續應該使用的記憶體配置設定函數<code>page_alloc</code>,根據前面我們知道,<code>page_free_list</code>指着第一個空閑頁,是以隻需要從這個連結清單上摘取一個下來即可。這裡通過前面的幾個函數或者宏,可以将<code>struct PageInfo</code>輕松地對應到實體位址或者虛拟位址。
<code>// 配置設定一個實體頁 // 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; }</code>
第五個函數,作用是釋放一個頁。也就是将一個<code>struct PageInfo</code>結構,重新挂回<code>page_free_list</code>。注意不能釋放一個引用值不為0的頁,或者連結值不為空的頁。
<code>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; }</code>
這一部分的前序知識,可以看上一篇文章Lab2記憶體管理準備知識。于是開始建立頁表管理。
第一個函數,用于給定一個頁目錄和虛拟位址,傳回對于的頁表項指針。就是一個通路二級頁表找值的過程,上一篇文章中詳細地寫到了。
<code>/* 給定一個指向頁目錄的指針,這個函數傳回 指向線性位址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); }</code>
第二個函數,建立起一段虛拟位址空間和實體位址空間的映射關系,也就是填充頁表的值。
<code>/* 将虛拟位址空間[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; } }</code>
第三個函數,查找一個虛拟位址對應的頁。
<code>/* 得到虛拟位址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)); }</code>
第四個函數,取消一個映射關系
<code>/* 取消虛拟位址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); } }</code>
第五個函數
<code>/* 将實體位址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; }</code>
繼續完善```mem_init()``
現在可以來一段總結了
這便是JOS目前建立起來的記憶體映射了,左側是實體位址空間,右邊是虛拟位址空間。比如說UVPT,在代碼中有這樣一段
而<code>PDX(UVPT)=1110 1111 01</code>是以位址區間<code>0xef400000~0xef7fffff</code>共計4MB被映射到<code>PADDR(kern_pgdir)</code>處。而正如JOS一開始所說,隻會使用256MB的記憶體,映射關系也滿足。
記憶體映射這塊,需要好好地閱讀代碼,文章中沒有詳細地列出JOS記憶體布局,虛拟記憶體的布局在<code>memlayout.h</code>中
為了更好地了解這部分,需要熟悉保護模式分頁模式下地尋址
要區分好實體位址和虛拟位址,頁表,頁目錄這些裡面裝地内容是什麼
要有一個記憶體模型總體上的概念