Lab:page tables
在這個lab中6.1810 / Fall 2025,要求我們先閲讀xv6課本的Chapter 3 Page tables(第三章)。要求我們探索xv6當中關於頁表的內容。並且要求我們實現一些頁表相關功能的實現(例如:虛地址和物理地址的映射/解除映射,頁表的創建和釋放等)。
並且官網也給出了提示:
- 在
kernel/memlayout.h當中存放了內存佈局,頁表大小相關的常量就在此。 - 在
kernel/vm.c當中是頁表相關邏輯的實現,接下來的大部分lab內容就在此實現。 - 在
kernel/kalloc.c當中存放的時內存分配相關的邏輯,在新建/刪除頁表時會用到這裏的函數。
Speed up system calls (簡單)
在這個lab當中,要求我們在 xv6 中添加一個新的 用户可讀的只讀內存映射(USYSCALL),用來讓用户態程序在不陷入內核的情況下,直接讀取部分內核數據(如 pid),並正確處理其 創建、映射、訪問與釋放的完整生命週期。
如何將一個用户可讀的只讀內存映射(USYSCALL)添加到進程頁表內?以及如何刪除該映射?
前言和注意事項:在xv6當中的有關進程的創建/釋放,進程頁表的創建/釋放的過程都在kernel/proc.h,並且按照官網的説法,我們需要將進程的pid存放到內存當中,這樣在調用gitpid系統調用時,則直接選擇從內存空間當中讀取該pid,大大提高了執行效率,並且不用陷入到內核態;這就意味着我們需要在進程的結構體當中添加一個成員用於指向存放當前進程的pid的空間,為了之後的讀取。
一、分配物理內存:
前面提到過,進程的結構體成員當中有指向進程pid的指針(struct usyscall *),因此,我們需要先給他分配物理內存(由內核分配)。
p->usyscall = (struct usyscall *)kalloc(); //分配物理內存
二、初始化內容:
將當前進程的pid存放到剛才的指針p->usyscall所指向的空間中。
p->usyscall->pid = p->pid;
// 以下是xv6提前寫好的,改進後的ugetpid方法
int
ugetpid(void)
{
struct usyscall *u = (struct usyscall *)USYSCALL; //通過虛擬地址USYSCALL訪問特點內存
return u->pid;
}
為什麼我們必須通過struct usyscall *來訪問,而不是直接返回進程結構體當中的pid呢?
答:首先,xv6有內核頁表和用户頁表,並且用户態下的進程只能看得見內存。因為進程的結構體存放在內核頁表當中,在用户態下我們只能訪問到用户頁表,所以準確來説我們只能通過虛擬內存搭配頁表機制的方式來訪問存放在該物理空間當中內容。我們在內核態下通過p->usyscall = (struct usyscall *)kalloc(); 分配的內存似乎也是被內核所管理,但是我們將USYSCALL這個虛擬地址和物理地址相映射了起來,因此我們可以通過在用户態下訪問該虛擬地址的方式下訪問到具體的物理地址當中的值。
三、創建用户頁表:
眾所周知,OS當中的進程採用頁表機制來將進程的虛地址映射到物理地址上,所以説無論我們是否要添加映射到頁表中,我們都必不可免地要創建一個用户頁表。
p->pagetable = proc_pagetable(p);
四、建立虛擬地址 到 物理地址映射:
説白了就是在用户頁表中添加一個新的頁表項,所以這一步的操作要在頁表的相關邏輯當中進行,該頁表項用於映射到剛才分配的物理內存。在kernel/defs.h當中,我們可以看到mappages的聲明(該函數用於添加映射到頁表)。
注意:頁表機制是將進程的虛擬地址映射為內存中真實的物理地址,所以在添加新的映射時,要一併給出這些參數以及映射大小和權限。
// 映射 USYSCALL
if(mappages(pagetable,
USYSCALL, //虛擬地址
PGSIZE, // 映射大小
(uint64)p->usyscall, //物理地址
PTE_R | PTE_U | PTE_V) < 0){ // 官網要求設置的權限
uvmfree(pagetable, 0);
return 0;
}
xv6的權限(添加權限的目的是防止“篡改”,“非法訪問”等等操作):
| 位 | 含義 |
|---|---|
| PTE_R | 用户可讀 |
| PTE_W | 防止用户寫 |
| PTE_X | 防止執行 |
| PTE_U | 用户態可訪問 |
| PTE_V | 映射有效 |
五、刪除/釋放映射:
首先在頁表釋放的相關邏輯當中進行釋放映射的操作,在kernel/defs.h當中,我們可以看到uvmunmap的聲明(該函數用於刪除/釋放映射到頁表)。
uvmunmap(pagetable, USYSCALL, 1, 0); //釋放USYSCALL
之後在進程釋放的相關邏輯進行釋放之前訪問的物理空間的操作,在kernel/defs.h當中,我們可以看到kfree的聲明(該函數用於釋放分配的內存)。
kfree(p->usyscall);
六、深入瞭解進程和頁表的底層邏輯:
| 函數(kernel/proc.c) | 負責什麼 |
|---|---|
allocproc |
分配“進程資源”(pid、usyscall、trapframe、kstack)(第一,二,三步在此進行) |
freeproc |
釋放“進程資源” (第五步後半部分在此進行) |
proc_pagetable |
構造頁表結構 (第五步前半部分在此進行) |
proc_freepagetable |
拆除頁表結構 (第四步在此進行) |
由此我們可以得知頁表的生命週期幾乎伴隨整個進程。
代碼的相關內容:
/* kernel/proc.c */
static struct proc*
allocproc(void)
{
struct proc *p;
for(p = proc; p < &proc[NPROC]; p++) {
acquire(&p->lock);
if(p->state == UNUSED) {
goto found;
} else {
release(&p->lock);
}
}
return 0;
found:
p->pid = allocpid();
p->state = USED;
// 分配物理內存
p->usyscall = (struct usyscall *)kalloc();
if(p->usyscall == 0){
freeproc(p);
release(&p->lock);
return 0;
}
// 初始化內容
p->usyscall->pid = p->pid;
// Allocate a trapframe page.
if((p->trapframe = (struct trapframe *)kalloc()) == 0){
freeproc(p);
release(&p->lock);
return 0;
}
// An empty user page table.
p->pagetable = proc_pagetable(p);
if(p->pagetable == 0){
freeproc(p);
release(&p->lock);
return 0;
}
// Set up new context to start executing at forkret,
// which returns to user space.
memset(&p->context, 0, sizeof(p->context));
p->context.ra = (uint64)forkret;
p->context.sp = p->kstack + PGSIZE;
return p;
}
// free a proc structure and the data hanging from it,
// including user pages.
// p->lock must be held.
static void
freeproc(struct proc *p)
{
// 釋放之前分配的物理內存
if(p->usyscall){
kfree((void*)p->usyscall);
p->usyscall = 0;
}
if(p->trapframe)
kfree((void*)p->trapframe);
p->trapframe = 0;
if(p->pagetable)
proc_freepagetable(p->pagetable, p->sz);
p->pagetable = 0;
p->sz = 0;
p->pid = 0;
p->parent = 0;
p->name[0] = 0;
p->chan = 0;
p->killed = 0;
p->xstate = 0;
p->state = UNUSED;
}
// Create a user page table for a given process, with no user memory,
// but with trampoline and trapframe pages.
pagetable_t
proc_pagetable(struct proc *p)
{
pagetable_t pagetable;
// An empty page table.
pagetable = uvmcreate();
if(pagetable == 0)
return 0;
// 映射 USYSCALL(也是關鍵部分)
if(mappages(pagetable,
USYSCALL, //虛擬地址
PGSIZE, // 映射大小
(uint64)p->usyscall, //物理地址
PTE_R | PTE_U | PTE_V) < 0){ // 權限
uvmfree(pagetable, 0);
return 0;
}
// map the trampoline code (for system call return)
// at the highest user virtual address.
// only the supervisor uses it, on the way
// to/from user space, so not PTE_U.
if(mappages(pagetable, TRAMPOLINE, PGSIZE,
(uint64)trampoline, PTE_R | PTE_X) < 0){
uvmfree(pagetable, 0);
return 0;
}
// map the trapframe page just below the trampoline page, for
// trampoline.S.
if(mappages(pagetable, TRAPFRAME, PGSIZE,
(uint64)(p->trapframe), PTE_R | PTE_W) < 0){
uvmunmap(pagetable, TRAMPOLINE, 1, 0);
uvmfree(pagetable, 0);
return 0;
}
return pagetable;
}
// Free a process's page table, and free the
// physical memory it refers to.
void
proc_freepagetable(pagetable_t pagetable, uint64 sz)
{
uvmunmap(pagetable, USYSCALL, 1, 0); //釋放/刪除USYSCALL對應的映射
uvmunmap(pagetable, TRAMPOLINE, 1, 0);
uvmunmap(pagetable, TRAPFRAME, 1, 0);
uvmfree(pagetable, sz);
}
Print a page table (簡單)
這個lab要求我們實現一個打印頁表的函數,同時也能幫助我們理解xv6當中,頁表是如何實現的。在本次實驗前,這門課程的作者已經將kpgtbl()這個系統調用添加到內核當中了,現在我們要做的就是完善kernel/vm.c當中的vmprint()函數,這個函數接收一個pagetable_t(頁表類型)的參數。
xv6當中的頁表是怎樣的?
零、專業詞彙闡述
| 名稱 | 含義 |
|---|---|
| VA | 虛擬地址(CPU 使用) |
| PTE | 頁表項(映射 + 權限) |
| PA | 物理地址(RAM 索引) |
| PPN | 物理頁號(PA 的高位) |
一、虛擬地址va的結構和xv6當中的三級頁表
根據本課程對應的課本xv6 book 當中的第三章,我們可以得知在xv6當中,虛擬地址va的位數為64位,並且我們只使用低39位,高25位用於擴展。相信你在看到這裏時肯定學過操作系統這門課程,在任何一本操作系統的教科書當中,對於虛擬地址va的構成的描述都是低n位是頁內偏移地址,用於定位某頁內的頁表項,剩下的高位都是索引,用於定位到某一頁。
在xv6當中,頁表的每頁大小為4096B,每個頁表項(PTE)的大小為8B,所以一個頁表的當中有4096/8 = 512個PTE。所以39位的虛擬地址va當中,低12位為頁內偏移量,省下的27位用於索引頁表。
xv6採用三級頁表,也就是説27位的索引地址,每9位構成一個層級,類似一個樹。以下內容是39位虛擬地址的構成。
PS:(牀圖網站隨時可能失效,所以下面我儘量使用文字來進行描述)。
|VPN[2] | VPN[1] | VPN[0]|頁內偏移|
9 9 9 12 共39位
一級索引 二級索引 三級索引 頁內偏移量 總位數
根 葉子
尋址時,先訪問VPN[2]當中的某個PTE,該PTE指向VPN[1],之後從VPN[1]中選取新的PTE,再次通過新的PTE尋址VPN[0],用VPN[0]獲得最終的PTE後即可獲得PNN(物理頁號)。最後通過對PNN操作得到PA(物理地址)。整個過程類似尋找樹的葉子結點那樣,一層一層向下尋找。
二、為什麼xv6採用三級頁表?
進程在創建之初,必須且至少擁有一個頁表。
如果採用一級頁表設計,為了滿足這一必須的條件,操作系統必須一次性分配一張覆蓋整個虛擬地址空間的頁表,即使進程只使用其中極小的一部分(大部分內存空間會浪費掉),也必須遵守該規定。
而在採用三級頁表的設計中,進程創建時只需要分配一個 4KB 的根頁表頁,其餘頁表頁在虛擬地址空間被實際使用時才按需分配。
二、PTE的內容
已知每個PTE的大小為8B,即一共64位。其中低10位(90位)為flags(權限位/標記),剩下的高位(5310位共44bit)為PNN(物理頁框號,分配內存之時,OS從空閒頁框表當中的表頭取下來的)。最後的10位(63~54位)暫時未用,置為0。
flags的內容:
| 位 | 含義 |
|---|---|
| V | 是否有效 |
| R | 可讀 |
| W | 可寫 |
| X | 可執行 |
| U | 用户可訪問 |
| A/D | 硬件訪問/修改標記 |
當一個 PTE 的 R/W/X 任一位為 1 時,該 PTE 是葉子結點,指向真實物理頁;
若 R/W/X 全為 0 且 V=1,則該 PTE 指向下一級頁表。
頁表的本質除了指明虛擬地址映射到哪裏外,還可以決定這個地址是否可讀,是否可寫,是否可執行,是否可用户態訪問/執行。
三、PNN如何轉為物理地址
xv6當中規定物理地址的位數為56位,由PNN和va的低12位拼接而成,具體操作手法如下:
1、首先講PTE右移10位,這樣低10位的flags會消失。
2、之後講PTE左移12位,這樣低12位的空白正好可以由虛擬地址的低12位偏移量進行填補。
3、我們現在需要將虛擬地址va的第12位進行填補,所以我們將va和0xFFF相與,這樣va就只剩下了第12位的偏移量。
4、將PTE和va相加或着進行“邏輯或”操作,這樣就拼接好了一個完整的物理地址。
注意:在xv6當中,以上的操作都有着對應的宏,在編碼時可以直接使用宏操作。
該lab的實現和代碼相關內容
一、個人的解析和官網提示
- 打印格式:第一行顯示 vmprint 的參數。之後,每個頁表項(PTE)對應一行,包括那些指向樹中更深層次頁表頁的頁表項。每個頁表項行都縮進若干個 “..”,以表示其在樹中的深度。每個頁表項行都會顯示其虛擬地址、頁表項位以及從該頁表項中提取的物理地址。不要打印無效的頁表項。
- 在
kernel/riscv.h的文件末尾,有關於va轉pa的宏。 freewalk這個函數也許會帶來啓發。- 在printf調用中使用%p,以官網上示例所示的方式打印完整的64位十六進制頁表項(PTE)和地址。
二、代碼相關內容
##在kernel/vm.c文件內:
static void
vmprint_walk(pagetable_t pagetable, int level, uint64 va){
//每個頁表521個PTE
for(int i = 0; i < 512; i++){
pte_t pte = pagetable[i];
// pte有效 並且 V位為1則不是葉子結點
if((pte & PTE_V) == 0)
continue;
// 將傳入的PA物理地址(此時PA第12位為空)和偏移量相加合併為完整的物理地址
uint64 newva = va | ((uint64)i << (12 + 9 * level));
// 打印層級, depth = 2 - level
for(int d = 0; d < 2 - level; d++)
printf(" ..");
printf("%p: pte %p pa %p\n",
(void*)newva,
(void*)pte,
(void*)PTE2PA(pte));
// 不是葉子結點則向下遞歸
if((pte & (PTE_R | PTE_W | PTE_X)) == 0){
// PTE2PA是將pte轉為了物理地址PA(此時低12位為空)
pagetable_t child = (pagetable_t)PTE2PA(pte);
vmprint_walk(child, level - 1, newva);
}
}
}
#if defined(LAB_PGTBL) || defined(SOL_MMAP) || defined(SOL_COW)
void
vmprint(pagetable_t pagetable) {
// your code here
// 打印第一行,之後遞歸進行遍歷
printf("page table %p\n", pagetable);
vmprint_walk(pagetable, 2, 0);
}
#endif
Use superpages (困難)
這個lab可以説是最難的lab。卡了我快20個小時。當用户通過sbrk()申請內存時,如果申請的內存≥2MB時,xv6不再使用傳統的三級頁表(即大小為4K的頁),而是採用二級頁表(即1個2MB的超級頁)。並且相關的函數也要適配處理超級頁的功能。
採用超級頁後的地址結構如下:
|VPN[2] | VPN[1](包含VPN[0])|頁內偏移|
9 9 9 12 共39位
一級索引 二級索引 頁內偏移量 總位數
根 葉子
===============================
level-2 (512GB)
|
level-1 (2MB) ← ★ superpage 在這裏(第一層)
|
level-0 (4KB) ← 普通的頁面在這裏(第0層)
起始該lab的某些地方的寫法是有跡可循的,你可以直接照搬之前原因的部分代碼。
順騰摸瓜尋找需要修改的內容
一、在kernel/kalloc.c文件當中的函數是負責分配頁表內存的,目前這裏只有普通頁的內容,我們需要添加超級頁的相關內容。在kmem中添加一個run結構,讓其指向一個超級頁的空閒頁表。 之後在freerange函數當中仿照普通頁的內存分配邏輯,照葫蘆畫瓢寫一個超級頁的內存分配邏輯。同時仿照kfree和kalloc寫一個superalloc和superfree,這兩個分別是超級頁的分配和釋放。
二、sbrk()當中調用了growproc()函數,使用參數n調整內存的大小。當n為有效值時則調用uvmalloc函數來對用户進行虛擬內存的分配(這裏需要修改uvmalloc)。進一步進入uvmalloc函數當中,其中涉及了mappages函數,該函數負責為每個頁表項映射物理地址(這裏需要修改mappages);同時也涉及了uvmdealloc函數,該函數的功能是釋放用户頁面,其內部涉及uvmunmap函數,這個函數是頁面釋放的具體實現(這裏需要修改uvmunmap)。在mappages函數當中涉及了walk函數,該函數負責返回虛擬地址 va 對應的頁表項(PTE)的地址(這裏需要修改walk)。
三、官網説了,通過用户程序pgtbltest來測試超級頁功能是否完成,所以我們順藤摸瓜在kernel/pgtbltest.c當中發現superpg_kfork函數調用了fork進程來創建新進程,打算讓新的進程採用超級頁。所以我們再次順騰摸瓜找到了kfork函數,裏面涉及了uvmcopy函數,這個函數負責將父進程的頁表複製給子進程(把父進程的數據拷貝一份給子進程),(這裏需要修改uvmcopy)。
代碼相關內容
這一小節本人一開始沒做出來,因此參考了很多大佬的博客和視頻才得以做出,以下代碼參考了這位大佬的博客→mit6.1810]Lab3: page tables。
1、在kalloc.c當中照葫蘆畫瓢添加對超級頁的管理。
struct {
struct spinlock lock;
struct run *freelist;
struct run *superfreelist; // 仿照上面的freelist
} kmem;
void
freerange(void *pa_start, void *pa_end)
{
char *p;
p = (char*)PGROUNDUP((uint64)pa_start);
#ifndef LAB_PGTBL
for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE)
kfree(p);
#else
int superpg_num = 10;
// 計算超級頁的起始地址,從 pa_end 向下對齊到超級頁邊界
char *superp = (char*)SUPERPGROUNDUP((uint64)pa_end - superpg_num * SUPERPGSIZE);
// 先釋放普通頁面部分
for(; p + PGSIZE <= superp; p += PGSIZE)
kfree(p);
// 再釋放超級頁部分
for(; superp + SUPERPGSIZE <= (char*)pa_end; superp += SUPERPGSIZE)
superfree(superp);
#endif
}
#ifdef LAB_PGTBL
// 超級頁釋放函數
void
superfree(void *pa)
{
struct run *r;
// 參數驗證:確保 pa 對齊到超級頁大小且在合法內存範圍內
if(((uint64)pa % SUPERPGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
panic("superfree");
memset(pa, 1, SUPERPGSIZE);
r = (struct run*)pa;
//加鎖
acquire(&kmem.lock);
r->next = kmem.superfreelist;
// 將超級頁插入空閒鏈表頭部
kmem.superfreelist = r;
//解鎖
release(&kmem.lock);
}
// 超級頁分配函數
void *
superalloc(void)
{
struct run *r;
acquire(&kmem.lock);
// 從空閒鏈表中取出一個超級頁
r = kmem.superfreelist;
if(r)
kmem.superfreelist = r->next;
release(&kmem.lock);
if(r)
memset((char*)r, 5, SUPERPGSIZE);
// 返回分配的超級頁地址
return (void*)r;
}
#endif
2、kalloc.h當中,我們給普通頁分配內存時用到了PGROUNDUP,於是超級頁的內存分配也需要類似的內容。我們順騰摸瓜找到riscv.h,在裏面仿照PGROUNDUP和PGROUNDDOWN,新增SUPERPGROUNDUP和SUPERPGROUNDDOWN。
#define SUPERPGROUNDUP(sz) (((sz)+SUPERPGSIZE-1) & ~(SUPERPGSIZE-1))
#define SUPERPGROUNDDOWN(a) (((a)) & ~(SUPERPGSIZE-1))
3、在defs.h中添加下剛才的新增的聲明。
void * superalloc(void);
void superfree(void *pa);
pte_t * superwalk(pagetable_t, uint64, int, int *);
接下來的內容都在kernel/vm.c當中實現。
4、添加uvmalloc函數。
uint64
uvmalloc(pagetable_t pagetable, uint64 oldsz, uint64 newsz, int xperm)
{
char *mem;
uint64 a;
int sz;
if(newsz < oldsz)
return oldsz;
oldsz = PGROUNDUP(oldsz);
for(a = oldsz; a < newsz; a += sz){
sz = PGSIZE;
#ifdef LAB_PGTBL
//判斷當前大小是否滿足使用超級頁的開銷
if (newsz - a >= SUPERPGSIZE && a % SUPERPGSIZE == 0) {
//更新大小為超級頁方便接下來的遞增
sz = SUPERPGSIZE;
//分配超級頁大小的物理內存
mem = superalloc();
} else
#endif
mem = kalloc();
if(mem == 0){
uvmdealloc(pagetable, a, oldsz);
return 0;
}
#ifndef LAB_SYSCALL
memset(mem, 0, sz);
#endif
//給分配的頁添加映射
if(mappages(pagetable, a, sz, (uint64)mem, PTE_R|PTE_U|xperm) != 0){
#ifdef LAB_PGTBL
// 如果分配的是超級頁大小內存則釋放超級頁內存
if(sz == SUPERPGSIZE)
superfree(mem);
else
#endif
kfree(mem);
uvmdealloc(pagetable, a, oldsz);
return 0;
}
}
return newsz;
}
5、修改mappages函數。
int
mappages(pagetable_t pagetable, uint64 va, uint64 size, uint64 pa, int perm)
{
uint64 a, last;
pte_t *pte;
if((va % PGSIZE) != 0)
panic("mappages: va not aligned");
if((size % PGSIZE) != 0)
panic("mappages: size not aligned");
if(size == 0)
panic("mappages: size");
a = va;
last = va + size - PGSIZE;
for (;;) {
#ifdef LAB_PGTBL
int use_superpage = 0; // 用於標識是否使用超級頁面映射
// 判斷是否可以使用超級頁面映射
if ((a % SUPERPGSIZE) == 0 && (a + SUPERPGSIZE <= last + PGSIZE) && (perm & PTE_U)) {
use_superpage = 1; // 更改標識
}
// 如果是超級頁則設置l為1,代表接下來在superwalk當中到1層後停止
// 傳統的walk會走到level0,之後返回pte(頁表項地址)
// 而改進過的superwalk可以被人為操控停到指定的層級。
// 層級從高到底為:2 1 0
if (use_superpage) {
int l = 1;
if ((pte = superwalk(pagetable, a, 1, &l)) == 0)
return -1;
} else {
if ((pte = walk(pagetable, a, 1)) == 0)
return -1;
}
#else
// 如果不能使用超級頁面映射 就用普通頁
if ((pte = walk(pagetable, a, 1)) == 0)
return -1;
#endif
// 檢查PTE是否已經被標記為有效
if (*pte & PTE_V)
panic("mappages: remap");
// 如果有效則將物理地址轉換為PTE格式 並加上權限位和有效位
// 這裏就是添加映射的核心
*pte = PA2PTE(pa) | perm | PTE_V;
#ifdef LAB_PGTBL
//如果使用超級頁
if (use_superpage) {
// 則檢查是否已經映射到最後一個超級頁面
if (a + SUPERPGSIZE == last + PGSIZE)
break;
// 更新起始地址和物理地址
a += SUPERPGSIZE;
pa += SUPERPGSIZE;
} else {
if (a == last)
break;
a += PGSIZE;
pa += PGSIZE;
}
#else
//不使用超級頁,則每次自增一個普通頁的大小
if (a == last)
break;
a += PGSIZE;
pa += PGSIZE;
#endif
}
return 0;
}
6、修改uvmunmap函數。
注意:在釋放整個頁時涉及三種情況(頁只會在其對應的虛擬地址被完全 unmap 時被釋放):
- 第一種情況是釋放普通頁,已知每個普通頁都是4KB,並且xv6的三級頁表的最低級也都是4KB,所以直接釋放即可。
- 第二種情況是釋放超級頁(整塊釋放),超級頁的大小為2M,因為xv6的三級頁表的第二層是表示超級頁的層級(如果第二層 PTE 是 leaf 並且覆蓋 2MB,則是超級頁),此時在地址對其的情況下並且釋放該頁不會對其它的頁造成影響則直接釋放即可。
- 第三種情況是釋放超級頁(非整塊釋放,可能比一塊小也可能比一塊大),眾所周知,在操作系統當中,一個 leaf PTE 要麼映射整個 4KB 頁框,要麼映射整個 2MB 頁框,不能只映射其中一部分。所以,當我們釋放內存時,被釋放的內存大小沒有超過一個超級頁 或者 超過了一個超級頁,那麼就必然導致有一個頁的完整性被打破,從而違反操作系統對單個頁完整性的規定。所以我們要將哪些被破壞了完整性的超級頁進行降級操作,使得其降為普通頁。降級的過程就是再開闢新的普通頁,然後將原來超級頁的內容(正常存在無需釋放的內容)複製到新的普通頁,之後我們刪除/釋放原來的超級頁。
問:為什麼2MB的超級頁的完整性被破壞後就必須降級為4KB的普通頁?
答:xv6支持3級頁表,普通頁(4KB)已經是最小的硬件映射粒度,不能再細分,所以不存在“普通頁被部分破壞後再降級”的問題。
void
uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free)
{
uint64 a;
pte_t *pte;
int sz;
if((va % PGSIZE) != 0)
panic("uvmunmap: not aligned");
for(a = va; a < va + npages*PGSIZE; a += sz){
sz = PGSIZE;
#ifdef LAB_PGTBL
int l = 0; // 標誌變量 用於確定是超級頁還是普通頁。
int flag = 0; // 標記是否已經處理過超級頁
if((pte = superwalk(pagetable, a, 0, &l)) == 0)
panic("uvmunmap: walk");
#else
if((pte = walk(pagetable, a, 0)) == 0)
panic("uvmunmap: walk");
#endif
if((*pte & PTE_V) == 0) {
printf("va=%ld pte=%ld\n", a, *pte);
panic("uvmunmap: not mapped");
}
if(PTE_FLAGS(*pte) == PTE_V)
panic("uvmunmap: not a leaf");
/*下面開始解除頁面映射*/
if(do_free){
uint64 pa = PTE2PA(*pte); // 從頁表項中提取物理地址
#ifdef LAB_PGTBL
if(l == 1) {
// 如果是超級頁則獲取權限
int perm = *pte & 0xFFF;
// 然後清空頁表項
*pte = 0;
// 設置標誌
flag = 1;
// 更新大小為超級頁大小
sz = SUPERPGSIZE;
// 這裏是上述的第三種情況,如果虛擬地址未對齊到超級頁
if(a % SUPERPGSIZE != 0){
// 對齊到超級頁邊界
for(uint64 i = SUPERPGROUNDDOWN(a); i < va; i += PGSIZE) {
char *mem = kalloc(); // 分配新的物理頁面
if(mem == 0)
panic("uvmunmap: kalloc");
mappages(pagetable, i, PGSIZE, (uint64)mem, perm); // 將新分配的頁面映射到虛擬地址空間
memmove(mem, (char*)pa + i - SUPERPGROUNDDOWN(a), PGSIZE); // 將數據從超級頁複製到新分配的頁面
}
a = SUPERPGROUNDUP(a); // 更新虛擬地址
sz = 0; // 更新大小
}
superfree((void*)pa); // 釋放超級頁
} else
#endif
// 如果是普通頁
kfree((void*)pa); // 釋放普通頁
}
#ifdef LAB_PGTBL
if(flag == 0) // 避免使用超級頁時候被重複清除
#endif
*pte = 0;
}
}
7、仿照walk添加superwalk。
#ifdef LAB_PGTBL
// 參數l用於指定頁表的起始級別
pte_t *
superwalk(pagetable_t pagetable, uint64 va, int alloc, int *l)
{
if(va >= MAXVA)
panic("superwalk");
for(int level = 2; level > *l; level--) {
// 獲取當前層的頁表項地址
pte_t *pte = &pagetable[PX(level, va)];
if(*pte & PTE_V) {
// 如果頁表項有效,將其轉為物理地址
pagetable = (pagetable_t)PTE2PA(*pte);
if(PTE_LEAF(*pte)) {
// 如果是葉節點代表找到想要的了,更新頁表級別,返回頁表地址。
*l = level;
return pte;
}
} else {
//頁表項無效則嘗試分配,分配失敗返回0
if(!alloc || (pagetable = (pde_t*)kalloc()) == 0)
return 0;
// 初始化新分配的頁表
memset(pagetable, 0, PGSIZE);
// 更新頁表項為有效
*pte = PA2PTE(pagetable) | PTE_V;
}
}
// 返回目標頁表項地址
return &pagetable[PX(*l, va)];
}
#endif
8、添加uvmcopy函數。
int
uvmcopy(pagetable_t old, pagetable_t new, uint64 sz)
{
pte_t *pte;
uint64 pa, i;
uint flags;
char *mem;
int szinc;
for(i = 0; i < sz; i += szinc){
szinc = PGSIZE;
#ifdef LAB_PGTBL
int l = 0; // 標誌變量 用於確定是普通頁還是超級頁
if((pte = superwalk(old, i, 0, &l)) == 0)
// 如果是超級頁l=1,普通頁l=0
panic("uvmcopy: pte should exist");
#else
if((pte = walk(old, i, 0)) == 0)
panic("uvmcopy: pte should exist");
#endif
if((*pte & PTE_V) == 0)
panic("uvmcopy: page not present");
pa = PTE2PA(*pte);
flags = PTE_FLAGS(*pte);
#ifdef LAB_PGTBL
if(l == 1) {
// 如果是超級頁則將地址增量設置為超級頁的大小
szinc = SUPERPGSIZE;
// 分配超級頁大小的內存
if((mem = superalloc()) == 0)
goto err;
// 將超級頁大小的物理內存從舊地址複製到新分配的內存地址(父進程的數據負責給子進程)
memmove(mem, (char*)pa, SUPERPGSIZE);
// 將超級頁大小的新內存映射到新頁表的虛擬地址
if(mappages(new, i, SUPERPGSIZE, (uint64)mem, flags) != 0){
// 釋放之前分配的超級頁內存
superfree(mem);
goto err;
}
} else {
// 如果是普通頁
#endif
if((mem = kalloc()) == 0)
goto err;
memmove(mem, (char*)pa, PGSIZE);
if(mappages(new, i, PGSIZE, (uint64)mem, flags) != 0){
kfree(mem);
goto err;
}
#ifdef LAB_PGTBL
}
#endif
}
return 0;
err:
uvmunmap(new, 0, i / PGSIZE, 1);
return -1;
}
寫在後面
這一lab,尤其是最後的用户頁表lab確實非常難,一開始花費了好長時間都沒做出來,好在網絡上有很多大佬對該lab進行了講解和提供了成品代碼,使得本人在後續的研究中才得以明白該lab的底層邏輯。