ucore lab2
練習0:填寫已有實驗
使用可視化diff/mege工具meld可以輕松完成填寫代碼的任務。只需要注意lab 2對lab 1中的文件進行了修改,不能把lab 1中的代碼照搬過去。
練習1:實現first-fit連續物理內存分配算法
物理地址空間的探查
在實現物理內存的分配之前必須先探查出當前物理內存的布局和大小,根據這些信息計算出操作系統可操控的內存大小並將其划分為等大的物理頁。
物理地址空間的探查在bootloader中完成,探查出的信息存放在物理地址0x8000,,C程序使用結構體struct e80map
對這些數據進行操作。
// 定義在kern/mm/memlayout.h中
struct e820map {
int nr_map; //探查到的內存塊總數
struct {
uint64_t addr; //內存塊的起始物理地址
uint64_t size; //內存塊的大小
uint32_t type; //內存塊的類型
} __attribute__((packed)) map[E820MAX];
};
物理頁的初始化
探查到的內存使用物理地址描述,ucore設置了物理內存地址到虛擬內存地址的臨時映射關系:
虛擬地址 = 物理地址 + 0xC0000000
ucore支持的最大物理地址空間是KMEMSIZE(0x380000000)
。
空閑塊以物理頁(4096字節)為為單位,空閑塊中的第一個物理頁代表整個空閑塊,每一個物理頁都對應着一個記錄它的結構struct Page
:
struct Page {
int ref; // page frame's reference counter
uint32_t flags; // array of flags that describe the status of the page frame
unsigned int property; // the num of free block, used in first fit pm manager
list_entry_t page_link; // free list link
};
-
當
flags
設置為Reserved
時,該物理頁被硬件保留,操作系統無法使用 -
當
flags
未被設置為Reserved
且未被設置為Property
時,該物理頁已被操作系統分配 -
當
flags
未被設置為Reserved
且設置為Property
時,該物理頁空閑 -
當物理頁是空閑塊的第一個物理頁時,
property
生效,代表該空閑塊的大小(以物理頁為單位)
物理頁的初始化的核心在於:將探查到的物理內存與相應的Page
關聯起來。
物理頁的初始化由kern/mm/pmm.c中的page_init()
和init_memmap()
完成。page_init
負責確定探查到的物理內存塊與對應的struct Page
之間的映射關系,Page
的初始化工作由init_memmap()
調用內存管理器pmm_manager
的init_memmap()
方法完成。
page_init()
的算法為:
- 遍歷
memmap
指針指向的結構e820map
中的元素,求出其中的最高物理地址,計算出可用地址總大小 - 計算出所需的
Page
結構,並順序存放在程序的.bss
段之上,形成以物理地址的PPN為索引的Page
數組 - 再次遍歷
e820map
結構,將所有的Page
設置為Reserved
,調用init_memmap
初始化操作系統可用的且地址在支持范圍內物理內存對應的Page
結構
init_memmap
的算法為:
- 將所有可用的
Page
的flags
設置為Property
,引用計數設置為0,property
設置為0,初始化page_link
- 空閑塊的第一個物理塊的
property
設置為該空閑塊的大小,將其加入到空閑鏈表末尾
default_init_memmap(struct Page *base, size_t n) {
assert(n > 0);
struct Page *p = base;
for (; p != base + n; p ++) {
// 在查找可用內存並分配struct Page數組時就已經將將全部Page設置為Reserved
// 將Page標記為可用的:清除Reserved,設置Property,並把property設置為0( 不是空閑塊的第一個物理頁 )
assert(PageReserved(p));
p->flags = p->property = 0;
SetPageProperty(p);
set_page_ref(p, 0);
list_init(&(p->page_link));
}
cprintf("Page address is %x\n", (uintptr_t)base);
base->property = n;
nr_free += n;
list_add(free_list.prev, &(base->page_link));
}
只有操作系統可使用的物理頁使用該函數進行初始化,因此不能被使用的物理頁的Page
中的Reserved
屬性不會被修改,確保了所有的物理頁都有正確的Page
。
e820map
結構中數組map
中的內存塊是從低地址從高地址排列的,設置內存塊對應的Page
時都是插入到鏈表的尾部,因此最后形成的空閑鏈表是從低地址到高地址的有序鏈表。
為了便於內存塊的邊界,我的實現要求空閑塊中只有第一個物理頁的property
域不為0,其他物理頁的property
必須為0。
初始化完成后的物理內存布局:
+-------------+-------------+--------------+------------+------------------------+
| | e820map | | Pages | Free memory |
+-------------+-------------+--------------+------------+------------------------+
^ ^ ^ ^ ^
| | | | |
0 0x8000 .bss段結束位置(end) freemem 0x380000000
物理頁的分配
設計目標:
- 能標識請求失敗的原因,便於調試
- 每次分配的內存塊都是空閑鏈表中的最低地址空閑塊
- 正確處理空閑塊分割、
static struct Page *
default_alloc_pages(size_t n) {
assert(n > 0);
/* There are not enough physical memory */
if (n > nr_free) {
warn("memory shortage");
return NULL;
}
struct Page *page = NULL;
struct Page *p = NULL;
list_entry_t *le = &free_list;
/* try to find empty space to allocate */
while ((le = list_next(le)) != &free_list) {
p = le2page(le, page_link);
if (p->property >= n) {
page = p;
break;
}
}
/* external fragmentation */
if (page == NULL) {
warn("external fragmentation: There are enough memory, but can't find continuous space to allocate");
return NULL;
}
unsigned int property = page->property;
/* modify pages in allocated block(except of first page)*/
p = page + 1;
for (; p < page + n; ++p) {
ClearPageProperty(p);
// property is zero, so we needn't modify it.
}
/* modify first page of allcoated block */
ClearPageProperty(page);
page->property = n;
nr_free -= n;
/*
* If block size is bigger than requested size, split it;
* */
if (property > n) {
p = page +
p->property = property - n;
list_add_after(&(page->page_link), &(p->page_link));
}
list_del(&(page->page_link));
return page;
}
物理頁的回收
default_free_pages(struct Page *page, size_t n)
設計目標:
- 能夠正確地處理參數:當1
n
超過已分配塊的頁數時,回收整個塊;當n
小於也分配塊的頁數時,回收n頁
,剩下的內存不回收;當base
指向的內存塊不是已分配塊的起始地址時,從base
開始回收 - 能夠正確的合並空閑塊(我的實現回收在
base
塊高地址的所有相鄰空閑塊) - 能夠正確分割已分配塊:
default_free_pages
要求回收已分配塊中的任意頁,剩下的未回收的部分作為新的已分配塊 - 在回收后,空閑鏈表仍然是有序的
思路:
- 根據
base
和n
合理分割欲回收的內存塊 - 合並鄰接與
base
塊的空閑塊 - 將代表新空閑塊的
page_link
插入到有序空閑鏈表中
static void
default_free_pages(struct Page *base, size_t n) {
assert(n > 0);
/* if @base is not the beginning of the alloacted block which @base points in,
* change the #property filed of the allocated block.
*/
/* find the beginning of the allocated block.
* only begging page's #property fild is non-zero.
*/
struct Page *begin = base;
size_t count = 0;
for ( ; begin->property == 0; ++count, --begin) {
assert(!PageReserved(begin) && !PageProperty(begin));
}
/* If @base is not the beginning of the allocated block,
* split the allocated block into two part.
* One part is @begin to @base,
* other part is @base to the end of the original part.
*/
if (begin != base) {
base->property = begin->property - count;
begin->property = count;
}
/* If @n is bigger than the number of pages in the @base block,
* it is not an error, just free all pages in block.
*/
if (n > base->property) {
n = base->property;
}
/* If @n is smaller than the number of pages in @base block,
* split @base block into two block.
*/
else if (n < base->property) {
(base + n)->property = base->property - n;
base->property = n;
}
/* modify status information */
struct Page *p = base;
for (; p != base + n; ++p) {
assert(!PageReserved(p) && !PageProperty(p));
p->flags = 0;
SetPageProperty(p);
}
// extern struct Page *pages;
// struct Page *pages_end = pages + npage;
// unsigned int property, old_base_property = base->property;
// list_entry_t *pos = NULL; //insert new free block after @pos
// /* merge free blocks next to current freeing block */
// p = base + base->property;
// while ((p < pages_end) && PageProperty(p)) {
// property = p->property;
// pos = (p->page_link).prev;
// base->property += p->property;
// p->property = 0;
// list_del(&p->page_link);
// p += property;
// }
// /* merge free blocks before current freeing block */
// p = base - 1;
// while ((p >= pages)) {
// while (p->property == 0) {
// --p;
// if ((p < pages) || (p->property != 0)) break;
// }
// if ((p >= pages) && (p->property != 0)) {
// p->property += base->property;
// base->property = 0;
// base = p;
// pos = (p->page_link).prev;
// list_del(&p->page_link);
// }
// }
// /* There is no free blocks adjcent to @base block. */
// if (base->property == old_base_property) {
// list_entry_t *le = &free_list;
// while ((le = list_next(le)) != &free_list) {
// if (le2page(le, page_link) > base) {
// pos = le->prev;
// break;
// }
// }
// /* free list is empty or @base points to the upmost free block */
// if (le == &free_list)
// pos = free_list.prev;
// }
// list_add(pos, &base->page_link);
// nr_free += n;
/* merge adjcent free blocks */
list_entry_t *le = list_next(&free_list), *pos = free_list.prev, *merge_before_ptr = NULL;
unsigned int old_base_property = base->property;
/* merge free blocks */
while (le != &free_list) {
p = le2page(le, page_link);
/* free_list is ascending sorted, only one free block before @base block will be merged */
if ((p + p->property == base)) {
p->property += base->property;
base->property = 0;
base = p;
pos = le->prev;
merge_before_ptr = le;
list_del(le);
}
if ((base + base->property) == p) {
base->property += p->property;
p->property = 0;
pos = le->prev;
list_del(le);
}
le = list_next(le);
}
/* if there may be free blocks before @base block, try to merge them */
if (merge_before_ptr != NULL) {
le = merge_before_ptr->prev;
while (le != &free_list) {
p = le2page(le, page_link);
if (p + p->property == base) {
p->property += base->property;
base->property = 0;
base = p;
pos = le->prev;
list_del(le);
}
le = list_prev(le);
}
}
/* @pos indicate position in whith @base's page_link should insert;
* only when there are no adjcent free blocks, should we try to find insertion position
*/
if (base->property == old_base_property) {
le = list_next(&free_list);
while (le != &free_list) {
if (le2page(le, page_link) > base) {
assert((base + base->property) < le2page(le, page_link));
pos = le->prev;
break;
}
le = list_next(le);
}
}
list_add(pos, &base->page_link);
nr_free += n;
}
運行結果:
缺陷
- 空閑鏈表是升序的,從合並空閑塊時從鏈表頭開始遍歷,最多只能夠合並一個在
base
塊之前(低地址)的空閑塊。為了將base
塊之前的空閑塊全部合並,不得不在第一次合並后,從base
塊之前的空閑塊向鏈表頭遍歷。 - 使用鏈表,分配、合並都要遍歷鏈表,時間復雜度為O(n)。可以使用平衡二叉樹替代鏈表,將時間復雜度降低到O(n*logn)。
default_check
有bug
//
風格的注釋中的代碼功能和下面未加注釋的代碼功能相同,但是注釋中的代碼通過查找欲回收的塊相鄰的Page
的flags
域來查找、合並空閑塊。這段代碼是正確的,卻無法通過default_check
測試。
default_check
無法修改內存布局,只能臨時篡改空閑鏈表,制造出沒有可用內存或只有特定數目的可用內存的假象。當實現不通過空閑鏈表查找鄰接空閑塊時,就會“看穿”測試代碼制造的假象,發現並合並空閑塊、修改怕property
域,測試檢查對應的property
時就會出錯。
這個實現應該是正確的,但是因為default_check
自身的缺陷,無法通過完整地通過default_check
,只能通過basic_check
及之前的檢查。
無法通過的default_check
代碼如下(之后的代碼進行的是相同的檢查):
struct Page *p0 = alloc_pages(5), *p1, *p2;
assert(p0 != NULL);
assert(!PageProperty(p0));
list_entry_t free_list_store = free_list; //制造沒有可用內存的假象,但是鄰接空閑塊仍然存在
list_init(&free_list);
assert(list_empty(&free_list));
assert(alloc_page() == NULL);
unsigned int nr_free_store = nr_free;
nr_free = 0;
free_pages(p0 + 2, 3); //只回收一部分內存。該內存塊高地址處有相鄰的空閑塊
assert(alloc_pages(4) == NULL);
assert(PageProperty(p0 + 2) && p0[2].property == 3);
assert((p1 = alloc_pages(3)) != NULL);
assert(alloc_page() == NULL);
assert(p0 + 2 == p1);
測試用例分配了大小為5頁的內存,但只回收其中第3頁及之后的內存。
回收時,注釋中的代碼發現在欲回收的塊之上(更高地址)的Page
的flags
被設置為PG_Property
,這表明存在鄰接空閑塊,所以將這三頁和其上的空閑塊合並成了一個空閑塊,p0[2]
是這個新空閑塊的第一頁,property
域被修改成新空閑塊的頁數。因此PageProperty(p0+2)
為真,但p0[2].property == 3
為假,測試失敗。
練習2:實現尋找虛擬地址對應的頁表項
頁目錄項和頁表項中每個組成部分的含義及對ucore而言的潛在用處(還有很多不清楚的地方!!!)
intel手冊還沒讀到詳細介紹,有很多不清楚的地方,等讀到了再補。
ucore使用的是頁大小為 4K 的 32-bit paging,頁目錄項和頁表項結構如下如下:
頁目錄項:
- bit 0(P): resent 位,若該位為 1 ,則 PDE 存在,否則不存在。
- bit 1(R/W): read/write 位,若該位為 0 ,則只讀,否則可寫。
- bit 2(U/S): user/supervisor位。
- bit 3(PWT): page-level write-through,若該位為1則開啟頁層次的寫回機制。
- bit 4(PCD): page-level cache disable,若該位為1,則禁止頁層次的緩存。
- bit 5(A): accessed 位,若該位為1,表示這項曾在地址翻譯的過程中被訪問。
- bit 6: 該位忽略。
- bit 7(PS): 這個位用來確定 32 位分頁的頁大小,當該位為 1 且 CR4 的 PSE 位為 1 時,頁大小為4M,否則為4K。
- bit 11:8: 這幾位忽略。
- bit 32:12: 頁表的PPN(頁對齊的物理地址)。
頁表項:
頁表項除了第 7 , 8 位與 PDE 不同,其余位作用均相同。
- bit 7(PAT): 如果支持 PAT 分頁,間接決定這項訪問的 4 K 頁的內存類型;如果不支持,這位保留(必須為 0 )。
- bit 8(G): global 位。當 CR4 的 PGE 位為 1 時,若該位為 1 ,翻譯是全局的;否則,忽略該位。
其中被忽略的位可以被操作系統用於實現各種功能;和權限相關的位可以用來增強ucore的內存保護機制;access 位可以用來實現內存頁交換算法。
出現頁訪問異常時,硬件執行的工作(待續...)
intel手冊還沒讀到這部分內容,等讀到了再補。
get_pte
函數的實現
get_pte
函數的語義為:根據頁目錄pgdir
來獲取或創建指向線性地址la
的 PTE ,是否創建頁表取決於create
。
步驟:
- 計算
la1
對應的 PDE 地址。 - 若該 PDE 不存在(PTE 所在的頁表不存在)且
create
為 不為 0 ,創建頁表並設置 PTE。 - 若該 PDE 不存在且
create
為 0 ,返回NULL
。 - 若該 PDE 存在,直接返回 PTE 虛擬地址。
PTE 內容的設置是調用者的職責,get_pte
只需要給調用者一個可訪問的 PTE即可。
pte_t *
get_pte(pde_t *pgdir, uintptr_t la, bool create) {
assert(pgdir != NULL);
struct Page *struct_page_vp; // virtual address of struct page
uint32_t pdx = PDX(la), ptx = PTX(la); // index of PDE, PTE
pde_t *pdep, *ptep;
pte_t *page_pa; // physical address of page
pdep = pgdir + pdx;
ptep = (pte_t *)KADDR(PDE_ADDR(*pdep)) + ptx;
// if PDE exists
if (test_bit(0, pdep)) {
return ptep;
}
/* if PDE not exsits, allocate one page for PT and create corresponding PDE */
if ((!test_bit(0, pdep)) && create) {
struct_page_vp = alloc_page(); // allocate page for PT
assert(struct_page_vp != NULL); // allocate successfully
set_page_ref(struct_page_vp, 1); // set reference count
page_pa = (pte_t *)page2pa(struct_page_vp); // convert virtual address to physical address
ptep = KADDR(page_pa + ptx); // virtual address of PTE
*pdep = (PADDR(ptep)) | PTE_P | PTE_U | PTE_W; // set PDE
memset(ptep, 0, PGSIZE); // clear PTE content
return ptep;
}
return NULL;
練習3:釋放某虛地址所在的頁並取消對應二級頁表項的映射
Page
數組元素與頁目錄項、頁表項的對應關系
直到現在,ucore還沒有實現進程,所以暫時認為只有一個頁目錄。頁目錄定義在kern/mm/entry.s
中,不是通過alloc_page()
創建的,所以
Page
數組元素與頁目錄項、頁表項存在對應關系。所有的物理頁都有一個描述它的Page
結構。所有的頁表都是通過alloc_page()
分配的,每個頁表項都存放在一個Page
結構描述的物理頁中;如果 PTE 指向某物理頁,同時也有一個Page
結構描述這個物理頁。
(1)可以通過 PTE 的地址計算其所在的頁表的Page
結構,(2)可以通過 PTE 指向的物理地址計算出該物理頁對應的Page
結構。
- (1): 將虛擬地址向下對齊到頁大小,換算成物理地址(減
KERNBASE
), 再將其右移PGSHIFT
(12)位獲得在pages
數組中的索引PPN
,&pages[PPN]
就是所求的Page
結構地址。 - (2): PTE 按位與
0xFFF
獲得其指向頁的物理地址,再右移PGSHIFT
(12)位獲得在pages
數組中的索引PPN
,&pages[PPN]
就 PTE 指向的地址對應的Page
結構。
C代碼如下:
// (1) this function don't exist in ucore
struct Page* page_for_pte(pde_t *ptep) {
return &pages[PPN(PADDR(ROUNDDOWN(ptep, PGSIZE))))
}
// (2) this function exists in ucore
static inline struct Page *
pte2page(pte_t pte) {
if (!(pte & PTE_P)) {
panic("pte2page called with invalid pte");
}
return pa2page(PTE_ADDR(pte));
}
讓虛擬地址等於物理地址的方法
ucore 設置虛擬地址到物理地址的映射分為兩步:
- lab 2 中 ucore的入口點
kern_entry()
(定義在 kern/init/entry.s)中,設置了一個臨時頁表,將虛擬地址 KERNBASE ~ KERNBASE + 4M 映射到物理地址 0 ~ 4M ,並將 eip 修改到對應的虛擬地址。ucore 所有代碼和本實驗操作的所有數據結構(Page
數組)都在這個虛擬地址范圍內。 - 在確保程序可以正常運行后,調用
boot_map_segment(boot_pgdir, KERNBASE, KMEMSIZE, 0, PTE_W);
將虛擬地址KERNBASE ~ KERNBASE + KMEMSIZE。
因為在編譯鏈接時 ld 腳本 kern/tools/kernel.ld設置鏈接地址(虛擬地址),代碼段基地址為0xC0100000(對應物理地址0x00100000),必須將該地址修改為0x00100000以確保內核加載正確。
/* Load the kernel at this address: "." means the current address */
/* . = 0xC0100000; */
. = 0x00100000;
.text : {
*(.text .stub .text.* .gnu.linkonce.t.*)
}
在第1步中,ucore 設置了虛擬地址 0 ~ 4M 到物理地址 0 ~ 4M 的映射以確保開啟頁表后kern_entry
能夠正常執行,在將 eip 修改為對應的虛擬地址(加KERNBASE
)后就取消了這個臨時映射。因為我們要讓物理地址等於虛擬地址,所以保留這個映射不變(將清除映射的代碼注釋掉)。
next:
# unmap va 0 ~ 4M, it's temporary mapping
#xorl %eax, %eax
#movl %eax, __boot_pgdir
ucore的代碼大量使用了KERNBASE
+物理地址等於虛擬地址的映射,為了盡可能降低修改的代碼數,仍使用宏KERNBASE
和VPT
(lab2中沒有用到,為了避免bug仍然修改它),但是將他們減去0x38000000。
// #define KERNBASE 0xC0000000
#define KERNBASE 0x00000000
// #define VPT 0xFAC00000
#define VPT 0xC2C00000
修改了KERNBASE
后,虛擬地址和物理地址的關系就變成了:
physical address + 0 == virtual address
接下來ucore的虛擬地址應該會等於物理地址,但是事情並沒有這么順利。如果僅做了這些修改,ucore會在boot_map_segment
設置“好”頁表后異常終止或跳轉到別的地方執行。閱讀源代碼無法發現錯誤。
GDB調試發現boot_map_setment()
在設置好boot_pgdir[0]
(0 ~ 4M)后,設置boot_pgdir[1]
時get_pte
會取得目錄項boot_pgdir[0]
指向的頁表。也就是說,頁目錄項 PDE 0 和 PDE 1共同指向同一個頁表__boot_pt1
,在設置虛擬地址4 ~ 8M 到物理地址 4 ~ 8M 的映射時,同時將虛擬地址地址0 ~ 4M 映射到了 4 ~ 8M ,導致ucore運行異常。
查看頁表可以發現boot_pgdir[0]
和boot_pgdir[1]
的內容相同!這導致了調用get_pte()
時,0 ~ 8M的虛擬地址會返回同一個 PTE __boot_pt1
,出現上述現象。
奇怪的是,kern_entry
中將boot_pgdir[1]
設置為0(.space
指令),而不是boot_pgdir[0]
。
.section .data.pgdir
.align PGSIZE
__boot_pgdir:
.globl __boot_pgdir
# map va 0 ~ 4M to pa 0 ~ 4M (temporary)
.long REALLOC(__boot_pt1) + (PTE_P | PTE_U | PTE_W)
.space (KERNBASE >> PGSHIFT >> 10 << 2) - (. - __boot_pgdir) # pad to PDE of KERNBASE
# map va KERNBASE + (0 ~ 4M) to pa 0 ~ 4M
.long REALLOC(__boot_pt1) + (PTE_P | PTE_U | PTE_W)
.space PGSIZE - (. - __boot_pgdir) # pad to PGSIZE
為了修復這個問題,在boot_map_segment()
中,先清除boot_pgdir[1]
的 present 位,再進行其他操作。這是get_pte
會分配一個物理頁作為boot_pgdir[1]
指向的頁表。
static void
boot_map_segment(pde_t *pgdir, uintptr_t la, size_t size, uintptr_t pa, uint32_t perm)
{
boot_pgdir[1] &= ~PTE_P;
...
}
虛擬地址到物理地址的映射改變了,不可能通過check_pgdir()
和check_boot_pgdir()
的測試,所以要注釋掉這兩行調用。
最終運行結果如下:
page_remove_pte
函數的實現
page_remove_pte
語義為:清除 PTE 指向的內存對應的 PTE 和 Page
結構。
步驟:
- 判斷
ptep
指向的 PTE 是否存在,若不存在,不需要進行處理。 - 若
ptep
指向的 PTE 存在,計算其指向的內存對應的Page
結構,遞減引用計數,若已無虛擬地址指向該頁,將其釋放。 - 清除 PTE 並刷新 TLB。
static inline void
page_remove_pte(pde_t *pgdir, uintptr_t la, pte_t *ptep) {
assert(pgdir != NULL);
assert(ptep != NULL);
pde_t *pdep = pgdir + PDX(la); // virtual address of PDE
// PTE pointed by ptep must reside in page pointed by PDE
assert(PDE_ADDR(*pdep) == PADDR(ROUNDDOWN(ptep, PGSIZE)));
// if PDE exists
if (test_bit(0, ptep)) {
// Page struct related with la pointed by PTE
struct Page *page = pte2page(*ptep);
// decrease page reference and free this page when page reference reachs 0
page_ref_dec(page);
if (page_ref(page) == 0)
free_page(page);
// clear PTE pointed by ptep
clear_bit(PTE_P, ptep);
// flush TLB
tlb_invalidate(pgdir, la);
}
// for debug
else
cprintf("test_bit(PTE_P, ptep) error\n");
}
運行結果: