3. Lab: page tables

https://pdos.csail.mit.edu/6.S081/2021/labs/pgtbl.html

1. 預備內容

在開始這個 lab 前需要先了解 Sv39 RISC-V 的虛擬地址轉換物理地址流程。

1.1 Sv39 RISC-V

xv6 爲 64 位的操作系統,在其採用的 Sv39 RISC-V 機制下,該操作系統的虛擬地址位長只有 39 位(也可選擇爲 48 位),剩餘的 25 位爲擴展位,默認情況下不使用。其物理地址由 44 位的物理頁號 PPN 和 12 位的頁內偏移組成(4kb)。

1.2 虛擬地址翻譯

首先,需要在 satp 寄存器中設置 MODE 開啓分頁,並且將頁目錄載入該寄存器 PPN 字段,該寄存器結構如下:
image.png
MODE 字段取值範圍如下,xv6 中該值爲 8,即開啓 39 bit 虛擬地址:
image.png
加載完頁表開啓分頁後,其虛擬地址翻譯流程大致如下:
image.png
可以看到分爲 3 級頁表(假如 MODE 設置爲 9,開啓 48bit 的虛擬地址,則爲 4 級頁表,39 ~ 47 位爲 4 級頁表的索引位置),首先忽略低 12 位的物理地址偏移,接下來的 27 位拆分爲 3 組,每組 9 位,每一組的值依次對應在每一級頁表中的索引位置。
假如以僞代碼的形式,模擬翻譯流程,不考慮頁目錄未創建的情況,則流程大致如下:

uint64 vaddr; // 要翻譯的虛擬地址
pagetable_t pagetable = get_pagetable(); // 獲取根頁表

// 獲取次級頁表
int L2 = get_va_L2_value(va); // 獲取 27 位中,高 9 位的值
pagetable = pagetable[L2]; // 根據 L2 索引根頁表,獲取次級頁表

// 獲取最終頁表
int L1 = get_va_L1_value(va); // 獲取 27 位中,中 9 位的值
pagetable = pagetable[L1]; // 獲取最終頁表

// 此時已經獲取到最後一個頁表,接下來獲取目標頁
int L0 = get_va_L0_value(va); // 獲取 27 位中,低 9 位的值
pte_t* pte = &pagetable[L0];
// 將獲取到的目標頁右移 10 位,去掉屬性位
// 然後左移動 12 位,這部分是實際物理地址的偏移,需要賦值爲 va 的低 12 位偏移
uint64 ppn = *pte >> 10 << 12;
// 加上虛擬地址低 12 位偏移,最終得到物理地址
uint64 paddr= ppn + (vaddr & 0x1ff);

xv6 內核的虛擬地址空間與物理地址空間映射圖
image.png

2. Speed up system calls(easy)

2.1 要求

Some operating systems (e.g., Linux) speed up certain system calls by sharing data in a read-only region between userspace and the kernel. This eliminates the need for kernel crossings when performing these system calls. To help you learn how to insert mappings into a page table, your first task is to implement this optimization for the getpid() system call in xv6.
When each process is created, map one read-only page at USYSCALL (a VA defined in memlayout.h). At the start of this page, store a struct usyscall (also defined in memlayout.h), and initialize it to store the PID of the current process. For this lab, ugetpid() has been provided on the userspace side and will automatically use the USYSCALL mapping. You will receive full credit for this part of the lab if the ugetpid test case passes when running pgtbltest.

簡單來說就是提速一些系統調用,在內核空間中將其結果保存到結構體 USYSCALL ,然後在創建用戶進程時,將 USYSCALL的地址空間映射到用戶空間,並設置爲只讀。
getpid() 系統調用爲例,將進程 pid 保存到 USYSCALL 中,然後通過 ugetpid() 調用獲取 pid,這樣可以避免用戶態與內核態的切換消耗。

2.2 分析

這次實驗主要需要修改的地方如下:

  • 保存 USYSCALL ,並初始化其結果,由於 USYSCALL 的內容與進程息息相關,故將 USYSCALL 結構定義在 struct proc
  • USYSCALL 所在的內核地址空間映射到用戶地址空間

2.3 實現

  1. USYSCALL 保存

給其分配一頁,方便後續做頁映射

// Per-process state
struct proc {
  struct usyscall* usyscall;
	// some code ...
};

struct usyscall {
  int pid;  // Process ID
};

// 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)
{
  struct proc *p;
  // some code ...
  // Allocate a usyscall page.
  if((p->usyscall = (struct usyscall *)kalloc()) == 0){
    freeproc(p);
    release(&p->lock);
    return 0;
  }
  p->usyscall->pid = p->pid;
  // some code ...
  return p;
}
  1. 映射 USYSCALL

其核心接口爲 mappageswalkmappages 爲物理頁和虛擬頁建立映射,並設置頁屬性。walk 爲根據虛擬地址,找到其頁表位置,其流程類似前文 1.2 虛擬地址翻譯
由於 USYSCALL 還是在內核空間,因此設置頁屬性時需要設置 PTE_R | PTE_U,用戶空間可訪問且只有讀權限。

#define USYSCALL (TRAPFRAME - PGSIZE)

// 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_t pagetable;
	// some code ...
  // map the usyscall below TRAPFRAME
    // map the trapframe just below TRAMPOLINE, for trampoline.S.
  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;
}

int mappages(pagetable_t pagetable, uint64 va, uint64 size, uint64 pa, int perm)
{
  uint64 a, last;
  pte_t *pte;

  if(size == 0)
    panic("mappages: size");
  
  a = PGROUNDDOWN(va);
  last = PGROUNDDOWN(va + size - 1);
  for(;;){
    if((pte = walk(pagetable, a, 1)) == 0)
      return -1;
    if(*pte & PTE_V)
      panic("mappages: remap");
    *pte = PA2PTE(pa) | perm | PTE_V;
    if(a == last)
      break;
    a += PGSIZE;
    pa += PGSIZE;
  }
  return 0;
}

pte_t * walk(pagetable_t pagetable, uint64 va, int alloc)
{
  if(va >= MAXVA)
    panic("walk");

  for(int level = 2; level > 0; level--) {
    pte_t *pte = &pagetable[PX(level, va)];
    if(*pte & PTE_V) {
      pagetable = (pagetable_t)PTE2PA(*pte);
    } else {
      if(!alloc || (pagetable = (pde_t*)kalloc()) == 0)
        return 0;
      memset(pagetable, 0, PGSIZE);
      *pte = PA2PTE(pagetable) | PTE_V;
    }
  }
  return &pagetable[PX(0, va)];
}

3. Print a page table(easy)

3.1 要求

Define a function called vmprint(). It should take a pagetable_t argument, and print that pagetable in the format described below. Insert if(p->pid==1) vmprint(p->pagetable) in exec.c just before the return argc, to print the first process's page table. You receive full credit for this part of the lab if you pass the pte printout test of make grade.

簡單來說就是給一個頁表,打印其頁表結構。格式如下:

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

3.2 實現

這裏流程與地址翻譯有些類似,找到 3 級頁表,依次遞歸打印。

char* prefix[] = {".. .. ..", ".. ..", ".."};
void _vmprint(pagetable_t pagetable, int level)
{
  int idx = 0;
  int pgtbl_size = 512;
  while(idx < pgtbl_size)
  {
    pte_t *pte = &pagetable[idx++];
    // 檢查該頁是否 valid
    if (!(*pte & PTE_V))
    {
      continue;
    }

    printf("%s%d: pte %p pa %p\n", prefix[level], idx - 1, *pte, PTE2PA(*pte));

    if (level)
    {
      _vmprint((pagetable_t)PTE2PA(*pte), level - 1);
    }
  }
}

void vmprint(pagetable_t pagetable)
{
  printf("page table %p\n", pagetable);
  _vmprint(pagetable, 2);
}

4. Detecting which pages have been accessed (hard)

4.1 要求

Your job is to implement pgaccess(), a system call that reports which pages have been accessed. The system call takes three arguments. First, it takes the starting virtual address of the first user page to check. Second, it takes the number of pages to check. Finally, it takes a user address to a buffer to store the results into a bitmask (a datastructure that uses one bit per page and where the first page corresponds to the least significant bit). You will receive full credit for this part of the lab if the pgaccess test case passes when running pgtbltest.

實現接口int pgaccess(void* base, int len, void* mask) ,其功能爲,檢查以 base 爲起始地址,連續 len 頁,是否有被訪問過,即 PTE_A 的頁屬性是否爲 1,並將結果通過 mask 返回,mask 值爲 32 位,第 0 位表示第 1 頁近期有被訪問過。
頁屬性可參考下圖:
1644630237(1).png
這裏需要注意的是,每次執行 pgaccess 的時候,需要清空被檢查頁的 PTE_A屬性,因爲該屬性一旦被設置,則永遠爲 1。

4.2 分析

難點主要有 2 個

  • 獲取檢查的虛擬地址頁的 pte,這裏可以仿照前文所說的 walk 接口
  • 返回 mask 結果,這裏參考 copyout 接口,該接口主要將內核空間的數據傳回用戶空間

4.3 實現

pte_t * _walk(pagetable_t pagetable, uint64 va)
{
  for(int level = 2; level > 0; level--) {
    pte_t *pte = &pagetable[PX(level, va)];
    if(*pte & PTE_V) {
      pagetable = (pagetable_t)PTE2PA(*pte);
    } else {
      return 0;
    }
  }
  return &pagetable[PX(0, va)];
}

int
sys_pgaccess(void)
{
  // lab pgtbl: your code here.
  int page_cnt;
  uint64 start_addr;
  uint64 out_result;

  if (argaddr(0, &start_addr) < 0)
    return -1;
  if (argint(1, &page_cnt) < 0)
    return -1;
  if (argaddr(2, &out_result) < 0)
    return -1;

  //printf("start addr %p \n ", start_addr);
  uint32 scan_result = 0;
  pagetable_t pagetable = myproc()->pagetable;
  for (int i = 0; i < page_cnt; i++)
  {
    pte_t* pte = _walk(pagetable, start_addr);
    if (pte == 0)
      continue;
    
    //printf("found pte %p no.%d\n", *pte, i);
    if ((*pte & PTE_A))
    {
      scan_result |= (1 << i);
      *pte &= (~PTE_A); // need to clear bit , becasue if set PTE_A , will exists forever
      //printf("scan %d is valid \n", i);
    }
      
    start_addr += PGSIZE;
  }

  if(copyout(pagetable, out_result, (char *)&scan_result, sizeof(uint32)) < 0)
      return -1;

  return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章