MIT6.S081-Lab3 Pgtbl
開始日期:22.3.27
作業系統:Ubuntu20.0.4
Link:Lab Pgtbl
目錄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
頁面的分配記憶體方式即可- 這裡沒有分配記憶體空間給
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; }
- 參考
-
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
放入堆的最底層,因為先進後出,第一層頁面最後列印,第二層頁面是中間列印,第三層頁面最先列印,和我們想要的順序相反了!
- 如果先進入遞迴了,可以想象為建立了一個堆,最上層即第一次函式呼叫
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
vmprint
in 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()
之後,再也無法判斷該頁面是否已經被訪問。Be sure to clear
PTE_A
after 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來,具體參考p73 ~ p74Each 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
-
-
-
程式碼
#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,我是把它當作電腦科學史來看的,無數的前輩是如此地熱愛這門藝術)
- 最近在聽電影《飛馳人生》的片尾曲:《奉獻》