開始日期:22.3.27
操作系統:Ubuntu20.0.4
Link:Lab Pgtbl
個人博客:Memory Dot
my github repository: duilec/MITS6.081-fall2021/tree/pgtbl
Lab Pgtbl
寫在前面
usertests bigdir無法通過
- 是2021fall lab pgtbl實驗本身的設計缺陷,建議跳過,或者去做2020fall lab pgtbl
ansewer-pgtbl.txt
- 這里填寫對問題一、二的回答,才能通過測試
調試的端口號被占用
-
我這里的qemu調試時的端口號是
26000*** Now run 'gdb' in another window. qemu-system-riscv64 -machine virt -bios none -kernel kernel/kernel -m 128M -smp 3 -nographic -drive file=fs.img,if=none,format=raw,id=x0 -device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0 -S -gdb tcp::26000 -
需要殺死占用端口號的進程,才能調試
sudo lsof -i tcp:26000 #查詢使用26000的進程,打印對應的pid sudo kill <pid> #將對應pid的進程殺死即可
格式符%p在windos10和linux的區別
-
如下程序段,在windos10和linux的輸出是不同的
- win10中不會輸出前綴
0x,但Linux會輸出前綴0x - 這個區別會在Print a page table (easy)中用到
/* test.c */ #include <stdio.h> int main(){ printf("%p", 1111); } - win10中不會輸出前綴
-
win10的輸出
PS D:\VSCode\vscode_a\os> cd "d:\VSCode\vscode_a\os\xv6-labs-2021\" ; if ($?) { gcc test.c -o test } ; if ($?) { .\test } 0000000000000457 -
linux的輸出
duile@ubuntu:~/Desktop/cpp$ ./test 0x457
補充
- 做題時當然是按照hints順序來寫的,本篇博客是總結整體思路,盡量按照流程來
- 頭兩個實驗末尾時都會提出個問題,筆者也統一放在實驗的末尾
(參考)鏈接
- 6.S081-2021FALL-Lab3:pgtbl
- MIT6.S081的學習筆記
- 推薦一位pku前輩的學習路線:CS自學指南,筆者看到了一位熱愛計算機的人
Speed up system calls (easy)
-
簡述題意:給系統調用函數
ugetpid()提速,方法是給每個進程的單獨內存空間里添加一個USYSCALL頁面,而里頭存放一個系統函數會經常使用的數據,這里專指pid,而我們把pid存放在struct usyscall中。- 通過上述方法,
ugetpid()需要用到pid時,會在用戶態使用USYSCALL頁面直接調用,而不用切換到內核態
/* kernel/memlayout.h */ ... struct usyscall { int pid; // Process ID }; - 通過上述方法,
-
如何添加
USYSCALL頁面呢?首先找到位置,然后完成兩步,第一步是完成頁面映射;第二步是分配內存空間 -
參考一個單獨進程的內存空間book-riscv-rev2.pdf所存儲的內容(Figure 3.4)以及
proc_pagetable.c(kernel/proc.c),可以猜測,USYSCALL頁面的位置在heap之前,trapfram之后
-
完成頁面映射
- 參考
TRAMPOLINE(蹦床)、TRAPFRAME(陷阱幀)頁面的映射方式即可 USYSCALL允許用戶read操作,所以mappages的最后參數使用PTE_R | PTE_U- 如果映射失敗,需要先將
TRAMPOLINE以及TRAPFRAME頁面解除映射,再將整個進程的內存空間釋放掉(此時的pagetable就是這個程序的程序頁面)
// Create a user page table for a given process, // with no user memory, but with trampoline pages. pagetable_t proc_pagetable(struct proc *p) { pagetable = uvmcreate(); if(pagetable == 0) return 0; ... // map the USYSCALL just below TRAMPOLINE and TRAPFRAME if(mappages(pagetable, USYSCALL, PGSIZE, (uint64)(p->usyscall), PTE_R | PTE_U) < 0){ uvmunmap(pagetable, TRAMPOLINE, 1, 0); uvmunmap(pagetable, TRAPFRAME, 1, 0); uvmfree(pagetable, 0); return 0; } return pagetable; } - 參考
-
分配內存空間
-
參考
TRAPFRAME頁面的分配內存方式即可,代碼如下-
注意最后要將
pid返回到用戶態,這樣才能在用戶態直接使用pid -
記得結構體
struct proc中需要多添加一條struct usyscall *usyscall// Per-process state struct proc { struct spinlock lock; ... struct usyscall *usyscall; // record info of syscall(pid) }; -
這里沒有分配內存空間給
TRAMPOLINE,因為事實上TRAMPOLINE已經映射到內核的內存空間了,沒錯,就是一開始xv6系統啟動時的那個系統內核
-
-
參考代碼
// Look in the process table for an UNUSED proc. // If found, initialize state required to run in the kernel, // and return with p->lock held. // If there are no free procs, or a memory allocation fails, return 0. static struct proc* allocproc(void) { ... // Allocate a USYSCALL page. if((p->usyscall = (struct usyscall *)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. ... p->context.sp = p->kstack + PGSIZE; p->usyscall->pid = p->pid; return p; } -
注意如果分配失敗,會調用
freeproc(),因此要比之前多釋放掉一個p->usyscall參數,同時還會再調用proc_freepagetable,這里也需要多解除一個對USYSCALL頁面的映射。static void freeproc(struct proc *p) { if(p->trapframe) kfree((void*)p->trapframe); p->trapframe = 0; p->usyscall = 0; ... // Free a process's page table, and free the // physical memory it refers to. void proc_freepagetable(pagetable_t pagetable, uint64 sz) { uvmunmap(pagetable, TRAMPOLINE, 1, 0); uvmunmap(pagetable, TRAPFRAME, 1, 0); uvmunmap(pagetable, USYSCALL, 1, 0); uvmfree(pagetable, sz); }
-
-
Which other xv6 system call(s) could be made faster using this shared page? Explain how.
- 可以加速fork(),通過在
struct usyscall中添加一個parent數據,以供child們需要的時候在用戶態直接使用USYSCALL頁面調用,而不用切換到內核態
/* kernel/memlayout.h */ ... struct usyscall { int pid; // Process ID struct proc *parent // Parent process }; - 可以加速fork(),通過在
Print a page table (easy)
- 簡述題意:打印xv6系統的第一個頁面的所有內容,方法是編寫一個
vmprint()函數,當第一個程序啟動時執行該函數即可- 這個第一個頁面就是根頁面
- 當然,這個第一個程序就是啟動xv6系統
/* kernel/exec.c */
int
exec(char *path, char **argv)
{
...
if(p->pid==1) {
vmprint(p->pagetable);
}
return argc; // this ends up in a0, the first argument to main(argc, argv)
...
- 下面就是編寫
vmprint()了,主要參考了freewalk,我們先看看freewalk是怎么編寫的。
// Recursively free page-table pages.
// All leaf mappings must already have been removed.
void
freewalk(pagetable_t pagetable)
{
// there are 2^9 = 512 PTEs in a page table.
for(int i = 0; i < 512; i++){
pte_t pte = pagetable[i];
if((pte & PTE_V) && (pte & (PTE_R|PTE_W|PTE_X)) == 0){
// this PTE points to a lower-level page table.
uint64 child = PTE2PA(pte);
freewalk((pagetable_t)child);
pagetable[i] = 0;
} else if(pte & PTE_V){
panic("freewalk: leaf");
}
}
kfree((void*)pagetable);
}
-
可以看出,該函數主要功能是遍歷所給頁面的所有PTE(條目),包括它子頁面的所以PTE,同時將所以PTE置
0-
因為要進入到子頁面所以使用了遞歸(Recurse)
- 何時迭代呢?就是當該條目有效,但卻無法讀、寫、執行的時候,說明這是一條指向子頁面的PTE,即
if((pte & PTE_V) && (pte & (PTE_R|PTE_W|PTE_X)) == 0)
- 何時迭代呢?就是當該條目有效,但卻無法讀、寫、執行的時候,說明這是一條指向子頁面的PTE,即
-
遇到leaf PTE的時候會報錯
// All leaf mappings must already have been removed. -
顯然,在執行
freewalk之前,所有的葉PTE必須被移除
-
-
那么
vmprint的功能就能想出來了,參照着格式來page table 0x0000000087f6e000 ..0: pte 0x0000000021fda801 pa 0x0000000087f6a000 .. ..0: pte 0x0000000021fda401 pa 0x0000000087f69000 .. .. ..0: pte 0x0000000021fdac1f pa 0x0000000087f6b000 .. .. ..1: pte 0x0000000021fda00f pa 0x0000000087f68000 .. .. ..2: pte 0x0000000021fd9c1f pa 0x0000000087f67000 ..255: pte 0x0000000021fdb401 pa 0x0000000087f6d000 .. ..511: pte 0x0000000021fdb001 pa 0x0000000087f6c000 .. .. ..509: pte 0x0000000021fdd813 pa 0x0000000087f76000 .. .. ..510: pte 0x0000000021fddc07 pa 0x0000000087f77000 .. .. ..511: pte 0x0000000020001c0b pa 0x0000000080007000 -
我們多寫一個輔助函數
recurse_treepage來實現遞歸- 頁表是分為三層的,所以用
level來標記到哪一次了 - 如果有效就立刻打印,同時在有效的情況下,無法讀、寫、執行該PTE就進入遞歸
- 注意,是有效時先立刻打印,不能先進入遞歸,否則會導致整個頁面信息的輸出順序反了
- 如果先進入遞歸了,可以想象為創建了一個堆,最上層即第一次函數調用
recurse_treepage放入堆的最底層,因為先進后出,第一層頁面最后打印,第二層頁面是中間打印,第三層頁面最先打印,和我們想要的順序相反了!
- 如果先進入遞歸了,可以想象為創建了一個堆,最上層即第一次函數調用
- 記得在defs.h中添加聲明
void vmprint(pagetable_t pagetable) { printf("page table %p\n", pagetable); recurse_treepage(pagetable, 0); } void recurse_treepage(pagetable_t pagetable, int level) { // there are 2^9 = 512 PTEs in a page table. for(int i = 0; i < 512; i++){ pte_t pte = pagetable[i]; // if PTE_V is vaild, print infomation // level == 0 => top; level == 1 => middle; level == 2 => bottom; if(pte & PTE_V) { for(int j = 0; j <= level ; j++){ if (j == 0) printf(".."); else printf(" .."); } uint64 child = PTE2PA(pte); printf("%d: pte %p pa %p\n", i, pte, child); // this PTE points to a lower-level page table. if((pte & (PTE_R|PTE_W|PTE_X)) == 0) recurse_treepage((pagetable_t)child, level + 1); } } } - 頁表是分為三層的,所以用
-
Explain the output of
vmprintin terms of Fig 3-4 from the text. What does page 0 contain? What is in page 2? When running in user mode, could the process read/write the memory mapped by page 1? What does the third to last page contain?- FIG 3-4 就是book-riscv-rev2.pdf所存儲的內容(Figure 3.4)

- 根據圖片就可以回答問題了
page0: date and text of process page1: guard page for protect stack by present page0 overflow page2: stack of process page3 to last page: heap, trapfram, trampoline- 程序在用戶態運行時是不能讀/寫page1(即
guard page)的,它本身就是用來保護page2即(stack page)不被用戶訪問
- FIG 3-4 就是book-riscv-rev2.pdf所存儲的內容(Figure 3.4)
Detecting which pages have been accessed (hard)
-
簡述題意:編寫
sys_pgaccess(),檢測頁面是否被訪問,這里需要注意兩點,一是該函數的輸入參數,二是該函數的輸出結果。 -
輸入參數有三個:第一個被檢測頁面的虛擬地址,被檢測頁面總數,輸出結果的用戶態地址
-
輸出結果是通過
copyout()從內核態傳出到用戶態的,自然需要一個用戶態地址,同時需要注意的是,該結果是一個32bits的數據,我們也恰好要檢測32個頁面(參考user/pgtbltest.c/pgaccess_test()),第1個頁面如果被訪問了,就將0bit位設置為1;第2個頁面如果被訪問了,就將1bit位設置為1,以此類推,第32個頁面對應31bit位。反之,沒被訪問就設置為0
eg. 第4,13,28bit位被訪問了,其它沒有被訪問,圖示如下

-
接下來我們進一步明晰整個檢測過程
-
首先,用戶先訪問了一些頁面,xv6系統會將被訪問頁面的
PTV_A標志位設置為1 -
其次,用戶調用
pgaccess(),檢測頁面是否被訪問 -
然后,
pgaccess()返回結果,同時,將已被訪問頁面的PTV_A標志位設置為0,這是為了防止調用過一次pgaccess()之后,再也無法判斷該頁面是否已經被訪問。(更切確地說,每一次調用pgaccess之前,用戶都會訪問一些頁面,如果我們在上一次調用pgaccess時保持為1,就無法判斷本次這些保持為1頁面是否被訪問)Be sure to clear
PTE_Aafter checking if it is set. Otherwise, it won't be possible to determine if the page was accessed since the last timepgaccess()was called (i.e., the bit will be set forever). -
圖示如下

-
-
接下來就是參考代碼的cv時間了,使用了
walk,該函數是遍歷三層頁表樹,通過va在當前頁表中找出對應的pte地址,同時它還有一個alloc參數,如果這個參數不為0,它就會為找不到的對應pte地址的va額外申請一個頁面來對應,反之alloc為0的話則不會。我們當然是設置為0,我們只是查詢,不能去申請多的頁面。-
這里會有一個當前頁面從哪里來的問題,后來看到
pgaccess_test()就懂了,這是一整個測試程序,它會調用proccess(),在這個測試程序中就創建了32個頁面,xv6系統會把部分頁面設置為已被訪問,而32個頁面自然就存儲在測試程序的程序頁表中 -
貼一下
pgaccess_test()/* user/pgtbltest.c */ void pgaccess_test() { char *buf; unsigned int abits; printf("pgaccess_test starting\n"); testname = "pgaccess_test"; buf = malloc(32 * PGSIZE); if (pgaccess(buf, 32, &abits) < 0) err("pgaccess failed"); buf[PGSIZE * 1] += 1; buf[PGSIZE * 2] += 1; buf[PGSIZE * 30] += 1; if (pgaccess(buf, 32, &abits) < 0) err("pgaccess failed"); if (abits != ((1 << 1) | (1 << 2) | (1 << 30))) err("incorrect access bits set"); free(buf); printf("pgaccess_test: OK\n"); }
-
-
然后主要參考了
walkaddr來編寫-
從trapfram中獲取參數需要用到
agraddr,agrint,記得按參數順序獲取 -
如果總數越界,需要報錯
if(len < 0 || len > 32) return -1;It's okay to set an upper limit on the number of pages that can be scanned.
-
注意我們不需要檢測其它標志位,只檢測
PTE_A -
PTE_A的具體標志位位置需要根據riscv-privileged來,具體參考p77Each leaf PTE contains an accessed (A) and dirty (D) bit. The A bit indicates the virtual page has been read, written, or fetched from since the last time the A bit was cleared.

-
添加
PTE_A/* kernel/riscv.h */ ... #define PTE_U (1L << 4) // 1 -> user can access #define PTE_A (1L << 6) // 1 -> page already be accessed-
核心部分需要用到一個
for循環,跳到下個頁面用va += PGSIZE即可 -
如果已被訪問,先添加到結果的對應bit位中,再置
0
-
-
-
記得在
defs.h中添加walk()的聲明 -
代碼
#ifdef LAB_PGTBL
// Return bitmask to user by detecting which page have been accessed.
uint64
sys_pgaccess(void)
{
uint64 va;
int len;
uint64 abits_addr;
if(argaddr(0, &va) < 0)
return -1;
if(argint(1, &len) < 0)
return -1;
if(argaddr(2, &abits_addr) < 0)
return -1;
if(len < 0 || len > 32)
return -1;
uint32 ret = 0;
pte_t *pte;
struct proc *p = myproc();
for(int i = 0; i < len; i++){
if(va >= MAXVA)
return -1;
pte = walk(p->pagetable, va, 0);
if(pte == 0)
return -1;
/* if pte has been accessed add bit of ret and clear*/
if(*pte & PTE_A){
ret |= (1 << i);
*pte &= (~PTE_A);
}
/* va of next page */
va += PGSIZE;
}
if(copyout(p->pagetable, abits_addr, (char*)&ret, sizeof(ret)) < 0)
return -1;
return 0;
}
#endif
總結
- 完成日期22.4.1
- 筆者有個小bug其實很尷尬,就是一開始沒把代碼編到
#ifdef LAB_PGTBL...#endif之間,找了我1個小時多。。。 - 參考測試程序來理清思路是很有用的,尤其是最后一個實驗中的三個輸入參數到底是啥
- 代碼是可以很優雅,很藝術的,把計算機科學當作一門藝術來學,而不是技術。這門學科可以是目的,而不是所謂手段。我感覺我要愛上這門藝術了。(最近在看Crash Course Computer Science,我是把它當作計算機科學史來看的,無數的前輩是如此地熱愛這門藝術)
- 最近在聽電影《飛馳人生》的片尾曲:《奉獻》
