XV6 Lab2:Page Tables

Lab2:Page Tables

本 lab 的任务是理解 xv6 页表的实现。

参考文章:
xv6实验课程–页表(2021)
6.S081-2021FALL-Lab3:pgtbl
MIT6.S081-Lab3 Pgtbl: answer-pgybl.txt 的讲解

阅读指路:
xv6 book Chapter3
kern/memlayout.h: 记录内存布局
kern/vm.c: 虚拟内存管理
kernel/kalloc.c: 分配和释放物理内存

Speed up system calls (easy)√

一些操作系统 (例如Linux) 在特定的只读区域共享用户态和内核态数据,用于加速特定的系统调用。这消除了系统调用时的上下文切换。

目标

实现 getpid() 系统调用的优化。

方法

每个进程创建时,在 USYSCALL 处(memlayout.h 中定义的虚拟地址)映射一个只读的物理页, 在该页的起始处保存一个 struct usyscall ,初始化为当前进程的 PID
本次实验中,user/ulib.c 提供了 ugetpid() 函数,它会使用 USYSCALL 映射来获取进程的 PID

memlayout.h 部分代码:

// map the trampoline page to the highest address,
// in both user and kernel space.
#define TRAMPOLINE (MAXVA - PGSIZE)

// map kernel stacks beneath the trampoline,
// each surrounded by invalid guard pages.
#define KSTACK(p) (TRAMPOLINE - (p)*2*PGSIZE - 3*PGSIZE)

// User memory layout.
// Address zero first:
//   text
//   original data and bss
//   fixed-size stack
//   expandable heap
//   ...
//   USYSCALL (shared with kernel)
//   TRAPFRAME (p->trapframe, used by the trampoline)
//   TRAMPOLINE (the same page as in the kernel)
#define TRAPFRAME (TRAMPOLINE - PGSIZE)

#ifdef LAB_PGTBL
#define USYSCALL (TRAPFRAME - PGSIZE)
struct usyscall {
  int pid;  // Process ID
};
#endif

ugetpid() 函数定义:

// [user/ulib.c]
#ifdef LAB_PGTBL
int
ugetpid(void)
{
  struct usyscall *u = (struct usyscall *)USYSCALL;
  return u->pid;
}
#endif

Hints:

  • kernel/proc.cproc_pagetable() 完成内存映射(mapping)
  • 用户权限设置为只读
  • kernel/vm.c中的mappages() 是很有用的函数
// Create PTEs for virtual addresses starting at va that refer to
// physical addresses starting at pa. va and size might not
// be page-aligned. Returns 0 on success, -1 if walk() couldn't
// allocate a needed page-table page.
int
mappages(pagetable_t pagetable, uint64 va, uint64 size, uint64 pa, int perm)
  • allocproc() 中分配和初始化物理页
  • freeproc() 中释放物理页

主要工作:

proc.h 定义的 proc 结构体中添加变量,存储共享页的【物理地址】:

  struct usyscall *usyscall;

allocproc() 函数中,分配新的物理页并初始化

// 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;

  for(p = proc; p < &proc[NPROC]; p++) {  // 寻找是否有UNUSED状态的进程p
    acquire(&p->lock);
    if(p->state == UNUSED) {
      goto found;
    } else {
      release(&p->lock);
    }
  }
  return 0;

found:  // 找到UNUSED进程
  p->pid = allocpid();  // 进程pid
  p->state = USED;  // 进程状态为USED

  // Allocate a trapframe page. 
  if((p->trapframe = (struct trapframe *)kalloc()) == 0){
    freeproc(p);  // 如果出错,释放该进程内存并返回0
    release(&p->lock);
    return 0;
  }

/* STRAT MY CODE */
  // Allocate a usyscall page.
  if((p->usyscall = (struct usyscall *)kalloc()) == 0){
    freeproc(p);
    release(&p->lock);
    return 0;
  }
  memmove(p->usyscall, &p->pid, sizeof(int)); // 初始化为存储当前进程号
/* END MY CODE*/
  
  // An empty user page table. 分配用户页表
  p->pagetable = proc_pagetable(p);	
  // 【调用proc_pagetable():其中完成usyscall为起始地址的物理页到进程页表的内存映射!】
  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.
  memset(&p->context, 0, sizeof(p->context));
  p->context.ra = (uint64)forkret;
  p->context.sp = p->kstack + PGSIZE;

  return p;
}

kernel/proc.cproc_pagetable() 完成内存映射(mapping)

// 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;

  // An empty page table.
  pagetable = uvmcreate(); // create an empty user page table, returns 0 if out of memory.
  if(pagetable == 0)
    return 0;

  // map the trampoline code (for system call return)
  // at the highest user virtual address.
  // only the supervisor uses it, on the way
  // to/from user space, so not PTE_U.
  if(mappages(pagetable, TRAMPOLINE, PGSIZE,
              (uint64)trampoline, PTE_R | PTE_X) < 0){	
// mappages:
//Create PTEs. Returns 0 on success, -1 if walk() couldn't allocate a needed page-table page.
    uvmfree(pagetable, 0);
// uvmfree:
// Free user memory pages,then free page-table pages.
    return 0;
  }

  // map the trapframe just below TRAMPOLINE, for trampoline.S.
  if(mappages(pagetable, TRAPFRAME, PGSIZE,
              (uint64)(p->trapframe), PTE_R | PTE_W) < 0){
    uvmunmap(pagetable, TRAMPOLINE, 1, 0);
// uvmunmap:
// Remove npages of mappings starting from va.
    uvmfree(pagetable, 0);
    return 0;
  }

/*START MY CODE*/
  if(mappages(pagetable, USYSCALL, PGSIZE,
              (uint64)(p->usyscall), PTE_R | PTE_U) < 0){
    // 注意设定PTE_U
    // 如果内存映射失败,恢复以上页面:
    uvmunmap(pagetable, TRAMPOLINE, 1, 0);
    uvmunmap(pagetable, TRAPFRAME, 1, 0);
    uvmfree(pagetable, 0);
    return 0;
  }
/*END MY CODE*/

  return pagetable;
}

freeproc() 函数中释放 p->usyscall 对应的物理页内存:

// free a proc structure and the data hanging from it,
// including user pages.
// p->lock must be held.
static void
freeproc(struct proc *p)
{
  /* START MY CODE */
  if(p->usyscall)
    kfree((void*)p->usyscall);
  p->usyscall = 0;
  /* END MY CODE*/
  
  if(p->trapframe)
    kfree((void*)p->trapframe);
  p->trapframe = 0;
  if(p->pagetable)
    proc_freepagetable(p->pagetable, p->sz);
  p->pagetable = 0;
  p->sz = 0;
  p->pid = 0;
  p->parent = 0;
  p->name[0] = 0;
  p->chan = 0;
  p->killed = 0;
  p->xstate = 0;
  p->state = UNUSED;
}

最后需要在 proc_freepagetable()释放页表中对应的页表项

// 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);
  /* START MY CODE */
  uvmunmap(pagetable, USYSCALL, 1, 0);
  /* END MY CODE */
  uvmfree(pagetable, sz);
}

Question:
还有哪些xv6系统调用可以通过共享页来加速?如何实现?
Answer:
系统调用sysinfo可以通过共享页加速。
系统在创建共享页时将sysinfo所需的信息存入,用户只需要读取即可。

Print a page table (easy)

为了可视化RISC-V页表,或帮助以后进行调试,第二个任务是写一个函数来输出页表的内容。

目标:

定义 vmprint() 函数,有一个pagetable_t类型的参数,按下面描述的格式输出页表:
(在 exec.c 中的 “return argc;” 前面加入 if(p->pid==1) vmprint(p->pagetable) ,输出第一个进程的页表)

当你启动xv6时,在第一个进程完成exec()初始化时,输出以下页表信息:
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

第一行显示vmprint的参数,之后,每个PTE都有一行,包括引用树中较深的页表页的PTE。
每个PTE行都有一些以“…”的缩进,表示它在树中的深度。
每个PTE行显示了的PTE索引,包括页表页、PTE位以及从PTE提取的物理地址。不输出无效的PTE。(在上面的示例中,第一级页表页具有0和255的映射;对于 entry 0,下一级只映射了索引0,该索引0映射了最后一级的0、1、2)

你的代码输出的物理地址与上面显示的可能不相同。但显示项数和虚拟地址应相同。

Hints:

  • kernel/vm.c里面写 vmprint() 函数 √
  • 使用在 kernel/riscv.h 最后部分定义的宏 √
  • freewalk() 函数可能会有所启发 √
  • kernel/defs.h 中定义 vmprint 的原型,这样在 exec.c 中就可以调用它 √
  • printf 中使用 %p 输出十六进制表示的64位页表项PTE,以及物理地址(如上例)√

准备工作:

exec.c 加入 vmprint() 调用:(和建议的调用语句实现上稍有不同)

int
exec(char *path, char **argv)
{
  ......
  if(p->pid == 1){
    printf("page table %p\n", p->pagetable);
    vmprint(p->pagetable, 1);
  }

  return argc; // this ends up in a0, the first argument to main(argc, argv)
  ......
}

defs.hvm.c 部分加入 vmprint 函数原型:

void            vmprint(pagetable_t, int);

主要工作:vmprint 函数的实现

先来看一下 hints 里面提到的 vm.c 中的函数 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);
}

有了以上的想法,vmprint 实现只需要遍历页表按格式输出页表信息:

// Recursively print page table information:
void vmprint(pagetable_t pagetable, int level){

  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);
      for(int t=0;t<level;t++){	// 对应页表的级数level进行输出
        printf(" ..");
      }
      printf("%d: pte %p pa %p\n", i, pte, child);
      vmprint((pagetable_t)child, level + 1);  // 递归过程
    }else if(pte & PTE_V){
      uint64 child = PTE2PA(pte);
      for(int t=0;t<level;t++){
        printf(" ..");
      }
      printf("%d: pte %p pa %p\n", i, pte, child);
    }
  }
}

确实是比较简单的一个任务,按照指导来做很快就能完成,有一点小小的充实感,开心!

Question:

2.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?

Now when you start xv6 it should print output like this,
describing the page table of the first process at the point 
when it has just finished exec()ing init:

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

Answer:
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,page1用于保护page2不被用户程序访问。

Detecting which pages have been accessed (hard)

垃圾收集器 garbage collectors(自动内存管理的一种形式)可以从已访问(读或写)页面的信息中获益。在本部分的实验中,你将向xv6添加一个新特性,通过检查RISC-V页表中的访问位来获取信息并向用户空间报告这些信息。 RISC-V硬件页面遍历器在解决TLB miss时在PTE中标记这些位。

目标:

实现 pgaccess() 函数:报告已访问哪些页面的系统调用。
系统调用接受三个参数:
第一个参数是需要检查第一个用户页面的起始虚拟地址。
第二个参数是需要检查页数。
最后一个是参数用户缓冲区的地址,检查结果以位掩码(一种数据结构,每页使用一位,第一页对应于最低有效位)的形式存储在这个缓冲区中。

Hints:

  • 首先在 kernel/sysproc.c 中实现 sys_pgaccess()

  • 用argaddr()和argint()解析参数。

  • 对于输出位掩码,在内核中存储一个临时缓冲区并在填充正确的位后将其复制给用户(通过copyout())更容易。

  • 可以设置可扫描页数的上限。

  • kernel/vm.c中的walk()对于查找正确的PTE非常有用。

  • 你需要在kernel/riscv.h中定义PTE_A,即访问位。请参阅RISC-V手册以确定其值。

  • 如果PTE_A已设置,在检查后务必清除它否则,将无法确定自上次调用pgaccess()以来是否访问了页面(该位将被永久设置)

主要工作:

首先在 kernel/riscv.h 中设定 PTE_A(访问位)

// in [kernel/riscv.h]
#define PTE_V (1L << 0) // valid
#define PTE_R (1L << 1)
#define PTE_W (1L << 2)
#define PTE_X (1L << 3)
#define PTE_U (1L << 4) // 1 -> user can access
#define PTE_A (1L << 6) //【设定访问位】

实现 sys_pgaccess() 系统调用:

// in [kernel/sysproc.c]
int
sys_pgaccess(void)
{
  // lab pgtbl: your code here.  
  uint64 va;
  uint64 mask;
  int num;  
  const int maxnum = 64;	
  // 【设定可扫描页数的上限,设定为 64 是因为掩码类型 uint64 最多存储 64 位的值】

  if(argaddr(0, &va) < 0 || argint(1, &num) < 0 || argaddr(2, &mask) < 0) // 解析参数
    return -1;
  
  if(num > maxnum)
    num = maxnum;
  
  struct proc *p = myproc();
  if(p == 0)
    return -1;
  
  pagetable_t pagetable = p->pagetable;  // 找到进程的页表
  uint64 procmask = 0;	// 【设定掩码初值为0】

  for (int i = 0; i < num; i++){
    // walk:
    // Return the address of the PTE in page table pagetable
    // that corresponds to virtual address va.
    // 【循环调用walk函数,得到页表中制定虚拟地址对应的页表项,并检查每一页的PTE_A位】 
    pte_t *pte = walk(pagetable, ((uint64)va) + (uint64)PGSIZE * i, 0);
    if(pte == 0)
      continue;
    if (((*pte) & PTE_A)) {
        procmask |= (1L << i);  // 记录在第i个有效位
        *pte = *pte & (~PTE_A); // 记录后清除PTE_A位,保证每一次pgaccess的正确性
    }
  }
  // copyout: Copy from kernel to user.
  // 调用copyout, 将procmask的值复制到页表pagetable中的虚拟地址mask.
  return copyout(pagetable, mask, (char *) &procmask, sizeof(uint64));
}
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值