Speed up system calls
這個實驗的目的是要“加速系統調用”,怎么加速呢?在內核和用戶程序之間創建一個共享的只讀頁,這樣內核往這個頁里寫入數據的時候,用戶程序就可以不經復雜的系統調用直接讀取它了。實驗要求,把一個只讀頁從USYSCALL(memlayout.h中定義的一個虛擬地址)映射的內核的某一個地方,並在頁的起始處存儲一個結構體struct usyscall。提示說"ugetpid() has been provided on the userspace side and will automatically use the USYSCALL mapping",看一下ugetpid()這個函數。
圖里面#ifdef 下面是灰的,不用管它,Makefile里面已經設置了相應的CFLAGS,編譯的時候會自動加上這個LAB_PGTBL宏。
很顯然,ugetpid()直接從USYSCALL這個地址讀數據,因此我們需要把usyscall結構寫到此頁表的開頭。
看一下USYSCALL是什么東西。trampoline這個詞的本意是蹦床,在這里,它是用來進行trap to the kernel操作的。下面的trapframe是保存了進程的一些參數。這個USYSCALL是緊鄰trapframe下端的一頁。(xv6手冊第三章里面有用戶進程和內核的內存分布圖):
然后提示說在proc_pagetable里面設置映射。這個proc_pagetable里面有兩個顯然是在進行map操作的函數,看來對USYSMAP的映射就在這里進行:
問題是到底把USYSCALL映射到哪兒呢?看xv6手冊里的這段話"When creating each process, xv6 allocates a page for the process’s trapframe, and arranges for it always to be mapped at user virtual address TRAPFRAME, which is just below TRAMPOLINE."xv6是先給trapframe分配一塊內存再把TRAPFRAME映射到它上面。看一下allocproc(),這個程序首先循環搜索進程表,搜索到UNUSED進程就為其分配內存,然后給進程表p賦各種值。重點看這一段:
很顯然p->trapframe就是在這里初始化的,在allocproc()初始化之后在proc_pagetable()之中映射。我們可以仿照trapframe的操作,在struct proc中添加一個參數struct usyscall *usyspage,然后用kalloc()分配一頁內存,地址指向usyspage,並把該進程的pid存到頁表中。稍后我們就把用戶內存中的USYSCALL映射到這里。
//給這個usyscall分配一個頁面
if((p->usyspage = (struct usyscall *)kalloc()) == 0){
freeproc(p);
release(&p->lock);
return 0;
}
p->usyspage->pid=p->pid;
usyspage有了值,就可以做映射操作了。這里要求只讀頁,因此把權限設成PTE_R,另外還要加上PTE_U,xv6手冊里表明,不加PTE_U的頁默認在supervisor mode里運行:
//map usyscall
if(mappages(pagetable, USYSCALL, PGSIZE,
(uint64)(p->usyspage), PTE_R | PTE_U) < 0){
uvmunmap(pagetable, TRAMPOLINE, 1, 0);
uvmunmap(pagetable, TRAPFRAME, 1, 0);
uvmfree(pagetable, 0);
return 0;
}
注意,如果mappage失敗的話,要撤銷前面TRAMPOLINE和TRAPFRAME的映射。
然后做好釋放,仿照freeproc里對trapframe里的操作來釋放usyspage:
//記得釋放usyscall
if(p->usyspage)
kfree((void*)p->usyspage);
p->usyspage = 0;
還要務必記得修改下面這個proc_freepagetable函數,加上對USYSCALL的操作:
// 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);
}
Print a page table
提示中說到: The function freewalk may be inspirational.仿照vm.c里面的freewalk()直接寫代碼:
void vmprint(pagetable_t pagetable,int count)//count應該為0
{
// there are 2^9 = 512 PTEs in a page table.
if(count==0)
{
printf("page table %p\n",pagetable);
}
int arg_tmp=count+1;
if(count!=3)
{
for(int i = 0; i < 512; i++){
pte_t pte = pagetable[i];
if(pte & PTE_V)
{
// this PTE points to a lower-level page table.
uint64 child = PTE2PA(pte);
if(count==0)
{
printf("..");
}
else if(count==1)
{
printf(".. ..");
}
else if(count==2)
{
printf(".. .. ..");
}
printf("%d: pte %p pa %p\n",i,pte,child);
vmprint((pagetable_t)child,arg_tmp);
}
else
{
continue;
}
}
}
return;
}
1.xv6使用的是三級頁表。xv6訪問頁的時候,首先從指向第一級頁表的寄存器satp開始(類似的東西在x86架構里叫CR3寄存器),使用虛擬地址的30~38位在頁表里定位一項;如果該項的PTE_V這一位是1,則此頁表項是有效的,可以根據此頁表項內的地址訪問下一級頁表。依次類推,直到搜索到第三級,獲取物理地址。由此可見,三級頁表其實是一棵深度為3的樹,我們可以使用BFS搜索來遍歷這棵樹。
2.使用BFS,遍歷每個節點上的所有葉子(也就是頁表項)。如果葉子的PTE_V為0,直接跳過;如果為1,先用宏PTE2PA把表項轉換成物理地址,再遞歸調用這個地址。
3.使用一個計數變量count來記錄遞歸深度,初始必須置為0,由於樹的深度最大只有3,則count==等於3時直接返回。不過看這門課的錄像,是實現了一個只傳入頁表物理地址的vmprint。我猜應該是聲明了一個全局變量,進入函數時變量+1,退出時將這個變量-1。
Detecting which pages have been accessed
不要被標題后面那個紅色的hard嚇住,這道題其實不難!
實驗要求:從一個用戶頁表地址開始,搜索所有被訪問過的頁並返回一個bitmap來顯示這些頁是否被訪問過。比如說,如果第二個頁被訪問過了,bitmap里從右往左數第二個bit就是1。如果你用過sys/select.h里面的select()函數的話,就會知道里面的幾個fd_set類型的參數就是bitmap。
那么這個“accessed(read or write)”是怎么回事呢?傳給sys_pgaccess()的第一個參數是用戶指針,即圖中的buf。所謂的access就是直接對頁進行寫入,往*(buf+30*PGSIZE)等幾個位置寫入數據:
那么PTE_A是從哪里來的呢?看一下這句話:”The RISC-V hardware page walker marks these bits in the PTE whenever it resolves a TLB miss.“很顯然,這個PTE_A位是由硬件設置的,所以我們只需要檢測它就可以了。代碼如下:
int
sys_pgaccess(void)
{
// lab pgtbl: your code here.
//先提取一下參數
struct proc* p =myproc();
uint64 usrpge_ptr;//待檢測頁表起始指針
int npage;//待檢測頁表個數
uint64 useraddr;//稍后寫入用戶內存
argaddr(0,&usrpge_ptr);
argint(1,&npage);
argaddr(2,&useraddr);
if(npage>64)
{
return -1;
}
uint64 bitmap=0;
uint64 mask=1;
uint64 complement=PTE_A;
complement=~complement;
int count=0;
for(uint64 page =usrpge_ptr;page<usrpge_ptr+npage*PGSIZE;page+=PGSIZE)
{
pte_t* pte = walk(p->pagetable,page,0);
if(*pte&PTE_A)
{
bitmap=bitmap|(mask<<count);
*pte=(*pte)∁
}
count++;
//printf("bitmap:%p\n",bitmap);
}
copyout(p->pagetable,useraddr,(char*)&bitmap,sizeof(bitmap));
return 0;
}
1.首先用argint和argaddr傳入三個參數。初始化bitmap,這里我用一個uint64變量來作為bitmap。
2.設置一個mask,用來修改bitmap里面的位;設置一個complement,PTE_A置0,其他位置1,用來清空原來PTE里的PTE_A位。設置一個計數器count,記錄正在檢查第幾個頁表。
3.遍歷傳入的頁,使用walk函數找到對應的PTE,如果PTE_A存在,則將mask左移count位和bitmap做與運算(邏輯運算基本知識,a|0=a,a|1=1,因此bitmap其他位不變,唯獨第count位一定被置為1),存回bitmap中。再清除PTE表中的PTE_A位,*pte和complement進行或運算(a&0=0,a&1=a,因此因此*pte其他位不變,唯獨PTE_A位一定被置為0)。最后使用copyout寫回即可。
