MIT6.S081-Lab3 Pgtbl [2021Fall]


開始日期: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的輸出

    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順序來寫的,本篇博客是總結整體思路,盡量按照流程來
  • 頭兩個實驗末尾時都會提出個問題,筆者也統一放在實驗的末尾

(參考)鏈接

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 
    };
    
  • 簡述題意:打印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)
    • 遇到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 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)不被用戶訪問

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_A after checking if it is set. Otherwise, it won't be possible to determine if the page was accessed since the last time pgaccess() was called (i.e., the bit will be set forever).

    • 圖示如下

  • 接下來就是參考代碼的cv時間了,使用了walk,該函數是遍歷三層頁表樹,通過va在當前頁表中找出對應的pte地址,同時它還有一個alloc參數,如果這個參數不為0它就會為找不到的對應pte地址的va額外申請一個頁面來對應,反之alloc0的話則不會。我們當然是設置為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中獲取參數需要用到agraddragrint,記得按參數順序獲取

    • 如果總數越界,需要報錯 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來,具體參考p77

      Each 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,我是把它當作計算機科學史來看的,無數的前輩是如此地熱愛這門藝術)
  • 最近在聽電影《飛馳人生》的片尾曲:《奉獻》


免責聲明!

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



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