所有的實驗報告將會在 Github 同步更新,更多內容請移步至Github:https://github.com/AngelKitty/review_the_national_post-graduate_entrance_examination/blob/master/books_and_notes/professional_courses/operating_system/sources/ucore_os_lab/docs/lab_report/
練習0:填寫已有實驗
lab2
會依賴 lab1
,我們需要把做的 lab1
的代碼填到 lab2
中缺失的位置上面。練習 0 就是一個工具的利用。這里我使用的是 Linux
下的系統已預裝好的 Meld Diff Viewer
工具。具體操作流程如下圖所示:
我們只需要將已經完成的 lab1
和待完成的 lab2
兩個文件夾導入進來,然后點擊 compare
就行了。
然后軟件就會自動分析兩份代碼的不同,然后就一個個比較比較復制過去就行了,在軟件里面是可以支持打開對比復制了,點擊 Copy Right
即可。當然 bin
目錄和 obj
目錄下都是 make
生成的,就不用復制了,其他需要修改的地方主要有以下三個文件,通過對比復制完成即可:
kern/debug/kdebug.c
kern/init/init.c
kern/trap/trap.c
練習1:實現 first-fit 連續物理內存分配算法(需要編程)
在沒有其它技術支持的情況下,我們在分配內存空間的時候,分配給一個進程的地址空間必須是連續的。為了提高利用效率,我們希望分配出來的空間有適度的選擇。這些動態分配算法實際上就是你在去做選擇的做法。而選擇完之后呢,每個進程可能用的時間長短不一樣,有的進程先結束,有的進程后結束,這個時候,有可能先結束的會留出一些空間,后面一個在分配的時候又會再去找,這個過程的執行就會在這過程中留下一些碎片,這些碎片對於我們后續的內存分配是有一定影響的,那我們就從如何去找你要的空閑分區和如何來處理不能利用的這些小的空閑分區兩個角度來看內存分配算法。
- 1、First Fit(最先匹配算法)
該算法從空閑分區鏈首開始查找,直至找到一個能滿足其大小要求的空閑分區為止。然后再按照作業的大小,從該分區中划出一塊內存分配給請求者,余下的空閑分區仍留在空閑分區鏈中。
-
- 優點:該算法傾向於使用內存中低地址部分的空閑區,在高地址部分的空閑區很少被利用,從而保留了高地址部分的大空閑區。顯然為以后到達的大作業分配大的內存空間創造了條件。
- 缺點:低地址部分不斷被划分,留下許多難以利用、很小的空閑區,而每次查找又都從低地址部分開始,會增加查找的開銷。
- 2、Next Fit(循環首次匹配算法)
該算法是由首次適應算法演變而成的。在為進程分配內存空間時,不再每次從鏈首開始查找,直至找到一個能滿足要求的空閑分區,並從中划出一塊來分給作業。
-
- 優點:使內存中的空閑分區分布的更為均勻,減少了查找時的系統開銷。
- 缺點:缺乏大的空閑分區,從而導致不能裝入大型作業。
- 3、Best Fit(最佳匹配算法)
該算法總是把既能滿足要求,又是最小的空閑分區分配給作業。為了加速查找,該算法要求將所有的空閑區按其大小排序后,以遞增順序形成一個空白鏈。這樣每次找到的第一個滿足要求的空閑區,必然是最優的。孤立地看,該算法似乎是最優的,但事實上並不一定。因為每次分配后剩余的空間一定是最小的,在存儲器中將留下許多難以利用的小空閑區。同時每次分配后必須重新排序,這也帶來了一定的開銷。
-
- 優點:每次分配給文件的都是最合適該文件大小的分區。
- 缺點:內存中留下許多難以利用的小的空閑區。
- 4、Worst Fit(最差匹配算法)
該算法按大小遞減的順序形成空閑區鏈,分配時直接從空閑區鏈的第一個空閑區中分配(不能滿足需要則不分配)。很顯然,如果第一個空閑分區不能滿足,那么再沒有空閑分區能滿足需要。這種分配方法初看起來不太 合理,但它也有很強的直觀吸引力:在大空閑區中放入程序后,剩下的空閑區常常也很大,於是還能裝下一個較大的新程序。
最壞適應算法與最佳適應算法的排序正好相反,它的隊列指針總是指向最大的空閑區,在進行分配時,總是從最大的空閑區開始查尋。
該算法克服了最佳適應算法留下的許多小的碎片的不足,但保留大的空閑區的可能性減小了,而且空閑區回收也和最佳適應算法一樣復雜。
-
- 優點:給文件分配分區后剩下的空閑區不至於太小,產生碎片的幾率最小,對中小型文件分配分區操作有利。
- 缺點:使存儲器中缺乏大的空閑區,對大型文件的分區分配不利。
我們要先熟悉兩個數據結構,第一個就是如下所示的每一個物理頁的屬性結構。
struct Page {
int ref; // 頁幀的 引用計數
uint32_t flags; // 頁幀的狀態 Reserve 表示是否被內核保留 另一個是 表示是否 可分配
unsigned int property; // 記錄連續空閑頁塊的數量 只在第一塊進行設置
list_entry_t page_link; // 用於將所有的頁幀串在一個雙向鏈表中 這個地方很有趣 直接將 Page 這個結構體加入鏈表中會有點浪費空間 因此在 Page 中設置一個鏈表的結點 將其結點加入到鏈表中 還原的方法是將 鏈表中的 page_link 的地址 減去它所在的結構體中的偏移 就得到了 Page 的起始地址
};
該結構四個成員變量意義如下:
- 1、
ref
表示這樣頁被頁表的引用記數,這里應該就是映射此物理頁的虛擬頁個數。一旦某頁表中有一個頁表項設置了虛擬頁到這個Page
管理的物理頁的映射關系,就會把Page
的ref
加一。反之,若是解除,那就減一。 - 2、
flags
表示此物理頁的狀態標記,有兩個標志位,第一個表示是否被保留,如果被保留了則設為1
(比如內核代碼占用的空間)。第二個表示此頁是否是free
的。如果設置為1
,表示這頁是free
的,可以被分配;如果設置為0
,表示這頁已經被分配出去了,不能被再二次分配。 - 3、
property
用來記錄某連續內存空閑塊的大小,這里需要注意的是用到此成員變量的這個Page
一定是連續內存塊的開始地址(第一頁的地址)。 - 4、
page_link
是便於把多個連續內存空閑塊鏈接在一起的雙向鏈表指針,連續內存空閑塊利用這個頁的成員變量page_link
來鏈接比它地址小和大的其他連續內存空閑塊。
然后是下面這個結構。一個雙向鏈表,負責管理所有的連續內存空閑塊,便於分配和釋放。
typedef struct {
list_entry_t free_list; // the list header
unsigned int nr_free; // # of free pages in this free list
} free_area_t;
- free_list 是一個 list_entry 結構的雙向鏈表指針
- nr_free 則記錄當前空閑頁的個數
首先我們需要完成的是 /home/moocos/moocos/ucore_lab/labcodes_answer/lab2_result/kern/mm/default_pmm.c
中的default_init
,default_init_memmap
,default_alloc_pages
,default_free_pages
這幾個函數的修改。
先來看看 default_init
函數,該函數它已經實現好了,不用做修改:
/*
default_init: you can reuse the demo default_init fun to init the free_list and set nr_free to 0.
free_list is used to record the free mem blocks. nr_free is the total number for free mem blocks.
*/
// 初始化空閑頁塊鏈表
static void default_init(void) {
list_init(&free_list);
nr_free = 0;// 空閑頁塊一開始是0個
}
然后是 default_init_memmap
,這個函數是用來初始化空閑頁鏈表的,初始化每一個空閑頁,然后計算空閑頁的總數。
初始化每個物理頁面記錄,然后將全部的可分配物理頁視為一大塊空閑塊加入空閑表。
我們可以有如下實現過程:
/*
default_init_memmap: CALL GRAPH: kern_init --> pmm_init-->page_init-->init_memmap--> pmm_manager->init_memmap
* This fun is used to init a free block (with parameter: addr_base, page_number).
* First you should init each page (in memlayout.h) in this free block, include:
* p->flags should be set bit PG_property (means this page is valid. In pmm_init fun (in pmm.c),
* the bit PG_reserved is setted in p->flags)
* if this page is free and is not the first page of free block, p->property should be set to 0.
* if this page is free and is the first page of free block, p->property should be set to total num of block.
* p->ref should be 0, because now p is free and no reference.
* We can use p->page_link to link this page to free_list, (such as: list_add_before(&free_list, &(p->page_link)); )
* Finally, we should sum the number of free mem block: nr_free+=n
**/
// 初始化n個空閑頁塊
static void default_init_memmap(struct Page *base, size_t n) {
assert(n > 0);
struct Page *p = base;
for (; p != base + n; p ++) {
assert(PageReserved(p));//確認本頁是否為保留頁
//設置標志位
p->flags = p->property = 0;
set_page_ref(p, 0);//清空引用
}
base->property = n; //連續內存空閑塊的大小為n,屬於物理頁管理鏈表,頭一個空閑頁塊 要設置數量
SetPageProperty(base);
nr_free += n; //說明連續有n個空閑塊,屬於空閑鏈表
list_add_before(&free_list, &(p->page_link));//插入空閑頁的鏈表里面,初始化完每個空閑頁后,將其要插入到鏈表每次都插入到節點前面,因為是按地址排序
}
接着是 default_alloc_memmap
,主要就是從空閑頁塊的鏈表中去遍歷,找到第一塊大小大於 n
的塊,然后分配出來,把它從空閑頁鏈表中除去,然后如果有多余的,把分完剩下的部分再次加入會空閑頁鏈表中即可。
首次適配算法要求按照地址從小到大查找空間,所以要求空閑表中的空閑空間按照地址從小到大排序。這樣,首次適配算法查詢空閑空間的方法就是從鏈表頭部開始找到第一個符合要求的空間,將這個空間從空閑表中刪除。空閑空間在分配完要求數量的物理頁之后可能會有剩余,那么需要將剩余的部分作為新的空閑空間插入到原空間位置(這樣才能保證空閑表中空閑空間地址遞增)
實現過程如下:
// 分配n個頁塊
* 設計思路:
分配空間的函數中進行了如下修改,因為free_list始終是排序的,分配的page塊有剩余空間,那么只需把
剩余空閑塊節點插入到當前節點的前一個節點的后面(或者是當前節點后一個節點的前面)即可
if (page->property > n) {
struct Page *p = page + n;
p->property = page->property - n;
// 將page的property改為n
page->property = n;
// 由於是排好序的鏈表,只需要在le的前一個節點后面插入即可
list_add(list_prev(le), &(p->page_link));
// list_add_before(list_next(le), &(p->page_link));
}
1.第一種情況(找不到滿足需求的可供分配的空閑塊(所有的size均 < n))
2.第二種情況(剛好有滿足大小的空閑塊)
執行分配前
-------------- -------------- -------------
| size < n | <---> | size = n | <---> | size > n |
-------------- -------------- -------------
執行分配后
-------------- -------------
| size < n | <---> | size > n |
-------------- -------------
3.第三種情況(不存在剛好滿足大小的空閑塊,但存在比其大的空閑塊)
執行分配前
-------------- ------------ --------------
| size < n | <---> | size > n | <---> | size > n1 |
-------------- ------------ --------------
執行分配后
-------------- --------------------- --------------
| size < n | <---> | size = size - n | <---> | size > n1 |
-------------- --------------------- --------------
--------------------------------------------------------------------------------------------
/*code*/
static struct Page * default_alloc_pages(size_t n) {
assert(n > 0);
if (n > nr_free) { //如果所有的空閑頁的加起來的大小都不夠,那直接返回NULL
return NULL;
}
struct Page *page = NULL;
list_entry_t *le = &free_list;//從空閑塊鏈表的頭指針開始
// 查找 n 個或以上 空閑頁塊 若找到 則判斷是否大過 n 則將其拆分 並將拆分后的剩下的空閑頁塊加回到鏈表中
while ((le = list_next(le)) != &free_list) {//依次往下尋找直到回到頭指針處,即已經遍歷一次
// 此處 le2page 就是將 le 的地址 - page_link 在 Page 的偏移 從而找到 Page 的地址
struct Page *p = le2page(le, page_link);//將地址轉換成頁的結構
if (p->property >= n) {//由於是first-fit,則遇到的第一個大於N的塊就選中即可
page = p;
break;
}
}
if (page != NULL) {
if (page->property > n) {
struct Page *p = page + n;
p->property = page->property - n;//如果選中的第一個連續的塊大於n,只取其中的大小為n的塊
SetPageProperty(p);
// 將多出來的插入到 被分配掉的頁塊 后面
list_add(&(page->page_link), &(p->page_link));
}
// 最后在空閑頁鏈表中刪除掉原來的空閑頁
list_del(&(page->page_link));
nr_free -= n;//當前空閑頁的數目減n
ClearPageProperty(page);
}
return page;
}
最后是 default_free_pages
,將需要釋放的空間標記為空之后,需要找到空閑表中合適的位置。由於空閑表中的記錄都是按照物理頁地址排序的,所以如果插入位置的前驅或者后繼剛好和釋放后的空間鄰接,那么需要將新的空間與前后鄰接的空間合並形成更大的空間。
實現過程如下:
// 釋放掉 n 個 頁塊
* 設計思路:
1.尋找插入位置(插入到地址 > base的空閑塊鏈表節點前)
// 1.尋找插入點
list_entry_t *le = LIST_HEAD;
struct Page * node = NULL;
while ((le = list_next(le)) != LIST_HEAD) {
node = le2page(le, page_link);
if (node > base) {
break;
}
}
2.進行地址比對,已確定插入位置及處理方式
分析: 循環結束情況及處理方式分為如下3種
(1).空閑鏈表為空(只有頭結點),直接添加到頭結點后面就可以
if (node == NULL) {
list_add(&free_list, &(base->page_link));
}
(2).查到鏈表尾均未發現比即將插入到空閑連表地址大的空閑塊。
a.先插入到鏈表尾部
b.嘗試與前一個節點進行合並
list_entry_t *prev = list_prev(le);
if (node < base) {
// 所需插入的節點為末節點
// 先插入到空閑鏈表中
list_add(prev, &(base->page_link));
// 進行前向合並
if (node + node->property == base) {
node->property += base->property;
ClearPageProperty(base);
list_del(&(base->page_link));
}
}
(3).找到比需要插入空閑塊地址大的空閑塊節點而跳出循環
a.先插入到找到的節點前面
b.嘗試與后一個節點進行合並
c.如果前一個節點不為頭結點,則嘗試與前一個節點進行合並
// 所需插入節點不為末節點
// 先插入到空閑鏈表中
list_add_before(le, &(base->page_link));
// 進行后向合並
if (base + base->property == node) {
base->property += node->property;
ClearPageProperty(node);
// 摘除后向節點
list_del(le);
}
// 進行前向合並
if (prev != LIST_HEAD) {
// 前拼接
node = le2page(prev, page_link);
if (node + node ->property == base) {
node->property += base->property;
ClearPageProperty(base);
// 摘掉當前節點
list_del(&(base->page_link));
}
}
3.更新空閑鏈表可用空閑塊數量
nr_free += n;
--------------------------------------------------------------------------------------------
/*code*/
static void default_free_pages(struct Page *base, size_t n) {
assert(n > 0);
struct Page *p = base;
for (; p != base + n; p ++) {
assert(!PageReserved(p) && !PageProperty(p));
p->flags = 0;//修改標志位
set_page_ref(p, 0);
}
base->property = n;//設置連續大小為n
SetPageProperty(base);
list_entry_t *le = list_next(&free_list);
// 合並到合適的頁塊中
while (le != &free_list) {
p = le2page(le, page_link);//獲取鏈表對應的Page
le = list_next(le);
if (base + base->property == p) {
base->property += p->property;
ClearPageProperty(p);
list_del(&(p->page_link));
}
else if (p + p->property == base) {
p->property += base->property;
ClearPageProperty(base);
base = p;
list_del(&(p->page_link));
}
}
nr_free += n;
le = list_next(&free_list);
// 將合並好的合適的頁塊添加回空閑頁塊鏈表
while (le != &free_list) {
p = le2page(le, page_link);
if (base + base->property <= p) {
break;
}
le = list_next(le);
}
list_add_before(le, &(base->page_link));//將每一空閑塊對應的鏈表插入空閑鏈表中
}
你的First Fit算法是否有進一步的改進空間?
在上面的 First Fit
算法中,有兩個地方需要 時間復雜度:鏈表查找和有序鏈表插入。對於其中的有序鏈表插入,在特殊情況下是可以優化的。當一個剛被釋放的內存塊來說,如果它的鄰接空間都是空閑的,那么就不需要進行線性時間復雜度的鏈表插入操作,而是直接並入鄰接空間,時間復雜度為常數。為了判斷鄰接空間是否為空閑狀態,空閑塊的信息除了保存在第一個頁面之外,還需要在最后一頁保存信息,這樣新的空閑塊只需要檢查鄰接的兩個頁面就能判斷鄰接空間塊的狀態。
練習2:實現尋找虛擬地址對應的頁表項(需要編程)
這里我們需要實現的是 get_pte
函數,函數找到一個虛地址對應的二級頁表項的內核虛地址,如果此二級頁表項不存在,則分配一個包含此項的二級頁表。
由於我們已經具有了一個物理內存頁管理器 default_pmm_manager
,我們就可以用它來獲得所需的空閑物理頁。
在二級頁表結構中,頁目錄表占 4KB 空間,ucore 就可通過 default_pmm_manager 的 default_alloc_pages 函數獲得一個空閑物理頁,這個頁的起始物理地址就是頁目錄表的起始地址。同理,ucore 也通過這種方式獲得各個頁表所需的空間。頁表的空間大小取決與頁表要管理的物理頁數 n,一個頁表項(32位,即4字節)可管理一個物理頁,頁表需要占 n/1024 個物理頁空間(向上取整)。這樣頁目錄表和頁表所占的總大小約為 4096+4∗n 字節。
根據LAZY,這里我們並沒有一開始就存在所有的二級頁表,而是等到需要的時候再添加對應的二級頁表。當建立從一級頁表到二級頁表的映射時,需要注意設置控制位。這里應該設置同時設置上 PTE_U、PTE_W 和 PTE_P(定義可在mm/mmu.h)。如果原來就有二級頁表,或者新建立了頁表,則只需返回對應項的地址即可。
如果 create 參數為 0,則 get_pte 返回 NULL;如果 create 參數不為 0,則 get_pte 需要申請一個新的物理頁。
頁目錄項內容 = (頁表起始物理地址 & ~0x0FFF) | PTE_U | PTE_W | PTE_P
或者(分配的地址是4K對齊的,即低12位為0,無需 & ~0x0FFF也行)
頁目錄項內容 = 頁表起始物理地址 | PTE_U | PTE_W | PTE_P
整理后的目錄項如下:
數據類型 | 說明 |
---|---|
pde_t | 全稱為page directory entry,也就是一級頁表的表項(注意:pgdir 實際不是表項,而是一級頁表本身,pgdir 給出頁表起始地址。)。高22位存儲該目錄項所對應的頁表起始物理地址,其中高10位為在頁目錄項表中的索引,中10位為在頁表中的索引,低12位用於存儲標識信息(權限等) |
pte_t | 全稱為page table entry,表示二級頁表的表項。高22為存儲該頁表項所對應的頁面起始物理地址,低12位存儲標識信息(權限等) |
uintptr_t | 表示為線性地址,由於段式管理只做直接映射,所以它也是邏輯地址。 |
PTE_U | 位3,表示用戶態的軟件可以讀取對應地址的物理內存頁內容 |
PTE_W | 位2,表示物理內存頁內容可寫 |
PTE_P | 位1,表示物理內存頁存在 |
實現過程如下:
* 設計思路:
提供一個虛擬地址,然后根據這個虛擬地址的高 10 位,找到頁目錄表中的 PDE 項。前20位是頁表項 (二級頁表) 的線性地址,后 12 位為屬性,然后判斷一下 PDE 是否存在(就是判斷 P 位)。不存在,則獲取一個物理頁,然后將這個物理頁的線性地址寫入到 PDE 中,最后返回 PTE 項。簡而言之就是根據所給的虛擬地址,構造一個 PTE 項。
// 目錄表中目錄項的起始地址 pdep
// 目錄表中目錄項的值 *pdep
// 頁表的起始物理地址 (*pdep & ~0xFFF) 即 PDE_ADDR(*pdep)
// 頁表的起始內核虛擬地址 KADDR((*pdep & ~0xFFF))
// la在頁表項的偏移量為 PTX(la)
// 頁表項的起始物理地址為 (pte_t *)KADDR((*pdep & ~0xFFF)) + PTX(la)
--------------------------------------------------------------------------------------------
/*code*/
pte_t *get_pte(pde_t *pgdir, uintptr_t la, bool create) {
pde_t *pdep = &pgdir[PDX(la)]; // 找到 PDE 這里的 pgdir 可以看做是頁目錄表的基址
if (!(*pdep & PTE_P)) { // 看看 PDE 指向的頁表是否存在
struct Page* page = alloc_page(); // 不存在就申請一頁物理頁
/* 這里說多幾句 通過 default_alloc_pages() 分配的頁 的地址 並不是真正的頁分配的地址
實際上只是 Page 這個結構體所在的地址而已 故而需要 通過使用 page2pa() 將 Page 這個結構體
的地址 轉換成真正的物理頁地址的線性地址 然后需要注意的是 無論是 * 或是 memset 都是對虛擬地址進行操作的
所以需要將 真正的物理頁地址再轉換成 內核虛擬地址
*/
if (!create || page == NULL) { //不存在且不需要創建,返回NULL
return NULL;
}
set_page_ref(page, 1); //設置此頁被引用一次
uintptr_t pa = page2pa(page);//得到 page 管理的那一頁的物理地址
memset(KADDR(pa), 0, PGSIZE); // 將這一頁清空 此時將線性地址轉換為內核虛擬地址
*pdep = pa | PTE_U | PTE_W | PTE_P; // 設置 PDE 權限
}
return &((pte_t *)KADDR(PDE_ADDR(*pdep)))[PTX(la)];
}
1、請描述頁目錄項(Page Directory Entry)和頁表項(Page Table Entry)中每個組成部分的含義以及對 ucore 而言的潛在用處。
從低到高,分別是:
- P (Present) 位:表示該頁保存在物理內存中。
- R (Read/Write) 位:表示該頁可讀可寫。
- U (User) 位:表示該頁可以被任何權限用戶訪問。
- W (Write Through) 位:表示 CPU 可以直寫回內存。
- D (Cache Disable) 位:表示不需要被 CPU 緩存。
- A (Access) 位:表示該頁被寫過。
- S (Size) 位:表示一個頁 4MB 。
- 9-11 位保留給 OS 使用。
- 12-31 位指明 PTE 基質地址。
從低到高,分別是:
- 0-3 位同 PDE。
- C (Cache Disable) 位:同 PDE D 位。
- A (Access) 位:同 PDE 。
- D (Dirty) 位:表示該頁被寫過。
- G (Global) 位:表示在 CR3 寄存器更新時無需刷新 TLB 中關於該頁的地址。
- 9-11 位保留給 OS 使用。
- 12-31 位指明物理頁基址。
因為頁的映射是以物理頁面為單位進行,所以頁面對應的物理地址總是按照 4096 字節對齊的,物理地址低 0-11 位總是零,所以在頁目錄項和頁表項中,低 0-11 位可以用於作為標志字段使用。
位 | 意義 |
---|---|
0 | 表項有效標志(PTE_U) |
1 | 可寫標志(PTE_W) |
2 | 用戶訪問權限標志(PTE_P) |
3 | 寫入標志(PTE_PWT) |
4 | 禁用緩存標志(PTE_PCD) |
5 | 訪問標志(PTE_A) |
6 | 臟頁標志(PTE_D) |
7 | 頁大小標志(PTE_PS) |
8 | 零位標志(PTE_MBZ) |
11 | 軟件可用標志(PTE_AVAIL) |
12-31 | 頁表起始物理地址/頁起始物理地址 |
2、如果ucore執行過程中訪問內存,出現了頁訪問異常,請問硬件要做哪些事情?
會進行換頁操作。首先 CPU 將產生頁訪問異常的線性地址放到 cr2 寄存器中,然后就是和普通的中斷一樣,保護現場,將寄存器的值壓入棧中,設置錯誤代碼 error_code,觸發 Page Fault 異常,然后壓入 error_code 中斷服務例程,將外存的數據換到內存中來,最后退出中斷,回到進入中斷前的狀態。
練習3:釋放某虛地址所在的頁並取消對應二級頁表項的映射(需要編程)
這里主要是 page_remove_pte
的補全及完善。
思路主要就是先判斷該頁被引用的次數,如果只被引用了一次,那么直接釋放掉這頁, 否則就刪掉二級頁表的該表項,即該頁的入口。
取消頁表映射過程如下:
- 將物理頁的引用數目減一,如果變為零,那么釋放頁面;
- 將頁目錄項清零;
- 刷新TLB。
實現過程如下:
* 設計思路:
首先判斷一下 ptep 是不是合法——檢查一下 Present 位就是了。
然后通過注釋中所說的,通過 pte2page(*ptep) 獲取相應頁,減少引用計數並決定是否釋放頁。
最后把 TLB 中該頁的緩存刷掉就可以了。
--------------------------------------------------------------------------------------------
/*code*/
static inline void page_remove_pte(pde_t *pgdir, uintptr_t la, pte_t *ptep) {
if ((*ptep & PTE_P)) { //判斷頁表中該表項是否存在
struct Page *page = pte2page(*ptep);// 將頁表項轉換為頁數據結構
if (page_ref_dec(page) == 0) { // 判斷是否只被引用了一次,若引用計數減一后為0,則釋放該物理頁
free_page(page);
}
*ptep = 0; // //如果被多次引用,則不能釋放此頁,只用釋放二級頁表的表項,清空 PTE
tlb_invalidate(pgdir, la); // 刷新 tlb
}
}
運行結果如下:
1、數據結構Page的全局變量(其實是一個數組)的每一項與頁表中的頁目錄項和頁表項有無對應關系?如果有,其對應關系是啥?
存在對應關系,從pmm.h中的一系列轉換函數及KADDR、PADDR等宏定義中可知(page為pages中的一個項):
PDX: 頁目錄表索引
PTX: 頁表索引
PPN: 線性地址的高20位,即PDX + PTX
PA: 物理地址 KA - KERNBASE
KA: 內核虛地址 PA + KERNBASE
page - pages = PPN
PPN << 12 = PA
&pages[PPN(pa)] = page
其實就是 Page 全局數組中以 Page Directory Index 和 Page Table Index 的組合 PPN (Physical Page Number) 為索引的那一項。
2、如果希望虛擬地址與物理地址相等,則需要如何修改lab2,完成此事? 鼓勵通過編程來具體完成這個問題
* 設計思路:
1.必須先使內核的鏈接地址等於加載地址(之前嘗試過完成分頁機制后進行重映射的方法,后查看kernel.asm發現
內核文件的鏈接地址均為0xc開頭的)
2.修改tool/kernel.ld文件, 將. = 0xC0100000修改為. = 0x00100000
3.修改kern/mm/memlayout.h中的KERNBASE宏定義為0x00000000
4.去除pmm_init中的臨時映射及取消臨時映射機制
5.移除與使用非物理地址相等的虛擬地址的檢驗函數相關語句,即boodir[0] = 0或某種形式的
assert(boot_pgdir == 0)
實現過程如下:
① 修改鏈接腳本,將內核起始虛擬地址修改為0x100000
;
/*
tools/kernel.ld
**/
--------------------------------------------------------------------------------------------
/*code*/
SECTIONS {
/* Load the kernel at this address: "." means the current address */
. = 0x100000;
...
② 修改虛擬內存空間起始地址為0
/*
kern/mm/memlayout.h
**/
--------------------------------------------------------------------------------------------
/*code*/
/* All physical memory mapped at this address */
#define KERNBASE 0x00000000
③ 注釋掉取消0~4M區域內存頁映射的代碼
/*
kern/mm/pmm.c
**/
--------------------------------------------------------------------------------------------
/*code*/
//disable the map of virtual_addr 0~4M
// boot_pgdir[0] = 0;
//now the basic virtual memory map(see memalyout.h) is established.
//check the correctness of the basic virtual memory map.
// check_boot_pgdir();
擴展練習Challenge
1、buddy system(伙伴系統)分配算法(需要編程)
初始化
在 Buddy System 中,空間塊之間的關系形成一顆完全二叉樹,對於一顆有着 n 葉子的完全二叉樹來說,所有節點的總數為 。也就是說,如果 Buddy System 的可分配空間為 n 頁的話,那么就需要額外保存 2n-1 個節點信息。
初始化空閑鏈表
Buddy System 並不需要鏈表,但是為了在調式的時候方便訪問所有空閑空間,還是將所有的空閑空間加入鏈表中。
確定分配空間大小
假設我們得到了大小為 n 的空間,我們需要在此基礎上建立 Buddy System,經過初始化后,Buddy System 管理的頁數為 ,那么大小為 n 的實際空間可能分為兩個或者三個部分。
節點信息區:節點信息區可以用來保存每個節點對應子樹中可用空間的信息,用於在分配內存的時候便於檢查子樹中是否有足夠的空間來滿足請求大小。在 32 位操作系統中,最大頁數不會超過 4GB/4KB=1M,所有使用一個 32 位整數即可表示每個節點的信息。所以節點信息區的大小為 字節,每頁大小為 4KB,內存占用按照頁面大小對齊,所以占用
頁。
虛擬分配區:占用 頁。
實際分配區:顯然實際可以得到的內存大小不大可能剛好等於節點信息區大小+分配空間區大小。如果節點信息區大小+分配空間區大小<=內存大小,那么實際可以分配的區域就等於 頁。如果節點信息區大小+分配空間區大小>內存大小,那么實際可以分配的區域就等於
頁。
作為操作系統,自然希望實際使用的區域越大越好,不妨分類討論。
當內存小於等於512頁:此時無論如何節點信息都會占用一頁,所以提高內存利率的方法就是將實際內存大小減一后向上取整(文中整數意為2的冪)。
當內存大於512頁:不難證明,對於內存大小 \(n\) 來說,最佳虛擬分配區大小往往是 n 向下取整或者向上取整的數值,所以候選項也就是只有兩個,所以可以先考慮向下取整。對於 中的數
,向下取整可以得到 :
- 當
時,顯然已經是最佳值;
- 當
時,擴大虛擬分配區導致節點信息區增加卻沒有使得實際分配區增加,所以當期 m 還是最佳值;
- 當
時,
可以擴大實際分配區。
初始化節點信息
虛擬分配區可能會大於實際分配區,所以一開始需要將虛擬分配區中沒有實際分配區對應的空間標記為已經分配進行屏蔽。另當前區塊的虛擬空間大小為 ,實際空間大小為
,屏蔽的過程如下:
- 如果
,將空間初始化為一個空閑空間,屏蔽過程結束;
- 如果
,將空間初始化為一個已分配空間,屏蔽過程結束;
- 如果
,將右半空間初始化為已分配空間,更新
后繼續對左半空間進行操作;
- 如果
,將左半空間初始化為空閑空間,更新
后繼續對左半空間進行操作。
以虛擬分配區 16 頁、實際分配區 14 頁為例,初始化后如下:
分配過程
Buddy System 要求分配空間為 2 的冪,所以首先將請求的頁數向上對齊到 2 的冪。
接下來從二叉樹的根節點(1號節點)開始查找滿足要求的節點。對於每次檢查的節點:
- 如果子樹的最大可用空間小於請求空間,那么分配失敗;
- 如果子樹的最大可用空間大於等於請求空間,並且總空間大小等於請求空間,說明這個節點對應的空間沒有被分割和分配,並且滿足請求空間大小,那么分配這個空間;
- 如果子樹的最大可用空間大於等於請求空間,並且總空間大小大於請求空間,那么在這個節點的子樹中查找:
- 如果這個節點對應的空間沒有被分割過(最大可用空間等於總空間大小),那么分割空間,在左子樹(左半部分)繼續查找;
- 如果左子樹包含大小等於請求空間的可用空間,那么在左子樹中繼續查找;
- 如果右子樹包含大小等於請求空間的可用空間,那么在右子樹中繼續查找;
- 如果左子樹的最大可用空間大於等於請求空間,那么在左子樹中繼續查找;
- 如果右子樹的最大可用空間大於等於請求空間,那么在右子樹中繼續查找。
算法中加粗的部分主要為了減少碎片而增加的額外優化。
當一個空間被分配之后,這個空間對應節點的所有父節點的可用空間表都會受到影響,需要自地向上重新更新可用空間信息。
釋放過程
Buddy System 要求分配空間為 2 的冪,所以同樣首先將請求的頁數向上對齊到2的冪。
在進行釋放之前,需要確定要釋放的空間對應的節點,然后將空間標記為可用。接下來進行自底向上的操作:
- 如果某節點的兩個子節點對應的空間都未分割和分配,那么合並這兩個空間,形成一個更大的空間;
- 否則,根據子節點的可用空間信息更新父節點的可用空間信息。
實現過程如下:
由於不能在堆上分配內存,無法使用二級鏈表,故 buddy 分配算法使用一級雙向鏈表進行組織,該算法配合
最佳適應算法進行查找獲取,可在一定程度上減小內存碎片,為程序騰出大的空閑空間。
設計思路:
1.新建buddy_pmm.h及buddy_pmm.c文件用於實現pmm_manager規定的接口(函數指針)
2.將pmm.c的init_pmm_manager的實現改為如下,通過Makefile加入預編譯宏_USE_PMM_BUDDY實現
向Buddy內存管理分配算法的切換,其中_USE_DEBUG宏用於輸出調試信息
static void init_pmm_manager(void) {
#ifdef _USE_PMM_BUDDY
pmm_manager = &buddy_pmm_manager;
#else
pmm_manager = &default_pmm_manager;
#endif
cprintf("memory management: %s\n", pmm_manager->name);
pmm_manager->init();
}
Makefile:
# memory management algorithm
ifdef DEFS
DEFS :=
endif
DEFS += -D_USE_PMM_BUDDY
DEFS += -D_USE_DEBUG
3.添加用於輔助伙伴分配算法的數學函數,主要是指數函數、對數函數及對數取整函數
// 求以base為底數,n為指數的值
int32_t pow(int32_t base, int32_t n);
int32_t pow2(int32_t n);
// 求以base為底數,n為真數的對數函數,其要求n必須為base的正整數次冪
size_t log(int32_t base, int32_t n);
size_t log2(int32_t n);
// 對數向上、向下取整函數簇
size_t log_round_up(int32_t base, int32_t n);
size_t log_round_down(int32_t base, int32_t n);
size_t log2_round_up(int32_t n);
size_t log2_round_down(int32_t n);
4.依次實現相關函數指針,並初始化buddy_pmm_manager數據結構
主要數據結構如下:
typedef struct Buddy {
// 伙伴大小(2的次冪),在頭結點表示最大的2的次冪空閑塊
size_t power;
// 伙伴的可用頁面大小(for speed up purpose),頭結點為可用頁面數量,也方便比較
size_t property;
// 用於組織buddy節點, 和page共享
list_entry_t node;
} buddy_t, *buddy_ptr_t;
// buddy
buddy_t _buddy;
其中初始化函數如下:
static void buddy_init_memmap(struct Page *base, size_t n) {
// 可用空閑塊必須大於0
assert(n > 0);
#ifdef _USE_DEBUG
cprintf("avialable pages: %d\n", n);
#endif
// 設置可用空間大小
struct Page *p = base;
_buddy.property += n;
// 指數計數變量
register volatile int cnt;
// 進行切分插入
size_t size;
struct Page * bp;
while (n > 0) {
// 求剩下部分的最大2次冪
cnt = log2_round_down(n);
size = pow2(cnt);
n -= size;
// 記錄最大的2的次冪(初始化時只需要記錄一次,后面再進行修改)
if (_buddy.power == 0) {
_buddy.power = cnt;
}
// 更新空閑塊頭指針
bp = p;
for (; p != bp + size; p++) {
assert(PageReserved(p));
p->flags = p->property = 0;
set_page_ref(p, 0);
}
bp->property = size;
SetPageProperty(bp);
// 加入頭結點后(按從小到大的順序組織)
list_add(get_buddy_head(), &(bp->page_link));
// 拆分完成,跳出
if (n == 0) {
break;
}
}
#ifdef _USE_DEBUG
traverse_free_page_list();
#endif
}
--------------------------------------------------------------------------------------------
/*
buddy.h
**/
--------------------------------------------------------------------------------------------
/*code*/
#ifndef __KERN_MM_BUDDY_H__
#define __KERN_MM_BUDDY_H__
#include <pmm.h>
extern const struct pmm_manager buddy_pmm_manager;
#endif /* ! __KERN_MM_BUDDY_H__ */
--------------------------------------------------------------------------------------------
/*
buddy.c
**/
--------------------------------------------------------------------------------------------
/*code*/
#include <pmm.h>
#include <list.h>
#include <string.h>
#include <buddy.h>
free_area_t free_area;
#define free_list (free_area.free_list)
#define nr_free (free_area.nr_free)
// Global block
static size_t buddy_physical_size;
static size_t buddy_virtual_size;
static size_t buddy_segment_size;
static size_t buddy_alloc_size;
static size_t *buddy_segment;
static struct Page *buddy_physical;
static struct Page *buddy_alloc;
#define MIN(a,b) ((a)<(b)?(a):(b))
// Buddy operate
#define BUDDY_ROOT (1)
#define BUDDY_LEFT(a) ((a)<<1)
#define BUDDY_RIGHT(a) (((a)<<1)+1)
#define BUDDY_PARENT(a) ((a)>>1)
#define BUDDY_LENGTH(a) (buddy_virtual_size/UINT32_ROUND_DOWN(a))
#define BUDDY_BEGIN(a) (UINT32_REMAINDER(a)*BUDDY_LENGTH(a))
#define BUDDY_END(a) ((UINT32_REMAINDER(a)+1)*BUDDY_LENGTH(a))
#define BUDDY_BLOCK(a,b) (buddy_virtual_size/((b)-(a))+(a)/((b)-(a)))
#define BUDDY_EMPTY(a) (buddy_segment[(a)] == BUDDY_LENGTH(a))
// Bitwise operate
#define UINT32_SHR_OR(a,n) ((a)|((a)>>(n)))
#define UINT32_MASK(a) (UINT32_SHR_OR(UINT32_SHR_OR(UINT32_SHR_OR(UINT32_SHR_OR(UINT32_SHR_OR(a,1),2),4),8),16))
#define UINT32_REMAINDER(a) ((a)&(UINT32_MASK(a)>>1))
#define UINT32_ROUND_UP(a) (UINT32_REMAINDER(a)?(((a)-UINT32_REMAINDER(a))<<1):(a))
#define UINT32_ROUND_DOWN(a) (UINT32_REMAINDER(a)?((a)-UINT32_REMAINDER(a)):(a))
static void buddy_init_size(size_t n) {
assert(n > 1);
buddy_physical_size = n;
if (n < 512) {
buddy_virtual_size = UINT32_ROUND_UP(n-1);
buddy_segment_size = 1;
} else {
buddy_virtual_size = UINT32_ROUND_DOWN(n);
buddy_segment_size = buddy_virtual_size*sizeof(size_t)*2/PGSIZE;
if (n > buddy_virtual_size + (buddy_segment_size<<1)) {
buddy_virtual_size <<= 1;
buddy_segment_size <<= 1;
}
}
buddy_alloc_size = MIN(buddy_virtual_size, buddy_physical_size-buddy_segment_size);
}
static void buddy_init_segment(struct Page *base) {
// Init address
buddy_physical = base;
buddy_segment = KADDR(page2pa(base));
buddy_alloc = base + buddy_segment_size;
memset(buddy_segment, 0, buddy_segment_size*PGSIZE);
// Init segment
nr_free += buddy_alloc_size;
size_t block = BUDDY_ROOT;
size_t alloc_size = buddy_alloc_size;
size_t virtual_size = buddy_virtual_size;
buddy_segment[block] = alloc_size;
while (alloc_size > 0 && alloc_size < virtual_size) {
virtual_size >>= 1;
if (alloc_size > virtual_size) {
// Add left to free list
struct Page *page = &buddy_alloc[BUDDY_BEGIN(block)];
page->property = virtual_size;
list_add(&(free_list), &(page->page_link));
buddy_segment[BUDDY_LEFT(block)] = virtual_size;
// Switch ro right
alloc_size -= virtual_size;
buddy_segment[BUDDY_RIGHT(block)] = alloc_size;
block = BUDDY_RIGHT(block);
} else {
// Switch to left
buddy_segment[BUDDY_LEFT(block)] = alloc_size;
buddy_segment[BUDDY_RIGHT(block)] = 0;
block = BUDDY_LEFT(block);
}
}
if (alloc_size > 0) {
struct Page *page = &buddy_alloc[BUDDY_BEGIN(block)];
page->property = alloc_size;
list_add(&(free_list), &(page->page_link));
}
}
static void buddy_init(void) {
list_init(&free_list);
nr_free = 0;
}
static void buddy_init_memmap(struct Page *base, size_t n) {
assert(n > 0);
// Init pages
for (struct Page *p = base; p < base + n; p++) {
assert(PageReserved(p));
p->flags = p->property = 0;
}
// Init size
buddy_init_size(n);
// Init segment
buddy_init_segment(base);
}
static struct Page * buddy_alloc_pages(size_t n) {
assert(n > 0);
struct Page *page;
size_t block = BUDDY_ROOT;
size_t length = UINT32_ROUND_UP(n);
// Find block
while (length <= buddy_segment[block] && length < BUDDY_LENGTH(block)) {
size_t left = BUDDY_LEFT(block);
size_t right = BUDDY_RIGHT(block);
if (BUDDY_EMPTY(block)) { // Split
size_t begin = BUDDY_BEGIN(block);
size_t end = BUDDY_END(block);
size_t mid = (begin+end)>>1;
list_del(&(buddy_alloc[begin].page_link));
buddy_alloc[begin].property >>= 1;
buddy_alloc[mid].property = buddy_alloc[begin].property;
buddy_segment[left] = buddy_segment[block]>>1;
buddy_segment[right] = buddy_segment[block]>>1;
list_add(&free_list, &(buddy_alloc[begin].page_link));
list_add(&free_list, &(buddy_alloc[mid].page_link));
block = left;
} else if (length & buddy_segment[left]) { // Find in left (optimize)
block = left;
} else if (length & buddy_segment[right]) { // Find in right (optimize)
block = right;
} else if (length <= buddy_segment[left]) { // Find in left
block = left;
} else if (length <= buddy_segment[right]) {// Find in right
block = right;
} else { // Shouldn't be here
assert(0);
}
}
// Allocate
if (length > buddy_segment[block])
return NULL;
page = &(buddy_alloc[BUDDY_BEGIN(block)]);
list_del(&(page->page_link));
buddy_segment[block] = 0;
nr_free -= length;
// Update buddy segment
while (block != BUDDY_ROOT) {
block = BUDDY_PARENT(block);
buddy_segment[block] = buddy_segment[BUDDY_LEFT(block)] | buddy_segment[BUDDY_RIGHT(block)];
}
return page;
}
static void buddy_free_pages(struct Page *base, size_t n) {
assert(n > 0);
struct Page *p = base;
size_t length = UINT32_ROUND_UP(n);
// Find buddy id
size_t begin = (base-buddy_alloc);
size_t end = begin + length;
size_t block = BUDDY_BLOCK(begin, end);
// Release block
for (; p != base + n; p ++) {
assert(!PageReserved(p));
p->flags = 0;
set_page_ref(p, 0);
}
base->property = length;
list_add(&(free_list), &(base->page_link));
nr_free += length;
buddy_segment[block] = length;
// Upadte & merge
while (block != BUDDY_ROOT) {
block = BUDDY_PARENT(block);
size_t left = BUDDY_LEFT(block);
size_t right = BUDDY_RIGHT(block);
if (BUDDY_EMPTY(left) && BUDDY_EMPTY(right)) { // Merge
size_t lbegin = BUDDY_BEGIN(left);
size_t rbegin = BUDDY_BEGIN(right);
list_del(&(buddy_alloc[lbegin].page_link));
list_del(&(buddy_alloc[rbegin].page_link));
buddy_segment[block] = buddy_segment[left]<<1;
buddy_alloc[lbegin].property = buddy_segment[left]<<1;
list_add(&(free_list), &(buddy_alloc[lbegin].page_link));
} else { // Update
buddy_segment[block] = buddy_segment[BUDDY_LEFT(block)] | buddy_segment[BUDDY_RIGHT(block)];
}
}
}
static size_t buddy_nr_free_pages(void) {
return nr_free;
}
static void macro_check(void) {
// Block operate check
assert(BUDDY_ROOT == 1);
assert(BUDDY_LEFT(3) == 6);
assert(BUDDY_RIGHT(3) == 7);
assert(BUDDY_PARENT(6) == 3);
assert(BUDDY_PARENT(7) == 3);
size_t buddy_virtual_size_store = buddy_virtual_size;
size_t buddy_segment_root_store = buddy_segment[BUDDY_ROOT];
buddy_virtual_size = 16;
buddy_segment[BUDDY_ROOT] = 16;
assert(BUDDY_LENGTH(6) == 4);
assert(BUDDY_BEGIN(6) == 8);
assert(BUDDY_END(6) == 12);
assert(BUDDY_BLOCK(8, 12) == 6);
assert(BUDDY_EMPTY(BUDDY_ROOT));
buddy_virtual_size = buddy_virtual_size_store;
buddy_segment[BUDDY_ROOT] = buddy_segment_root_store;
// Bitwise operate check
assert(UINT32_SHR_OR(0xCC, 2) == 0xFF);
assert(UINT32_MASK(0x4000) == 0x7FFF);
assert(UINT32_REMAINDER(0x4321) == 0x321);
assert(UINT32_ROUND_UP(0x2321) == 0x4000);
assert(UINT32_ROUND_UP(0x2000) == 0x2000);
assert(UINT32_ROUND_DOWN(0x4321) == 0x4000);
assert(UINT32_ROUND_DOWN(0x4000) == 0x4000);
}
static void size_check(void) {
size_t buddy_physical_size_store = buddy_physical_size;
buddy_init_size(200);
assert(buddy_virtual_size == 256);
buddy_init_size(1024);
assert(buddy_virtual_size == 1024);
buddy_init_size(1026);
assert(buddy_virtual_size == 1024);
buddy_init_size(1028);
assert(buddy_virtual_size == 1024);
buddy_init_size(1030);
assert(buddy_virtual_size == 2048);
buddy_init_size(buddy_physical_size_store);
}
static void segment_check(void) {
// Check buddy segment
size_t total = 0, count = 0;
for (size_t block = BUDDY_ROOT; block < (buddy_virtual_size<<1); block++)
if (BUDDY_EMPTY(block))
total += BUDDY_LENGTH(block);
else if (block < buddy_virtual_size)
assert(buddy_segment[block] == (buddy_segment[BUDDY_LEFT(block)] | buddy_segment[BUDDY_RIGHT(block)]));
assert(total == nr_free_pages());
// Check free list
total = 0, count = 0;
list_entry_t *le = &free_list;
while ((le = list_next(le)) != &free_list) {
struct Page *p = le2page(le, page_link);
count ++, total += p->property;
}
assert(total == nr_free_pages());
}
static void alloc_check(void) {
// Build buddy system for test
size_t buddy_physical_size_store = buddy_physical_size;
for (struct Page *p = buddy_physical; p < buddy_physical + 1026; p++)
SetPageReserved(p);
buddy_init();
buddy_init_memmap(buddy_physical, 1026);
// Check allocation
struct Page *p0, *p1, *p2, *p3;
p0 = p1 = p2 = NULL;
assert((p0 = alloc_page()) != NULL);
assert((p1 = alloc_page()) != NULL);
assert((p2 = alloc_page()) != NULL);
assert((p3 = alloc_page()) != NULL);
assert(p0 + 1 == p1);
assert(p1 + 1 == p2);
assert(p2 + 1 == p3);
assert(page_ref(p0) == 0 && page_ref(p1) == 0 && page_ref(p2) == 0 && page_ref(p3) == 0);
assert(page2pa(p0) < npage * PGSIZE);
assert(page2pa(p1) < npage * PGSIZE);
assert(page2pa(p2) < npage * PGSIZE);
assert(page2pa(p3) < npage * PGSIZE);
list_entry_t *le = &free_list;
while ((le = list_next(le)) != &free_list) {
struct Page *p = le2page(le, page_link);
assert(buddy_alloc_pages(p->property) != NULL);
}
assert(alloc_page() == NULL);
// Check release
free_page(p0);
free_page(p1);
free_page(p2);
assert(nr_free == 3);
assert((p1 = alloc_page()) != NULL);
assert((p0 = alloc_pages(2)) != NULL);
assert(p0 + 2 == p1);
assert(alloc_page() == NULL);
free_pages(p0, 2);
free_page(p1);
free_page(p3);
struct Page *p;
assert((p = alloc_pages(4)) == p0);
assert(alloc_page() == NULL);
assert(nr_free == 0);
// Restore buddy system
for (struct Page *p = buddy_physical; p < buddy_physical + buddy_physical_size_store; p++)
SetPageReserved(p);
buddy_init();
buddy_init_memmap(buddy_physical, buddy_physical_size_store);
}
static void default_check(void) {
// Check buddy system
macro_check();
size_check();
segment_check();
alloc_check();
}
const struct pmm_manager buddy_pmm_manager = {
.name = "buddy_pmm_manager",
.init = buddy_init,
.init_memmap = buddy_init_memmap,
.alloc_pages = buddy_alloc_pages,
.free_pages = buddy_free_pages,
.nr_free_pages = buddy_nr_free_pages,
.check = default_check,
};
2、任意大小的內存單元slub分配算法(需要編程)
實際上 Slub 分配算法是非常復雜的,需要考慮緩存對齊、NUMA 等非常多的問題,作為實驗性質的操作系統就不考慮這些復雜因素了。簡化的 Slub 算法結合了 Slab 算法和 Slub 算法的部分特征,使用了一些比較右技巧性的實現方法。具體的簡化為:
- Slab 大小為一頁,不允許創建大對象倉庫
- 復用Page數據結構,將 Slab 元數據保存在 Page 結構體中
數據結構
在操作系統中經常會用到大量相同的數據對象,例如互斥鎖、條件變量等等,同種數據對象的初始化方法、銷毀方法、占用內存大小都是一樣的,如果操作系統能夠將所有的數據對象進行統一管理,可以提高內存利用率,同時也避免了反復初始化對象的開銷。
倉庫
每種對象由倉庫(感覺cache在這里翻譯為倉庫更好)進行統一管理:
struct kmem_cache_t {
list_entry_t slabs_full; // 全滿Slab鏈表
list_entry_t slabs_partial; // 部分空閑Slab鏈表
list_entry_t slabs_free; // 全空閑Slab鏈表
uint16_t objsize; // 對象大小
uint16_t num; // 每個Slab保存的對象數目
void (*ctor)(void*, struct kmem_cache_t *, size_t); // 構造函數
void (*dtor)(void*, struct kmem_cache_t *, size_t); // 析構函數
char name[CACHE_NAMELEN]; // 倉庫名稱
list_entry_t cache_link; // 倉庫鏈表
};
由於限制 Slab 大小為一頁,所以數據對象和每頁對象數據不會超過 ,所以使用 16 位整數保存足夠。然后所有的倉庫鏈接成一個鏈表,方便進行遍歷。
Slab
在上面的Buddy System中,一個物理頁被分配之后,Page 結構中除了 ref 之外的成員都沒有其他用處了,可以把Slab的元數據保存在這些內存中:
struct slab_t {
int ref; // 頁的引用次數(保留)
struct kmem_cache_t *cachep; // 倉庫對象指針
uint16_t inuse; // 已經分配對象數目
int16_t free; // 下一個空閑對象偏移量
list_entry_t slab_link; // Slab鏈表
};
為了方便空閑區域的管理,Slab 對應的內存頁分為兩部分:保存空閑信息的 bufcnt 以及可用內存區域 buf。
對象數據不會超過2048,所以 bufctl 中每個條目為 16 位整數。bufctl 中每個“格子”都對應着一個對象內存區域,不難發現,bufctl 保存的是一個隱式鏈表,格子中保存的內容就是下一個空閑區域的偏移,-1 表示不存在更多空閑區,slab_t 中的 free 就是鏈表頭部。
內置倉庫
除了可以自行管理倉庫之外,操作系統往往也提供了一些常見大小的倉庫,本文實現中內置了 8 個倉庫,倉庫對象大小為:8B、16B、32B、64B、128B、256B、512B、1024B。
操作函數
私有函數
void *kmem_cache_grow(struct kmem_cache_t *cachep);
申請一頁內存,初始化空閑鏈表 bufctl,構造 buf 中的對象,更新 Slab 元數據,最后將新的 Slab 加入到倉庫的空閑Slab表中。
void kmem_slab_destroy(struct kmem_cache_t *cachep, struct slab_t *slab);
析構 buf 中的對象后將內存頁歸還。
公共函數
void kmem_int();
初始化 kmem_cache_t 倉庫:由於 kmem_cache_t 也是由 Slab 算法分配的,所以需要預先手動初始化一個kmem_cache_t 倉庫;
初始化內置倉庫:初始化 8 個固定大小的內置倉庫。
kmem_cache_create(const char *name, size_t size, void (*ctor)(void*, struct kmem_cache_t *, size_t),void (*dtor)(void*, struct kmem_cache_t *, size_t));
從 kmem_cache_t 倉庫中獲得一個對象,初始化成員,最后將對象加入倉庫鏈表。其中需要注意的就是計算 Slab 中對象的數目,由於空閑表每一項占用 2 字節,所以每個 Slab 的對象數目就是:4096 字節/(2字節+對象大小)。
void kmem_cache_destroy(struct kmem_cache_t *cachep);
釋放倉庫中所有的 Slab,釋放 kmem_cache_t。
void *kmem_cache_alloc(struct kmem_cache_t *cachep);
先查找 slabs_partial,如果沒找到空閑區域則查找 slabs_free,還是沒找到就申請一個新的 slab。從 slab 分配一個對象后,如果 slab 變滿,那么將 slab 加入 slabs_full。
void *kmem_cache_zalloc(struct kmem_cache_t *cachep);
使用 kmem_cache_alloc 分配一個對象之后將對象內存區域初始化為零。
void kmem_cache_free(struct kmem_cache_t *cachep, void *objp);
將對象從 Slab 中釋放,也就是將對象空間加入空閑鏈表,更新 Slab 元信息。如果 Slab 變空,那么將 Slab 加入slabs_partial 鏈表。
size_t kmem_cache_size(struct kmem_cache_t *cachep);
獲得倉庫中對象的大小。
const char *kmem_cache_name(struct kmem_cache_t *cachep);
獲得倉庫的名稱。
int kmem_cache_shrink(struct kmem_cache_t *cachep);
將倉庫中 slabs_free 中所有 Slab 釋放。
int kmem_cache_reap();
遍歷倉庫鏈表,對每一個倉庫進行 kmem_cache_shrink 操作。
void *kmalloc(size_t size);
找到大小最合適的內置倉庫,申請一個對象。
void kfree(const void *objp);
釋放內置倉庫對象。
size_t ksize(const void *objp);
獲得倉庫對象大小。
實現過程如下:
--------------------------------------------------------------------------------------------
/*
slub.h
**/
--------------------------------------------------------------------------------------------
/*code*/
#ifndef __KERN_MM_SLUB_H__
#define __KERN_MM_SLUB_H__
#include <pmm.h>
#include <list.h>
#define CACHE_NAMELEN 16
struct kmem_cache_t {
list_entry_t slabs_full;
list_entry_t slabs_partial;
list_entry_t slabs_free;
uint16_t objsize;
uint16_t num;
void (*ctor)(void*, struct kmem_cache_t *, size_t);
void (*dtor)(void*, struct kmem_cache_t *, size_t);
char name[CACHE_NAMELEN];
list_entry_t cache_link;
};
struct kmem_cache_t *
kmem_cache_create(const char *name, size_t size,
void (*ctor)(void*, struct kmem_cache_t *, size_t),
void (*dtor)(void*, struct kmem_cache_t *, size_t));
void kmem_cache_destroy(struct kmem_cache_t *cachep);
void *kmem_cache_alloc(struct kmem_cache_t *cachep);
void *kmem_cache_zalloc(struct kmem_cache_t *cachep);
void kmem_cache_free(struct kmem_cache_t *cachep, void *objp);
size_t kmem_cache_size(struct kmem_cache_t *cachep);
const char *kmem_cache_name(struct kmem_cache_t *cachep);
int kmem_cache_shrink(struct kmem_cache_t *cachep);
int kmem_cache_reap();
void *kmalloc(size_t size);
void kfree(void *objp);
size_t ksize(void *objp);
void kmem_int();
#endif /* ! __KERN_MM_SLUB_H__ */
--------------------------------------------------------------------------------------------
/*
slub.c
**/
--------------------------------------------------------------------------------------------
/*code*/
#include <slub.h>
#include <list.h>
#include <defs.h>
#include <string.h>
#include <stdio.h>
struct slab_t {
int ref;
struct kmem_cache_t *cachep;
uint16_t inuse;
uint16_t free;
list_entry_t slab_link;
};
// The number of sized cache : 16, 32, 64, 128, 256, 512, 1024, 2048
#define SIZED_CACHE_NUM 8
#define SIZED_CACHE_MIN 16
#define SIZED_CACHE_MAX 2048
#define le2slab(le,link) ((struct slab_t*)le2page((struct Page*)le,link))
#define slab2kva(slab) (page2kva((struct Page*)slab))
static list_entry_t cache_chain;
static struct kmem_cache_t cache_cache;
static struct kmem_cache_t *sized_caches[SIZED_CACHE_NUM];
static char *cache_cache_name = "cache";
static char *sized_cache_name = "sized";
// kmem_cache_grow - add a free slab
static void * kmem_cache_grow(struct kmem_cache_t *cachep) {
struct Page *page = alloc_page();
void *kva = page2kva(page);
// Init slub meta data
struct slab_t *slab = (struct slab_t *) page;
slab->cachep = cachep;
slab->inuse = slab->free = 0;
list_add(&(cachep->slabs_free), &(slab->slab_link));
// Init bufctl
int16_t *bufctl = kva;
for (int i = 1; i < cachep->num; i++)
bufctl[i-1] = i;
bufctl[cachep->num-1] = -1;
// Init cache
void *buf = bufctl + cachep->num;
if (cachep->ctor)
for (void *p = buf; p < buf + cachep->objsize * cachep->num; p += cachep->objsize)
cachep->ctor(p, cachep, cachep->objsize);
return slab;
}
// kmem_slab_destroy - destroy a slab
static void kmem_slab_destroy(struct kmem_cache_t *cachep, struct slab_t *slab) {
// Destruct cache
struct Page *page = (struct Page *) slab;
int16_t *bufctl = page2kva(page);
void *buf = bufctl + cachep->num;
if (cachep->dtor)
for (void *p = buf; p < buf + cachep->objsize * cachep->num; p += cachep->objsize)
cachep->dtor(p, cachep, cachep->objsize);
// Return slub page
page->property = page->flags = 0;
list_del(&(page->page_link));
free_page(page);
}
static int kmem_sized_index(size_t size) {
// Round up
size_t rsize = ROUNDUP(size, 2);
if (rsize < SIZED_CACHE_MIN)
rsize = SIZED_CACHE_MIN;
// Find index
int index = 0;
for (int t = rsize / 32; t; t /= 2)
index ++;
return index;
}
// ! Test code
#define TEST_OBJECT_LENTH 2046
#define TEST_OBJECT_CTVAL 0x22
#define TEST_OBJECT_DTVAL 0x11
static const char *test_object_name = "test";
struct test_object {
char test_member[TEST_OBJECT_LENTH];
};
static void test_ctor(void* objp, struct kmem_cache_t * cachep, size_t size) {
char *p = objp;
for (int i = 0; i < size; i++)
p[i] = TEST_OBJECT_CTVAL;
}
static void test_dtor(void* objp, struct kmem_cache_t * cachep, size_t size) {
char *p = objp;
for (int i = 0; i < size; i++)
p[i] = TEST_OBJECT_DTVAL;
}
static size_t list_length(list_entry_t *listelm) {
size_t len = 0;
list_entry_t *le = listelm;
while ((le = list_next(le)) != listelm)
len ++;
return len;
}
static void check_kmem() {
assert(sizeof(struct Page) == sizeof(struct slab_t));
size_t fp = nr_free_pages();
// Create a cache
struct kmem_cache_t *cp0 = kmem_cache_create(test_object_name, sizeof(struct test_object), test_ctor, test_dtor);
assert(cp0 != NULL);
assert(kmem_cache_size(cp0) == sizeof(struct test_object));
assert(strcmp(kmem_cache_name(cp0), test_object_name) == 0);
// Allocate six objects
struct test_object *p0, *p1, *p2, *p3, *p4, *p5;
char *p;
assert((p0 = kmem_cache_alloc(cp0)) != NULL);
assert((p1 = kmem_cache_alloc(cp0)) != NULL);
assert((p2 = kmem_cache_alloc(cp0)) != NULL);
assert((p3 = kmem_cache_alloc(cp0)) != NULL);
assert((p4 = kmem_cache_alloc(cp0)) != NULL);
p = (char *) p4;
for (int i = 0; i < sizeof(struct test_object); i++)
assert(p[i] == TEST_OBJECT_CTVAL);
assert((p5 = kmem_cache_zalloc(cp0)) != NULL);
p = (char *) p5;
for (int i = 0; i < sizeof(struct test_object); i++)
assert(p[i] == 0);
assert(nr_free_pages()+3 == fp);
assert(list_empty(&(cp0->slabs_free)));
assert(list_empty(&(cp0->slabs_partial)));
assert(list_length(&(cp0->slabs_full)) == 3);
// Free three objects
kmem_cache_free(cp0, p3);
kmem_cache_free(cp0, p4);
kmem_cache_free(cp0, p5);
assert(list_length(&(cp0->slabs_free)) == 1);
assert(list_length(&(cp0->slabs_partial)) == 1);
assert(list_length(&(cp0->slabs_full)) == 1);
// Shrink cache
assert(kmem_cache_shrink(cp0) == 1);
assert(nr_free_pages()+2 == fp);
assert(list_empty(&(cp0->slabs_free)));
p = (char *) p4;
for (int i = 0; i < sizeof(struct test_object); i++)
assert(p[i] == TEST_OBJECT_DTVAL);
// Reap cache
kmem_cache_free(cp0, p0);
kmem_cache_free(cp0, p1);
kmem_cache_free(cp0, p2);
assert(kmem_cache_reap() == 2);
assert(nr_free_pages() == fp);
// Destory a cache
kmem_cache_destroy(cp0);
// Sized alloc
assert((p0 = kmalloc(2048)) != NULL);
assert(nr_free_pages()+1 == fp);
kfree(p0);
assert(kmem_cache_reap() == 1);
assert(nr_free_pages() == fp);
cprintf("check_kmem() succeeded!\n");
}
// ! End of test code
// kmem_cache_create - create a kmem_cache
struct kmem_cache_t * kmem_cache_create(const char *name, size_t size,
void (*ctor)(void*, struct kmem_cache_t *, size_t),
void (*dtor)(void*, struct kmem_cache_t *, size_t)) {
assert(size <= (PGSIZE - 2));
struct kmem_cache_t *cachep = kmem_cache_alloc(&(cache_cache));
if (cachep != NULL) {
cachep->objsize = size;
cachep->num = PGSIZE / (sizeof(int16_t) + size);
cachep->ctor = ctor;
cachep->dtor = dtor;
memcpy(cachep->name, name, CACHE_NAMELEN);
list_init(&(cachep->slabs_full));
list_init(&(cachep->slabs_partial));
list_init(&(cachep->slabs_free));
list_add(&(cache_chain), &(cachep->cache_link));
}
return cachep;
}
// kmem_cache_destroy - destroy a kmem_cache
void kmem_cache_destroy(struct kmem_cache_t *cachep) {
list_entry_t *head, *le;
// Destory full slabs
head = &(cachep->slabs_full);
le = list_next(head);
while (le != head) {
list_entry_t *temp = le;
le = list_next(le);
kmem_slab_destroy(cachep, le2slab(temp, page_link));
}
// Destory partial slabs
head = &(cachep->slabs_partial);
le = list_next(head);
while (le != head) {
list_entry_t *temp = le;
le = list_next(le);
kmem_slab_destroy(cachep, le2slab(temp, page_link));
}
// Destory free slabs
head = &(cachep->slabs_free);
le = list_next(head);
while (le != head) {
list_entry_t *temp = le;
le = list_next(le);
kmem_slab_destroy(cachep, le2slab(temp, page_link));
}
// Free kmem_cache
kmem_cache_free(&(cache_cache), cachep);
}
// kmem_cache_alloc - allocate an object
void * kmem_cache_alloc(struct kmem_cache_t *cachep) {
list_entry_t *le = NULL;
// Find in partial list
if (!list_empty(&(cachep->slabs_partial)))
le = list_next(&(cachep->slabs_partial));
// Find in empty list
else {
if (list_empty(&(cachep->slabs_free)) && kmem_cache_grow(cachep) == NULL)
return NULL;
le = list_next(&(cachep->slabs_free));
}
// Alloc
list_del(le);
struct slab_t *slab = le2slab(le, page_link);
void *kva = slab2kva(slab);
int16_t *bufctl = kva;
void *buf = bufctl + cachep->num;
void *objp = buf + slab->free * cachep->objsize;
// Update slab
slab->inuse ++;
slab->free = bufctl[slab->free];
if (slab->inuse == cachep->num)
list_add(&(cachep->slabs_full), le);
else
list_add(&(cachep->slabs_partial), le);
return objp;
}
// kmem_cache_zalloc - allocate an object and fill it with zero
void * kmem_cache_zalloc(struct kmem_cache_t *cachep) {
void *objp = kmem_cache_alloc(cachep);
memset(objp, 0, cachep->objsize);
return objp;
}
// kmem_cache_free - free an object
void kmem_cache_free(struct kmem_cache_t *cachep, void *objp) {
// Get slab of object
void *base = page2kva(pages);
void *kva = ROUNDDOWN(objp, PGSIZE);
struct slab_t *slab = (struct slab_t *) &pages[(kva-base)/PGSIZE];
// Get offset in slab
int16_t *bufctl = kva;
void *buf = bufctl + cachep->num;
int offset = (objp - buf) / cachep->objsize;
// Update slab
list_del(&(slab->slab_link));
bufctl[offset] = slab->free;
slab->inuse --;
slab->free = offset;
if (slab->inuse == 0)
list_add(&(cachep->slabs_free), &(slab->slab_link));
else
list_add(&(cachep->slabs_partial), &(slab->slab_link));
}
// kmem_cache_size - get object size
size_t kmem_cache_size(struct kmem_cache_t *cachep) {
return cachep->objsize;
}
// kmem_cache_name - get cache name
const char * kmem_cache_name(struct kmem_cache_t *cachep) {
return cachep->name;
}
// kmem_cache_shrink - destroy all slabs in free list
int kmem_cache_shrink(struct kmem_cache_t *cachep) {
int count = 0;
list_entry_t *le = list_next(&(cachep->slabs_free));
while (le != &(cachep->slabs_free)) {
list_entry_t *temp = le;
le = list_next(le);
kmem_slab_destroy(cachep, le2slab(temp, page_link));
count ++;
}
return count;
}
// kmem_cache_reap - reap all free slabs
int kmem_cache_reap() {
int count = 0;
list_entry_t *le = &(cache_chain);
while ((le = list_next(le)) != &(cache_chain))
count += kmem_cache_shrink(to_struct(le, struct kmem_cache_t, cache_link));
return count;
}
void * kmalloc(size_t size) {
assert(size <= SIZED_CACHE_MAX);
return kmem_cache_alloc(sized_caches[kmem_sized_index(size)]);
}
void kfree(void *objp) {
void *base = slab2kva(pages);
void *kva = ROUNDDOWN(objp, PGSIZE);
struct slab_t *slab = (struct slab_t *) &pages[(kva-base)/PGSIZE];
kmem_cache_free(slab->cachep, objp);
}
void kmem_int() {
// Init cache for kmem_cache
cache_cache.objsize = sizeof(struct kmem_cache_t);
cache_cache.num = PGSIZE / (sizeof(int16_t) + sizeof(struct kmem_cache_t));
cache_cache.ctor = NULL;
cache_cache.dtor = NULL;
memcpy(cache_cache.name, cache_cache_name, CACHE_NAMELEN);
list_init(&(cache_cache.slabs_full));
list_init(&(cache_cache.slabs_partial));
list_init(&(cache_cache.slabs_free));
list_init(&(cache_chain));
list_add(&(cache_chain), &(cache_cache.cache_link));
// Init sized cache
for (int i = 0, size = 16; i < SIZED_CACHE_NUM; i++, size *= 2)
sized_caches[i] = kmem_cache_create(sized_cache_name, size, NULL, NULL);
check_kmem();
}