Lab page tables
內核地址空間,進程地址空間。
地址映射
守護頁,PTE的flags
物理內存分配
sbrk和exec
Speed up system calls
通過在用戶空間和內核之間的只讀區域共享數據加速特定的系統調用,執行這些系統調用可以不再進入內核。本實驗可以學習向頁表中插入映射。
實驗方法:當進程創建時,將地址 USYSCALL
映射為只讀頁。在該頁的起始處,存儲一個 struct usyscall
,設為當前進程的 pid 。ugetpid()
已經實現了用戶空間的代碼。
在 struct proc
中加入指向共享區域的指針。(kernel/proc.h)
struct usyscall *usyscall; // shared data page to speed up system calls.
(kernel/proc.c)
在allocproc
中分配物理頁,並設置該頁內容。
// Allocate a USYSCALL page which is shared with kernel.
if((p->usyscall = (struct usyscall *)kalloc()) == 0){
freeproc(p);
release(&p->lock);
return 0;
}
p->usyscall->pid = p->pid;
在freeproc
中釋放物理頁。
if(p->usyscall)
kfree((void*)p->usyscall);
p->usyscall = 0;
在proc_pagetable
中完成虛擬地址到物理地址的映射,並設置權限。
// map the USYSCALL just below TRAPFRAME, for speeding system calls.
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;
}
在proc_freepagetable
中完成映射的解除。
uvmunmap(pagetable, USYSCALL, 1, 0);
這里參數0代表不free物理頁,因為在 freeproc
中已經釋放。
此處如果不解除映射,會造成系統執行 exec 系統調用成功后,free 舊的鏡像,運行到 uvmfree
,free 掉所有的三級頁表 PTE 的映射(大小按照 p.sz
,不包括 USYSCAL這個頁),直到freewalk
,會發現 USYSCALL 這頁的 PTE 沒有free(不等於0),且 PTE_R 存在(是三級頁表的PTE),且 PTE_V 存在(該頁映射存在),然后會panic("freewalk: leaf")
。
這里 debug 的方法總結:在 panic 處用 gdb 的 backtrace
查看調用棧,然后找到 uvmfree
查看 sz 發現只有 \(4096B\) ,也就是 uvmfree
不會解除 USYSCALL 處的映射。需要去 proc_freepagetable
中解除。
Print a page table
根據進程的 p->pagetable
按照特定格式打印出該進程的頁表。
模仿 walk
的實現:從一級頁表開始遍歷,找出 valid PTE,按照是一級頁表的 PTE ,二級頁表的 PTE,還是三級頁表的 PTE,設置一定的輸出格式。
PTE2PA
用於實現將 PTE 轉換為物理地址(Chapter3:取 \(44\) 位的 PPN 和全 \(0\) 的低12位,因為 \(4096B\) 對齊格式,所以低 \(12\) 位全 \(0\))。
結果顯示一級頁表項不是 \(512\) 個,而是 \(255\) 個,為什么?
Preparation 中說明虛擬地址本應為 \(39\) 位(這個位數是硬件提供的位數),但 xv6 只使用了 \(38\) 位,一級頁表的頁表項只有 \(256\) 個。為了簡單,將內核的 text 和 data 被放在一頁。
// Recursively print a page table of the process.
void
walkprint(pagetable_t pagetable, int level)
{
for (int i = 0; i < 512; i++) {
pte_t pte = pagetable[i];
if (pte & PTE_V) {
if (level == 1) {
printf("..");
} else if (level == 2) {
printf(".. ..");
} else {
printf(".. .. ..");
}
printf("%d: pte %p pa %p\n", i, pte, PTE2PA(pte));
if ((pte & (PTE_X|PTE_R|PTE_W)) == 0) {
uint64 child = PTE2PA(pte);
walkprint((pagetable_t)child, level + 1);
}
}
}
}
void
vmprint(pagetable_t pagetable)
{
printf("page table %p\n", pagetable);
walkprint(pagetable, 1);
}
Detecting which pages have been accessed
一些 GC 的實現或者自動內存管理需要哪些頁被讀寫過的信息。本實驗需要通過檢測 RISC-V 頁表的 access bits,並向用戶空間傳遞這樣的信息。
pgaccess
系統調用:向用戶返回哪些頁被訪問過。接受三個參數:一個虛擬地址base,一個頁長度len,一個返回結果的地址maskaddr。
實現:虛擬地址所在頁作為第一個頁,從這個頁開始之后len個頁,找出每個頁對應的 PTE(通過walk
),判斷每個 PTE 的 PTE_A 位,記錄結果在 unsigned int mask
中,然后將該位clear,第一頁的結果作為mask的最低有效位。最后將結果傳輸到用戶空間(copyout
)。
系統調用需要校驗參數,len不能大於 \(32\) ,因為 unsigned int 可以存儲的結果位為 \(32\) 位。
最終需要clear PTE_A位,否則下次無法判斷之前是否access過。
定義訪問位 PTE_A
(kernel/riscv.h)
#define PTE_A (1L << 6)
// The A bit indicates the virtual page has been read, written, or fetched from since the last time the A bit was cleared.
完成系統調用的實現,從用戶空間獲取參數並check。
(kernel/sysproc.c)
int
sys_pgaccess(void)
{
uint64 base;
int len;
uint64 mask;
if(argaddr(0, &base) < 0 || argint(1, &len) < 0
|| argaddr(2, &mask) < 0)
return -1;
// set an upper limit to unsigned int mask
if (len > 32) {
return -1;
}
return pgaccess(base, len, mask);
}
通過walk
找出需要判斷是否訪問過的頁的PTE,並clear掉PTE_A,將結果傳至用戶空間。
// Mask the page accessed from base address
// to the bits which matched the order of pages.
int
pgaccess(uint64 base, int len, uint64 maskaddr)
{
struct proc *p = myproc();
uint mask;
for (int i = 0; i < len; i++) {
pte_t *pte;
pte = walk(p->pagetable, base + PGSIZE * (uint64)i, 0);
if (pte != 0 && ((*pte) & PTE_A)) {
mask |= (1 << i);
*pte &= ~PTE_A;
}
}
if (copyout(p->pagetable, maskaddr, (char *)&mask, sizeof(mask)) < 0)
return -1;
return 0;
}