MIT6.828 Lab2 記憶體管理

周小倫發表於2021-06-26

Lab2

0. 任務介紹

你將編寫一個記憶體管理程式碼。主要分為兩大部分。分別對實體記憶體和虛擬記憶體的管理。

  • 對於實體記憶體,每次分配記憶體分配器會為你分配4096bytes。也稱為一個頁(在大部分作業系統中一個頁的大小都是4B)你需要維護一個資料結構來記錄哪個物理頁是空閒的哪個物理頁是已被佔用的。以及有多少程式共享已分配的頁。並且你需要編寫程式來進行記憶體的分配和回收
  • 對於虛擬記憶體,它將核心和使用者軟體使用的虛擬地址對映到實體記憶體中的地址。 x86硬體的記憶體管理單元(MMU)在指令使用記憶體時執行對映,查閱一組頁面表。 您將根據我們提供的規範設定MMU的頁面表。

1. Part1: Physical Page Management

作業系統必須跟蹤哪部分實體記憶體是被使用的以及哪部分實體記憶體是空閒的。你需要在切入到虛擬記憶體之前完成這一操作,因為當使用虛擬記憶體的時候我們需要頁表來進行管理,而你需要為頁表分配實體記憶體。

下面的圖來自於[https://blog.csdn.net/qq_40871466/article/details/103922416]

img

你需要實現 kern/pmap.c的下列函式

boot_alloc()
mem_init() (only up to the call to `check_page_free_list(1)`)
page_init()
page_alloc()
page_free()

check_page_free_list() and check_page_alloc() test your physical page allocator. You should boot JOS and see whether check_page_alloc() reports success. Fix your code so that it passes.

1.1 實現boot_alloc

在lab1我們知道了pc的啟動過程。這裡是在核心執行的init.c中先呼叫了mem_init

根據我的除錯輸出(printf)我發現.bss的地址是 0xf01156a0這個地址在虛擬地址0xf0000000之上。我們知道核心的虛擬地址是在0xf0000000為起點的。隨後是核心的程式碼段+資料段然後就是.bss所以這裡bss大於核心虛擬地址起始位置是合理的.

boot_alloc就是在.bss之上分配製定大小為n的區域。注意這裡都是在虛擬記憶體地址下進行的操作

  1. 如果n為0則直接範圍nextfree的地址。也就是把.bss向上取整(為了都符合一頁一頁的儲存形式。分頁管理)
  2. 如果不為0則為他分配記憶體。其實就是把nextfree的地址往後移動 (n * PGSIZE)。不過我們要返回這段地址的起始地址。就相當於一個page陣列的起始地址
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[];
		cprintf("end is %08x\n",end);
		nextfree = ROUNDUP((char *) end, PGSIZE);
	}
	cprintf("nextfree is %08x\n",nextfree);
	// 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.
	if (n == 0) {
		return nextfree;
	} 
	//allocate
	result = nextfree;
	nextfree = ROUNDUP((char *)(nextfree + n), PGSIZE);
	return result;
}

1.2 實現mem_init

在mem_init中我們會呼叫兩次boot_alloc。第一次為了建立頁目錄。第二次則為了建立所有的物理頁表。分別為它們分配記憶體然後memset成0。

	// 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;
	cprintf("Page nubmer %d\n",npages);
	//////////////////////////////////////////////////////////////////////
	// 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:
	//	all 32768 number pages
	pages = (struct PageInfo *) boot_alloc(sizeof(struct PageInfo) * npages);
	memset(pages, 0, sizeof(struct PageInfo) * npages);

1.3 實現page_alloc

這裡給了我們示例程式碼。不過這裡把所有的pages都初始化了成了0和可用。這顯然是不合理的

  1. 根據下圖和實驗中給的提示。base_memory(也就是 1mb + extended_memoy)這一段 。的low memory是可以被分配成use的

    但是注意最下面的一個page不可以(實驗中有提到儲存真實模式的一些資訊)

    ![](/Users/zhouxiaolun/Library/Application Support/typora-user-images/image-20210623224201539.png)

  2. 第二就是extended memory裡會存有核心的資訊。我們要找到核心的結束位置,然後給剩餘部分進行初始化

    這裡就可以簡單利用boot_alloc(0)。因為這個會返回核心的結束位置對應的虛擬地址。

    但是我們要找的是這個虛擬地址定於的實體地址在哪個page中。也就是要找到它在pages陣列中的標號。

    這裡實驗給我們提供了一個巨集定義page2kva即可獲得它對應的實體地址。然後除頁表大小就可以獲得對應的標號

    好了程式碼已經呼之欲出了

    	cprintf("npages_basemem is %d\n",npages_basemem);
    	cprintf("pages addr is %0x8 \n", pages);
    	size_t i;
    	//all low memeory is free expect 0 page
    	for (i = 1; i < npages_basemem; i++) {
    		cprintf("pages addr is %0x8 \n", pages[i]);
    		pages[i].pp_ref = 0;
    		pages[i].pp_link = page_free_list;
    		page_free_list = &pages[i];
    	}
    	i = PADDR(boot_alloc(0)) / PGSIZE;
    	for (; i < npages; i++) {
    		pages[i].pp_ref = 0;
    		pages[i].pp_link = page_free_list;
    		page_free_list = &pages[i];
    	}
    }
    

    1.4 實現page_free

    這個就比較簡單了。只需要簡單的把要釋放的頁加入到free_page_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.
    	if (pp->pp_ref  || pp->pp_link) {
    		panic("no shoule page free");
    	}
    	//head insert 
    	pp -> pp_link = page_free_list;
    	page_free_list = pp;
    }
    

2. Part2: Virtual Memory

跳過中間的一些廢話,直接開始exercise4。這裡要求我們編寫程式碼來管理頁面表。要插入和刪除linear-to-physical(其實就是虛擬地址和實體地址之間的mappings,並按需分配頁。

這個是JOS所用的32位虛擬地址的分佈

img

下面就是虛擬地址的翻譯過程。這個學過os的應該非常熟悉了吧。這裡的page_dirpage_table其實就是一個二級頁表。的多級索引非常簡單。

整體過程就是我們先通過CR3暫存器找到PAGE_DIR所在的位置,然後通過虛擬地址的前10位在PAGE_DIR中獲取到下一級頁表,也就是PAGE_TABLE的地址。隨後通過虛擬地址的12-21這10位去找到對應的PAGE_FRAME的地址。從裡面獲取到ppa的地址結合OFFSET就可以得到最終的實體地址了。

img

好了搞清楚大概邏輯之後,下面開始完成第二部分的程式碼

2.1 pgdir_walk

這個程式碼是後面四個程式碼的基礎,因此一定要小心認真,這裡的意思就是說給你pgdir的地址。和虛擬地址va你要返回一個指向pte的指標。pte就是頁表條目在最後一層頁表對應位置處。

  1. 首先我們通過幾個巨集定義把虛擬地址分解
  2. 如果page_dir對應的PTE_P也就是有效位為0的話則表明相關的頁表條目並不存在
  3. 如果不存在的話需要通過create標記來判斷是否需要建立對應的頁
  4. 這裡根據提示我們需要把新建立頁的實體地址儲存在對應的page_dir位置處
  5. 然後就是在page_table中找到對應的PTE返回
pte_t *
pgdir_walk(pde_t *pgdir, const void *va, int create)
{
	// Fill this function in
	uintptr_t dir = PDX(va); //表示對應的page_dir索引
	uintptr_t page = PTX(va); // 表示對應的page_table索引
	uintptr_t offset = PGOFF(va); // 表示在page中對應的頁內偏移
	pde_t dir_entry = pgdir[dir]; // 首先要判斷這個虛擬地址是否有對映
	if (!(dir_entry & PTE_P)) {
		if (create) {
			// allocate
			struct PageInfo *newPage = page_alloc(ALLOC_ZERO);
            if (newPage == NULL) {
                // allocation failed
                return NULL;
            }
			newPage->pp_ref++;
			pgdir[dir]  = (pde_t)page2pa(newPage)|PTE_P|PTE_U|PTE_W;
		} else {
			return NULL;
		}
	}
	//
	pte_t *ptab = (pte_t *)KADDR(PTE_ADDR(pgdir[dir]));
	return &ptab[page];
}

2.2 boot_map_region

這個函式的實現就比較簡單了。要求是把虛擬地址[va, va+size)對映到實體地址[pa, pa+size],使用許可權位為perm|PTE_P,只是在UTOP上方做靜態對映,所以不能修改pp_ref欄位.

基本上就是通過上面實現的pgdir_walk函式找到給定虛擬地址對應的pte。然後修改pte條目即可

static void
boot_map_region(pde_t *pgdir, uintptr_t va, size_t size, physaddr_t pa, int perm)
{
	// Fill this function in
	uintptr_t start = 0;
	for ( ; start < size; start += PGSIZE, va += PGSIZE, pa += PGSIZE) {
		pte_t *pte = pgdir_walk(pgdir,(void *) va, 1);
		*pte = pa | perm | PTE_P;
	}
}

2.3 Page_lookup

返回一個虛擬地址va對映的頁面。如果pte_store不是0,那麼將對應的頁表項地址存到pte_store的地址裡(用於結果返回)。如果沒有頁面對映在va那麼返回NULL。提示:使用pgdir_walk和pa2page

struct PageInfo *
pgee_lookup(pde_t *pgdir, void *va, pte_t **pte_store)
{
	// Fill this function in
	pte_t *pte = pgdir_walk(pgdir,(void *) va, 0);
	if (!pte) {
		return NULL;
	}
	if (*pte && !(*pte & PTE_P)) {
		return NULL;
	}
	if (pte_store) {
		*pte_store = pte;
	}
	struct PageInfo* page = pa2page(PTE_ADDR(*pte));
	return page;
}

基本上通過給的提示就可以實現這個函式

2.4 Page_remove

也是通過提示。移除給定的va對應的對映。

  1. 如果給定的va在頁表中沒有對應對映則直接返回。
  2. 否則把對應的pte清0,然後呼叫tlb_invalidatepage_decref
void
page_remove(pde_t *pgdir, void *va)
{
	// Fill this function in
	pte_t * pte;
	struct PageInfo *page = page_lookup(pgdir,va,&pte);
	if (!page) {
		return;
	}
	*pte = 0;
	tlb_invalidate(pgdir,va);
	page_decref(page);
}

2.5 Page_insert

把物理頁pp對映在虛擬地址va,頁表項許可權設定為perm|PTE_P。

  1. 如果已經有一個頁面在va,它應該先呼叫page_remove()刪除
  2. 如有必要,應按需分配頁表並將其插入“ pgdir”。插入成功pp->ref應該自增。如果以前有頁面位於“ va”,則TLB必須無效。
  3. 提示:使用pgdir_walk,page_remove和page2pa。
  4. 根據提示我們需要分配頁表並將其插入pgdir。這不是就是前面實現的pgir_walk把crate設定成1的功能。
  5. 同樣如果以前有頁面位於va。則讓他的tlb無效。。這裡聽起來很麻煩,但實際上只需要呼叫page_remove原來va對應的對映移除即可。而且page_remove已經實現了讓tlb無效的操作。
int
page_insert(pde_t *pgdir, struct PageInfo *pp, void *va, int perm)
{
	// Fill this function in
	pte_t *pte = pgdir_walk(pgdir,(void *) va, 1);
	if (!pte) {
		return -E_NO_MEM;
	}
	pp->pp_ref++;
	if (*pte & PTE_P) {
		page_remove(pgdir,va);
	}
	*pte = page2pa(pp) | perm | PTE_P;
	return 0;
}

3. Part3 : Kernel Address Space

第三部分需要我們補齊mem_init函式

只要跟隨提示來完成對於核心部分的一些對映。按照下面這樣做就好了

boot_map_region(kern_pgdir, UPAGES, PTSIZE, PADDR(pages), PTE_U);
boot_map_region(kern_pgdir, KSTACKTOP - KSTKSIZE, KSTKSIZE, PADDR(bootstack), PTE_W);
boot_map_region(kern_pgdir, KERNBASE, 0xffffffff - KERNBASE, 0, PTE_W);

3.1 Question

補充完第三部分的程式碼之後,我們來看一下第三部分的問題

  1. What entries (rows) in the page directory have been filled in at this point? What addresses do they map and where do they point? In other words, fill out this table as much as possible:

    Entry Base Virtual Address Points to (logically):
    1023 0xff000000 Page table for top 4MB of phys memory
    1022 ? ?
    959 0xefc00000 cpu0's kernel stack(0xefff8000),cpu1's kernel stack(0xeffe8000)
    956 0xef000000 npages of PageInfo(0xef000000)
    952 0xee000000 bootstack
    2 0x00800000 Program Data & Heap
    1 0x00400000 Empty
    0 0x00000000 [see next question]

    這個地方要參考一下memlayout.h就可以寫出了

    其實主要搞清楚幾個重要的就可以了

    比如0xef000000表示UPAGES

    oxefc00000表示核心棧等等

  2. We have placed the kernel and user environment in the same address space. Why will user programs not be able to read or write the kernel's memory? What specific mechanisms protect the kernel memory?

    通過把頁表項中的 Supervisor/User位置0,那麼使用者態的程式碼就不能訪問記憶體中的這個頁。

  3. What is the maximum amount of physical memory that this operating system can support? Why?

    這個作業系統利用一個大小為4MB的空間也就是UPAGES這一段。來存放所有的頁的PageInfo結構體資訊,每個結構體的大小為8B,所以一共可以存放512K個PageInfo結構體,所以一共可以出現512K個物理頁,每個物理頁大小為4KB,自然總的實體記憶體佔2GB。

  4. How much space overhead is there for managing memory, if we actually had the maximum amount of physical memory? How is this overhead broken down?

    這個問題是說如果我們現在的物理頁達到最大,那麼管理這些記憶體所需要的額外空間開銷有多少

    首先我們所有的pageinfo需要4mb。然後需要存放頁目錄表。一共1024個每一個需要4B所以一共4kb

    還有存放當前的頁表。頁表是1024 * 4kb = 4mb

    所以一共需要4MB + 4MB + 4KB = 8MB + 4KB

  5. Revisit the page table setup in kern/entry.S and kern/entrypgdir.c. Immediately after we turn on paging, EIP is still a low number (a little over 1MB). At what point do we transition to running at an EIP above KERNBASE? What makes it possible for us to continue executing at a low EIP between when we enable paging and when we begin running at an EIP above KERNBASE? Why is this transition necessary?

    1. 是通過下面的程式碼來跳轉到kernbase之上的虛擬地址的

    mov $relocated, %eax

    jmp *%eax

    1. 是因為我們把[0,4mb]和[KernalBASE,KERNALBASR + 4MB]這段的虛擬地址都對映到了0-4MB的實體地址上,因此無論EIP在高位還是在低位都可以執行。必須這樣做的原因是,如果只對映高位地址。在我們剛開啟分頁模式之後就會crash。

      因為剛開始我們訪問的還是地位地址。是通過jump來跳轉到高位》

4. Part4 : Challenge

//TODO

相關文章