MIT 6.S081 2021: Lab page tables


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)&complement;
    }
    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寫回即可。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM