mit6.828 - lab2筆記

toso發表於2024-05-07

lab2的實驗手冊帶著我們學習作業系統是如何處理記憶體管理的。lab2將記憶體管理劃分為 實體記憶體管理、頁表管理、核心地址空間劃分三個部分。

lab2的學習目標:重點學習記憶體管理的相關知識,包括記憶體佈局、頁表結構、頁對映
lab2的學習任務:完成記憶體管理的相關程式碼

在lab2中,完全可以跟著實驗手冊的節奏走,逐步完善記憶體管理的程式碼。不過在根據註釋補充各個函式的時候,大機率是懵逼的,需要自己多看幾遍,總結

環境準備:

image.png

實驗 2 包含以下新的原始檔:

  • inc/memlayout.h
  • kern/pmap.c
  • kern/pmap.h
  • kern/kclock.h
  • kern/kclock.c

memlayout.h pmap.h 定義了 PageInfo 結構,用於跟蹤哪些實體記憶體頁是空閒的。
kclock.ckclock.h 操作 PC 的電池時鐘和 CMOS RAM 硬體,其中 BIOS 記錄了 PC 所含的實體記憶體量等資訊。
pmap.c 中的程式碼需要讀取這些裝置硬體,以計算出實體記憶體的容量,但這部分程式碼已經為你完成:你不需要了解 CMOS 硬體工作的細節。
請特別注意 memlayout.h pmap.h,因為本實驗要求您使用並理解其中包含的許多定義。您可能還需要檢視 inc/mmu.h,因為其中包含的許多定義對本實驗也很有用。


Part1:實體記憶體管理(練習1)

作業系統必須跟蹤實體記憶體中哪些是空閒記憶體,哪些是當前正在使用的記憶體。JOS 以頁面粒度管理 PC 的實體記憶體,這樣它就可以使用 MMU 來對映和保護每一塊已分配的記憶體。

現在你將編寫物理頁面分配器來實現實體記憶體管理。它透過 struct PageInfo 物件的連結串列來跟蹤哪些頁面是空閒的,每個物件對應一個物理頁面。我們要做的就是透過 PageInfo連結串列,實現對實體記憶體的申請、釋放。

練習 1. 在 kern/pmap.c 檔案中,您必須實現以下函式的程式碼(可能按照給出的順序)。
boot_alloc()
mem_init()(只呼叫到 check_page_free_list(1))。
page_init()
page_alloc()
page_free()
check_page_free_list() 和 check_page_alloc() 對物理頁面分配器進行測試。你應該啟動 JOS 並檢視 check_page_alloc() 是否報告成功。修改程式碼,使其透過測試。你可能會發現新增自己的 assert()s 來驗證你的假設是否正確很有幫助。

為了逐步理解物理頁面分配器的工作原理,我們就按照練習1的要求,逐個實現程式碼就好了,不過在那之前,我們來看一下lab1 最後的實體記憶體的情況和虛擬地址空間的對映情況:

image.png

boot_alloc

只在初始化時使用,用來確定申請n子節記憶體後後,空閒記憶體的首地址(虛擬記憶體空間)是多少。
為了讓JOS能夠追蹤空閒記憶體的首地址究竟是多少,這裡使用一個全域性變數 nextfree 來記錄.

static void *
boot_alloc(uint32_t n)
{
	static char *nextfree;	// virtual address of next byte of free memory
	char *result;

	// nextfree 一開始的值應該是多少?當然是kernel.ld中的標號end所指的位置,即核心載入進記憶體後的尾部
	if (!nextfree) {
		extern char end[]; 
		nextfree = ROUNDUP((char *) end, PGSIZE);
	}

	// 分配一塊足夠放下n個位元組的地址塊,然後將這個地址塊的地址返回。
	// 注意地址塊必須按照 PGSIZE 對齊
	result = nextfree;
	// 更新nextfree
	nextfree = ROUNDUP((char *)result + n, PGSIZE);

	return result;
}

完事了之後,按照 練習1 的指引,我們看一眼 mem_init

mem_init

mem_init 是用來初始化記憶體管理的函式。物存管理的部分在前面被處理,大致工作流程為:

  1. 獲取硬體資訊,記憶體有多大,i386_detect_memory();
  2. 初始化頁目錄 kern_pgdir
  3. 初始化pages陣列

// Set up a two-level page table:
//    kern_pgdir is its linear (virtual) address of the root
//
// This function only sets up the kernel part of the address space
// (ie. addresses >= UTOP).  The user part of the address space
// will be set up later.
//
// From UTOP to ULIM, the user is allowed to read but not write.
// Above ULIM the user cannot read or write.
void
mem_init(void)
{
	uint32_t cr0;
	size_t n;

	// Find out how much memory the machine has (npages & npages_basemem).
	i386_detect_memory();

	// Remove this line when you're ready to test this function.
	// panic("mem_init: This function is not finished\n");

	//////////////////////////////////////////////////////////////////////
	// create initial page directory.
	kern_pgdir = (pde_t *) boot_alloc(PGSIZE);
	memset(kern_pgdir, 0, PGSIZE);

	//////////////////////////////////////////////////////////////////////
	// Recursively insert PD in itself as a page table, to form
	// a virtual page table at virtual address UVPT.
	// (For now, you don't have understand the greater purpose of the
	// following line.)

	// Permissions: kernel R, user R
	kern_pgdir[PDX(UVPT)] = PADDR(kern_pgdir) | PTE_U | PTE_P;

	//////////////////////////////////////////////////////////////////////
	// Allocate an array of npages 'struct PageInfo's and store it in 'pages'.
	// The kernel uses this array to keep track of physical pages: for
	// each physical page, there is a corresponding struct PageInfo in this
	// array.  'npages' is the number of physical pages in memory.  Use memset
	// to initialize all fields of each struct PageInfo to 0.
	// Your code goes here:
	pages =(struct PageInfo *) boot_alloc(sizeof(struct PageInfo)*npages);
	memset(pages, 0, sizeof(struct PageInfo) * npages);

	//////////////////////////////////////////////////////////////////////
	// Now that we've allocated the initial kernel data structures, we set
	// up the list of free physical pages. Once we've done so, all further
	// memory management will go through the page_* functions. In
	// particular, we can now map memory using boot_map_region
	// or page_insert
	page_init();

	check_page_free_list(1);
	check_page_alloc();
	check_page();
	//....
}

在完成了 mem_init刀 page_init 之前的程式碼後,整理一下目前的實體記憶體和 虛擬地址空間的對映情況:

image.png
目前我們對 PageInfo 的瞭解還不足夠,在研究page_init之前,有必要學習下 struct PageInfo 的具體細節。

struct PageInfo

先來看看 PageInfo 這個結構體,這個註釋真棒。

/*
 * 頁面描述符結構,對映到 UPAGES。
 * 核心可讀寫,使用者程式只讀。
 *
 * 每個結構 PageInfo 儲存一個物理頁面的後設資料。
 *  它不是物理頁面本身,但物理頁面和結構 PageInfo 之間有一一對應的關係。
 * 您可以使用 kern/pmap.h 中的 page2pa() 將結構 PageInfo * 對映到相應的實體地址。
 */
struct PageInfo {
	//空閒列表中的下一頁。
	struct PageInfo *pp_link;

	// pp_ref 是指向此頁的指標(通常是頁表條目)的計數。
	// 對於使用 page_alloc 分配的頁面,pp_ref 是指向該頁面的指標計數(通常在頁表項中)。
	// 在啟動時使用 pmap.c 的boot_alloc 分配的頁面沒有有效的引用計數字段。

	uint16_t pp_ref;
}

如註釋所述, PageInfo 和實體記憶體是一一對應的,一個PageInfo 對應一頁實體記憶體(4KB),可以從 page2pa 這個對映函式中看出來

image.png

對於一個實體地址 pa ,將其右移 12 位,然後就可以作為 pages 陣列的下標了。
也就是說 :
pages[0] 對應 pa 0x0000_0000 到 0x0000_1000
pages[1] 對應 pa 0x0000_1000 到 0x0000_2000

image.png

pmap.h 中還有很多好用的函式和宏,除了這個 pa2page 還有 PADDR、KADDR等,可以先看一看,理解下。

理解了 struct PageInfo 的結構和對映方法,可以來看 page_init 了。

page_init

page_init 初始化了 pages 陣列,註釋給的相當詳盡了。按照上面mem_init總結的圖寫,可以參照 lab1筆記 中的記憶體佈局和 memlayout.h 中關於 IOPHYSMEM、EXTPHYSMEM 的定義寫。

//  初始化頁面結構和記憶體空閒列表。
// 完成後,永遠不要再使用 boot_alloc。 只使用下面的頁面分配器函式來分配和取消分配實體記憶體。
// 透過 page_free_list 分配和刪除實體記憶體。
//
void
page_init(void)
{
	// 這裡的示例程式碼將所有物理頁面標記為空閒。
	// 但實際情況並非如此。 哪些記憶體是空閒的?
	//  1) 將物理頁 0 標記為使用中。
	//     這樣,我們就可以保留實際模式 IDT 和 BIOS 結構,以備不時之需。 (目前還不需要,但是......)。
	//     
	//  2) 其餘的基本記憶體 [PGSIZE, npages_basemem * PGSIZE)是空閒的。
	//     
	//  3) 然後是 IO 孔 [IOPHYSMEM, EXTPHYSMEM),它必須永遠不會被分配。
	//     
	//  4) 然後是擴充套件記憶體 [EXTPHYSMEM, ...)  其中一些在使用中,一些是空閒的。
	//     核心在實體記憶體的哪裡?哪些物理頁已經用於頁表和其他資料結構?
	//     
	// 修改程式碼以反映這一點。
	// 注意:切勿實際觸及與空閒頁面對應的實體記憶體!
	// 
	size_t i;
	//物理頁 0 標記為使用中
	pages[0].pp_ref = 1;

	for(int i = 1; i<PGNUM(IOPHYSMEM); i++){
		pages[i].pp_ref = 0;
		pages[i].pp_link = page_free_list;
		page_free_list = &pages[i];
	}
	//然後是 IO 孔 [IOPHYSMEM, EXTPHYSMEM),它必須永遠不會被分配。
	for(int i = PGNUM(IOPHYSMEM); i<PGNUM(EXTPHYSMEM); i++){
		pages[i].pp_ref = 1;
	}
	//獲取當前空閒的記憶體首地址 cur_free_paddr(實體記憶體)
	physaddr_t cur_free_paddr = PADDR(boot_alloc(0));
	//[EXTPHYSMEM, cur_free_paddr) 中的擴充套件記憶體在使用中
	for(int i = PGNUM(EXTPHYSMEM); i<PGNUM(cur_free_paddr); i++){
		pages[i].pp_ref = 1;
	}
	//[cur_free_paddr, ...] 之後的實體記憶體目前是空閒的
	for(int i = PGNUM(cur_free_paddr); i<npages; i++){
		pages[i].pp_ref = 0;
		pages[i].pp_link = page_free_list;
		page_free_list = &pages[i];
	}
}

page_alloc

從pageinfo 空閒連結串列中摘下一個,並返回,細節見註釋:

//
// 分配一個物理頁面。 如果(alloc_flags & ALLOC_ZERO),則用“\0 ”位元組填充返回的整個物理頁
// 不要增加頁的引用,page_alloc 的呼叫者負責增加頁面的引用(顯式地或透過 page_insert)。

//
// 務必將已分配頁面的 pp_link 欄位設定為 NULL,以便page_free 可以檢查是否存在雙重引用。
// 
//
// 如果沒有可用記憶體,則返回 NULL。
//
// Hint: use page2kva and memset
struct PageInfo *
page_alloc(int alloc_flags)
{
	// Fill this function in
	if(page_free_list == NULL){
		return NULL;
	}
	//取出一個空閒 pageinfo
	struct PageInfo * pp = page_free_list;
	page_free_list = page_free_list->pp_link;
	pp->pp_link = NULL;
	//置零
	if(alloc_flags & ALLOC_ZERO){
		void* pp_kv = page2kva(pp);
		memset(pp_kv, 0, PGSIZE);
	}
	
	return pp;
}

page_free

//
// 返回一個頁面到空閒列表。
// (只有當 pp->pp_ref 達到 0 時才呼叫此函式)。
//
void
page_free(struct PageInfo *pp)
{
	//  填充此函式
	// 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: pp->pp_ref is nonzero or pp->pp_link is not NULL.");
	}
	pp->pp_link = page_free_list;
	page_free_list = pp;
}

到此為止,練習1就算完成了,然後測試一下

image.png

沒毛病


Part2:虛擬記憶體

Part2的核心是頁表管理,經過Part1我們可以完成了對 PageInfo 連結串列 page_free_list 和 陣列 pages 的維護。其中pages 儲存了所有的Pageinfo物件,page_free_list則記錄了空閒的物理頁。透過連結串列能夠更方便的分配和釋放,透過陣列可以輕鬆的完成PageInfo到其對應的物理頁地址的對映。

在進行這一節的學習前,需要了解關於 x86 保護模式的知識。主要是關於分段和頁轉換方面的,練習2的內容即學習 80386手冊中的相關章節。這部分手冊寫的很好,這裡簡單翻譯了一下,已經瞭解了的話直接跳到 2.2頁表管理

2.1 前置知識

練習 2. 如果您還沒有閱讀《英特爾 80386 參考手冊》
[《英特爾 80386 參考手冊》](https://pdos.csail.mit.edu/6.828/2018/readings/i386/toc.htm)第 5 章和第 6 章,請閱讀這兩章。仔細閱讀有關頁面轉換和基於頁面的保護的章節(5.2 和 6.4)。我們建議你也略讀一下有關分段的章節;雖然 JOS 使用分頁硬體來實現虛擬記憶體和保護,但在 x86 上無法禁用分段轉換和基於分段的保護,因此你需要對其有基本的瞭解。

關於虛擬地址、線性地址、實體地址

在 x86 術語中,虛擬地址 virtual address 由段選擇器和段內偏移量組成。線性地址 linear address是在段轉換後、頁轉換前得到的地址。實體地址 physical address 是經過段和頁轉換後最終得到的地址,也是最終透過硬體匯流排到達 RAM 的地址。


           Selector  +--------------+         +-----------+
          ---------->|              |         |           |
                     | Segmentation |         |  Paging   |
Software             |              |-------->|           |---------->  RAM
            Offset   |  Mechanism   |         | Mechanism |
          ---------->|              |         |           |
                     +--------------+         +-----------+
            Virtual                   Linear                Physical

C 指標是虛擬地址的 “偏移 ”部分。在 boot/boot.S 中,我們安裝了全域性描述符表 (GDT),透過將所有段基址設定為 0 和將限制設定為 0xffffffffff,有效地禁用了段轉換。因此,“選擇器 ”不起作用,線性地址始終等於虛擬地址的偏移量。在實驗 3 中,我們將不得不與分段進行更多互動,以設定許可權級別,但至於記憶體轉換,我們可以在整個 JOS 實驗中忽略分段,而只關注頁面轉換。

記得在實驗室 1 的第 3 部分中,我們安裝了一個簡單的頁表,這樣核心就可以在 0xf0100000 的連結地址上執行,儘管它實際上是載入在 ROM BIOS 上方 0x00100000 的實體記憶體中。這個頁表只對映了 4MB 的記憶體。在本實驗室為 JOS 設定的虛擬地址空間佈局中,我們將擴充套件虛擬地址空間佈局,以對映從虛擬地址 0xf0000000 開始的前 256MB 實體記憶體,並對映虛擬地址空間的其他一些區域。

練習 3. 雖然 GDB 只能透過虛擬地址訪問 QEMU 的記憶體,但在設定虛擬記憶體時檢查實體記憶體往往很有用。檢視實驗工具指南中的 QEMU 監視器命令,尤其是 xp 命令,它可以讓你檢查實體記憶體。要訪問 QEMU 監視器,請在終端按下 Ctrl-a c(同樣的繫結返回序列控制檯)。

使用 QEMU 監視器中的 xp 命令和 GDB 中的 x 命令檢查相應實體地址和虛擬地址的記憶體,確保看到的資料相同。

我們的 QEMU 補丁版本提供的 info pg 命令也很有用:它顯示了當前頁表的緊湊而詳細的資訊,包括所有對映的記憶體範圍、許可權和標誌。Stock QEMU 還提供了 info mem 命令,可顯示虛擬地址的對映範圍和許可權概覽。

從 CPU 上執行的程式碼來看,一旦我們進入保護模式(在 boot/boot.S 中首先進入),就無法直接使用線性地址或實體地址。所有記憶體引用都被解釋為虛擬地址,並由 MMU 進行轉換,這意味著 C 語言中的所有指標都是虛擬地址。

例如,在實體記憶體分配器中,JOS 核心經常需要將地址作為不透明值或整數來處理,而不對其進行取消引用。這些地址有時是虛擬地址,有時是實體地址。為了幫助記錄程式碼,JOS 原始碼區分了這兩種情況:uintptr_t 型別代表不透明的虛擬地址,physaddr_t 代表實體地址。這兩種型別實際上只是 32 位整數(uint32_t)的同義詞,因此編譯器不會阻止你將一種型別賦值給另一種型別!由於它們都是整數型別(而不是指標),如果你試圖取消引用,編譯器會發出抱怨。

JOS 核心可以透過先將 uintptr_t 轉換為指標型別來取消引用 uintptr_t。相比之下,核心無法合理地取消引用實體地址,因為 MMU 會翻譯所有記憶體引用。如果將 physaddr_t 轉換為指標型別並取消引用,也許可以載入和儲存到得到的地址(硬體會將其解釋為虛擬地址),但很可能得不到想要的記憶體位置。

C type Address type
T* Virtual
uintptr_t Virtual
physaddr_t Physical

JOS 核心有時需要讀取或修改只知道實體地址的記憶體。例如,向頁表新增對映可能需要分配實體記憶體來儲存頁目錄,然後初始化該記憶體。但是,核心無法繞過虛擬地址轉換,因此無法直接載入和儲存到實體地址。JOS 從虛擬地址 0xf0000000 的實體地址 0 開始重對映所有實體記憶體的原因之一,就是幫助核心讀寫只知道實體地址的記憶體。為了將實體地址轉換為核心可以實際讀寫的虛擬地址,核心必須在實體地址上加上 0xf0000000,才能在重對映區域找到相應的虛擬地址。你應該使用 KADDR(pa) 來完成這一加法。

有時,JOS 核心還需要根據核心資料結構所在記憶體的虛擬地址來查詢實體地址。核心全域性變數和由 boot_alloc() 分配的記憶體位於核心載入區域,從 0xf0000000 開始,也就是我們對映所有實體記憶體的區域。因此,要將該區域的虛擬地址轉換為實體地址,核心只需減去 0xf0000000。你應該使用 PADDR(va) 來做減法。

參考計數

在今後的實驗中,您經常會同時在多個虛擬地址(或多個環境的地址空間)中對映同一個物理頁。您將在與物理頁對應的結構 PageInfopp_ref 欄位中記錄每個物理頁的引用次數。當某個物理頁面的引用次數為零時,就可以釋放該頁面,因為它已不再被使用。一般來說,這個計數應該等於物理頁在所有頁表中出現在 UTOP 以下的次數(UTOP 以上的對映大多是由核心在啟動時設定的,永遠不會被釋放,所以沒有必要對它們進行引用計數)。我們還將用它來記錄指向頁面目錄頁面的指標數量,以及頁面目錄對頁面表頁面的引用數量。

使用 page_alloc 時要小心。它返回的頁面的引用計數始終為 0,因此一旦對返回的頁面進行了操作(例如將其插入頁表),就應該立即遞增 pp_ref。有時會由其他函式處理(例如 page_insert),有時則必須由呼叫 page_alloc 的函式直接處理。

2.2 頁表管理

Part2 虛擬記憶體的關鍵就在於頁表管理。Part1 我們透過 pages 實現了 PageInfo 物件和真實實體記憶體的 1對1 對映。而頁表管理則是實現 頁表項(pte)和 PageInfo 的 n對1 對映。沒錯,對於同一個頁目錄(pgdir,pde組成的陣列)來說,一個PageInfo可能被多個pte記錄下來(所以PageInfo有個 ref 成員)。

為了實現這樣的頁表管理,練習4帶著我們編寫一套例程來管理頁表:插入和刪除線性到物理對映,並在需要時建立頁表頁面。

練習 4. 在 kern/pmap.c 檔案中,您必須實現以下函式的程式碼。

        pgdir_walk()
        boot_map_region()
        page_lookup()
        page_remove()
        page_insert()
	
從 mem_init() 呼叫的 check_page(),用於測試頁表管理例程。在繼續執行之前,應確保它報告成功。

我在做這個練習的時候發現頁表管理的 page_insert 和 page_remove 與物存管理的 page_alloc 和 page_free 有點容易混淆。這時候要記住,物存管理的 page_alloc 和 page_free處理的是 pageinfo 到物理頁的對映,是1對1對映,我們只要維護page_free_list和pages陣列即可。
而頁表管理的 page_insert 和 page_remove 處理的是 pte 到 pageinfo的對映,是n對1對映,我們需要維護雙層頁表結構,以及PageInfo的ref。

pgdir_walk()

給定 pgdir,返回va對應的pte的指標。按照頁表的結構,需要:

  1. 透過va的高10位在pgdir中找 pde,
  2. 透過pde的高20位找到對應的 pte_table
  3. 透過va的次高10位,在pte_table 中找 pte
    因為 mem_init 的時候,我們實際上只有兩個 pte_table。因此上面第2步可能會發現,pte_table還不存在,需要page_alloc。這是唯二需要做pageinfo計數遞增操作的地方。另一個計數遞增的地方發生在 page_insert中。
// 給定指向頁面目錄指標 “pgdir”,pgdir_walk 返回指向線性地址 “va ”的頁表項 (PTE) 的指標。
// 這需要走兩級頁表結構。相關的頁表頁可能還不存在。
// 如果不存在,且 create == false,則 pgdir_walk 返回 NULL。
// 否則,pgdir_walk 將使用 page_alloc 分配一個新的頁表頁。
//    - 如果分配失敗,pgdir_walk 返回 NULL。
//    - 否則,新頁面的引用計數被遞增,頁面被清空,pgdir_walk 返回一個指向新頁表頁面的指標。
//
// Hint 1: 可以使用 kern/pmap.h 中的 page2pa() 將 PageInfo * 轉換為其引用頁的實體地址。
// Hint 2: x86 MMU 同時檢查頁目錄和頁表中的許可權位,因此頁目錄中的許可權比嚴格需要的許可權更大是安全的。
// Hint 3: 檢視 inc/mmu.h,獲取用於操作頁表和頁目錄項的有用宏。
//
pte_t *
pgdir_walk(pde_t *pgdir, const void *va, int create)
{
	// Fill this function in
	// 查詢va對應的頁目錄項(PDE)
	pde_t * pde_ptr = &pgdir[PDX(va)]; //用陣列的形式寫更容易理解
	// PTE_P沒有置位,說明pde_ptr對應頁表(pte_table)沒有分配
	if (!(*pde_ptr & PTE_P)) {
		if(create){//如果對應的pte_table的記憶體空間還沒有申請,則申請空間,並填寫pde
			struct PageInfo *pp = page_alloc(ALLOC_ZERO);//申請一個物理頁
			if(pp == NULL){//如果沒有物理頁了,直接返回NULL
				return NULL;
			}
			pp->pp_ref ++;
			//將物理頁的“實體地址”填寫至pde
			*pde_ptr = page2pa(pp) | PTE_U |PTE_W|PTE_P;
		}else{
			return NULL;
		}
	}
	//PTE_ADDR(*pde_ptr)取pde的前20位,含義是va對應的pte所在pte_talbe的*物理*地址
	//注意啊,是實體地址,在這裡必須使用KADDR宏,因為程式碼在執行時會將地址當做虛擬地址處理,即要經過頁轉換
	pte_t *pte_table = KADDR(PTE_ADDR(*pde_ptr));
	pte_t * pte_ptr = &pte_table[PTX(va)]; //PTX(va)取va的中間10位,含義是va對應的pte在頁表中的索引
	return pte_ptr;
}

boot_map_region

//
// 將虛擬地址空間的 [va, va+size) 對映到以 pgdir 為根的頁表中的實體地址空間 [pa, pa+size)。 
// 大小是 PGSIZE 的倍數,va 和 pa 都是頁面對齊的。
// 對條目使用許可權位 perm|PTE_P。
//
// 該函式僅用於設定 UTOP 以上的 “靜態 ”對映。因此,它不應**改變對映頁面上的 pp_ref 欄位。
//
// Hint: the TA solution uses pgdir_walk
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 page_num = size / PGSIZE;    
	if (size % PGSIZE != 0) {
		page_num++;
	}                            //計算總共有多少頁
	for (int i = 0; i < page_num; i++) {
		//獲取 va 在pgdir中的pte
		pte_t *pte_ptr = pgdir_walk(pgdir, (void *)va, 1);//獲取va對應的PTE的地址
		if (pte_ptr == NULL) {
			panic("boot_map_region(): out of memory\n");
		}
		*pte_ptr = pa | PTE_P | perm; //將pa填寫到 va對應的pte中
		pa += PGSIZE;             //更新pa和va,進行下一輪迴圈
		va += PGSIZE;
	}
}

page_insert

// 在虛擬地址 “va ”處對映物理頁 “pp”。
// 頁表項的許可權(低 12 位)應設定為 “perm|PTE_P”。
// should be set to 'perm|PTE_P'.
//
// Requirements
//   - 如果'va'處已經對映了一個頁面,則應將其 page_remove()。
//   - 如有必要,應按要求分配一個頁表並插入到'pgdir'中。
//   - 如果插入成功,pp->pp_ref 應遞增。
//   -  如果'va'中以前存在頁面,則必須使 TLB 失效。
//
// Corner-case hint: 請務必考慮在同一 pgdir 中的同一虛擬地址重新插入同一 pp 時會發生什麼情況。
// 不過,儘量不要在程式碼中區分這種情況,因為這經常會導致微妙的錯誤;
// 有一種優雅的方法可以在一條程式碼路徑中處理所有問題。
//
// RETURNS:
//   0 on success
//   -E_NO_MEM, if page table couldn't be allocated
//
// Hint: The TA solution is implemented using pgdir_walk, page_remove,
// and page2pa.
//
int
page_insert(pde_t *pgdir, struct PageInfo *pp, void *va, int perm)
{
	// Fill this function in
	pte_t * pte_ptr = pgdir_walk(pgdir, va, 1);

	if(pte_ptr == NULL){
		return -E_NO_MEM;
	}

	pp->pp_ref ++;
	if((*pte_ptr) & PTE_P){
		page_remove(pgdir, va);
	}
	physaddr_t pa = page2pa(pp);

	*pte_ptr = pa | PTE_P | perm;

	pgdir[PDX(va)] |= perm;

	return 0;
}

page_lookup

// 返回對映到虛擬地址 “va ”的 struct PageInfo。
// 如果 pte_store 不為零,我們將在其中儲存該頁面的 pte 地址。 
// page_remove 使用了這個地址,它可以用來驗證系統呼叫引數的頁面許可權,但大多數呼叫者不應該使用它。
// 
// 如果在 va 處沒有頁面對映,則返回 NULL。
//
// Hint: the TA solution uses pgdir_walk and pa2page.
//
struct PageInfo *
page_lookup(pde_t *pgdir, void *va, pte_t **pte_store)
{

	pte_t * pte_ptr = pgdir_walk(pgdir, va, 0);
	if(pte_ptr == NULL||!(*pte_ptr & PTE_P)){
		return NULL;
	}
	physaddr_t pa = PTE_ADDR(*pte_ptr);
	struct PageInfo * pp = pa2page(pa);

	if(pte_store != NULL){
		*pte_store = pte_ptr;
	}
	return pp;
}

page_remove

// 解對映虛擬地址 “va ”處的物理頁。
// 如果該地址處沒有物理頁,則什麼也不做。
//
// Details:
//   - 物理頁面上的 refcount 應該遞減。
//   - 如果 refcount 為 0,則釋放物理頁。
//   - 對應於'va'的 pg 表項應設定為 0。
//     (如果存在這樣的 PTE)
//   - 如果從頁表中刪除一個條目,TLB 必須失效。
//
//
// Hint: The TA solution is implemented using page_lookup,
// 	tlb_invalidate, and page_decref.
//
void
page_remove(pde_t *pgdir, void *va)
{
	// Fill this function in
	pte_t * pte_ptr = NULL;
	struct PageInfo * pp = page_lookup(pgdir, va, &pte_ptr);
	if(pp == NULL){
		return ;
	}
	*pte_ptr = 0;
	page_decref(pp);
	tlb_invalidate(pgdir, va);
}

到此為止練習4算是結束了。


Part3:核心地址空間

經過Part2,我們擁有了將虛擬地址va對映到實體地址pa的能力(以及取消對映的能力)。Part3我們利用Part2的基礎設施,對32位線性地址空間進行規劃,將地址空間劃分為核心區域(高地址)和使用者區域(低地址)。分界線由 inc/memlayout.h 中的 ULIM 符號定義,為核心保留了約 256MB 的虛擬地址空間。

在Lab2,我們先只考慮執行一個程序的情況。在後面完整的JOS,每個程序都會有一個頁表,其中核心部分而頁表內容都一樣,使用相同的虛擬地址,且都對映同一塊實體地址;
使用者部分使用相同的虛擬記憶體空間,但是對映的實體地址則不同,是各自的程式碼。為了實現這一目標,需要進行。

許可權和故障隔離

對於每一個程序而言,JOS的核心和使用者空間處在同一個地址空間中。為了限制使用者程式碼只能訪問使用者記憶體區域,需要使用PTE的標誌位,標記使用者可以訪問的記憶體區域。
要知道,核心的程式碼是由我們完成的,我們可以控制,但是作業系統還要載入使用者的程式碼,作為核心的開發者,我們當然不希望每次載入的使用者的程式碼破壞掉核心資料,導致崩潰。也不洗某個程序竊取其他程序的資料。

具體來說:

  • 使用者程序沒有許可權訪問 ULIM 以上的任何記憶體,而核心則可以讀寫這些記憶體。
  • 對於地址範圍 [UTOP,ULIM),核心和使用者環境的許可權相同:可讀、不可寫。該地址範圍用於向使用者環境公開某些核心資料結構的只讀許可權。
  • UTOP 以下的地址空間供使用者環境使用;使用者環境將設定訪問該記憶體的許可權。

image.png

初始化核心地址空間

inc/memlayout.h 顯示了你應該使用的佈局。你將使用剛才編寫的函式來設定適當的線性到物理對映。

練習 5. 填寫呼叫 check_page() 後 mem_init() 中缺失的程式碼。

現在,您的程式碼應能透過 check_kern_pgdir() 和 check_page_installed_pgdir() 檢查。
	//////////////////////////////////////////////////////////////////////
	// Now we set up virtual memory

	//////////////////////////////////////////////////////////////////////
	// 將 “頁面 ”對映到線性地址 UPAGES 的使用者只讀位置
	// 許可權:
	// UPAGES 處的新映像 -- 核心 R、使用者 R
	// (即 perm = PTE_U | PTE_P)
	// - 頁面本身 -- 核心 RW,使用者 NONE
	// 你的程式碼放在這裡:
	boot_map_region(kern_pgdir, UPAGES, PTSIZE, PADDR(pages), PTE_U);

	//////////////////////////////////////////////////////////////////////
	// 使用 “bootstack ”所指的實體記憶體作為核心堆疊。 核心堆疊從虛擬地址 KSTACKTOP 開始向下增長。
	// 我們認為從 [KSTACKTOP-PTSIZE, KSTACKTOP) 開始的整個範圍都是核心堆疊,但將其分成兩部分:
	// * [KSTACKTOP-KSTKSIZE, KSTACKTOP) -- 由實體記憶體支援
	// * [KSTACKTOP-PTSIZE, KSTACKTOP-KSTKSIZE) -- 沒有實體記憶體支援;
	// 所以如果核心堆疊溢位,它將出錯而不是覆蓋記憶體。 被稱為 “保護頁”。
	// 許可權:核心 RW,使用者 NONE
	// 你的程式碼放在這裡:
	boot_map_region(kern_pgdir, KSTACKTOP-KSTKSIZE, KSTKSIZE, PADDR(bootstack), PTE_W);
	//////////////////////////////////////////////////////////////////////
	// 將所有實體記憶體對映到 KERNBASE。
	// 也就是說,VA 範圍 [KERNBASE, 2^32) 應該對映到 PA 範圍 [0, 2^32 - KERNBASE)
	// 我們可能沒有 2^32 - KERNBASE 位元組的實體記憶體,但我們還是設定了對映。
	// 許可權:核心 RW,使用者 NONE
	// Your code goes here:
	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();

其實,直到目前為止,我們使用的頁表還是 lab1 中那個entrypgdir.c 中寫死的頁表,在lab1中,我們忽略了這個頁表的解釋,現在可以回來看一看了。

image.png

所以說,目前的實體記憶體和虛擬地址空間的對映關係如下:

image.png

可以看到,在Part1中,實體記憶體增加了 kern_pgidr和pages。在part2中由於一些記憶體對映操作(pgdir_walk操作可能為儲存新的pte_table而插入物理頁),實體記憶體增加了一些pte_table。
但是左邊,地址對映卻沒有變化。

這是因為,上面的操作完全是對 kern_pgdir這個pde陣列做的修改,以及對插入的新pte_table們的修改,我們還沒有用修改CR3暫存器,載入我們新的頁表 kern_pgdir。

讓我們繼續將 mem_init 的最後一段看完,

// 從 entry_pgdir 切換到我們剛剛建立的完整 kern_pgdir 頁表。	
// 我們的指令指標現在應該位於 KERNBASE 和 KERNBASE+4MB 之間的某個位置,這兩個頁表的對映方式相同。
	lcr3(PADDR(kern_pgdir));

	check_page_free_list(0);

	// entry.S set the really important flags in cr0 (including enabling
	// paging).  Here we configure the rest of the flags that we care about.
	cr0 = rcr0();
	cr0 |= CR0_PE|CR0_PG|CR0_AM|CR0_WP|CR0_NE|CR0_MP;
	cr0 &= ~(CR0_TS|CR0_EM);
	lcr0(cr0);

	// Some more checks, only possible after kern_pgdir is installed.
	check_page_installed_pgdir();

LCR3指令將kern_pgdir載入後,實體記憶體和虛擬地址空間的對映如下:

image.png

相關文章