Lab3: page tables

Overview

  • 本 lab 关于页表(page table),涉及到 xv6 的虚拟内存、物理内存、三级页表、内存访问权限等内容
    • Speed up system calls:要在内核和用户态之间创建一个共享的只读页,使用户态能够直接读取内核态写入的数据,从而加速系统调用
    • Print a page table:要在 exec 中插入一个打印函数,使得 xv6 启动时会打印首个进程的页表信息。我们要实现这个打印函数,将三级页表的所有可用 PTE 均打印出来
    • Detect which pages have been accessed:利用 PTE_A 位,监控有哪些 PTE 被访问(read/write)过,将结果返回给用户态
  • 用xv6-book上的一张图来展示xv6的页表设计
    • image.png|475
    • 虚拟地址只用了39位
    • 页表有2272^{27}227个页表项(PTE)的数组,分三级来排布,每一级有 512 个 PTE
    • 每一个 PTE 包含 44 位的物理页号(PPN)和 10 位的 Flags
    • 第一、二级的PPN指向下一级页表的 PTE,而第三级的PPN指向物理内存
    • 映射时,xv6 先通过 39 位虚拟地址的高 27 位来寻找第三级 PTE(通过三级页表映射),然后将其中的 44 位 PPN 和虚拟地址剩下的 12 位组合成 56 位的物理地址

Speed up system calls (easy)

  • 为了保护数据安全,用户态是不能直接读取内核态的数据,而是要通过系统调用。如果创建一个可读 PTE 指向一块内存,该 PTE 是用户态和内核态共享的,那么用户态就可以直接读取这块内核数据,而无需经过复杂的系统调用
  • 以 getpid 为例,proc结构是内核态数据,用户态无法直接读取,因此需要通过系统调用 getpid 来读取 pid。
    • 所以需要我们创建一个共享 PTE,将虚拟地址 USYSCALL 映射到 pid 的物理地址,这样用户态直接读取 USYSCALL 就可以获取到 pid 了
// user/ulib.c
int
ugetpid(void)
{
  struct usyscall *u = (struct usyscall *)USYSCALL;
  return u->pid;
}
  • 首先,我们 proc 新增一个字段,存放 usyscall 的地址
// kernel/proc.h 
struct proc 
{ 
   // ... 
   struct usyscall *usyscall; 
}
  • 然后,在 allocproc 时,为其分配一块物理内存,参考已给出的Allocate a trapframe page(进程的陷阱帧的分配)代码
// kernel/proc.c
static struct proc*
allocproc(void)
{
  // ...
  // Allocate a trapframe page.
  if((p->trapframe = (struct trapframe *)kalloc()) == 0)
  {
    freeproc(p);
    release(&p->lock);
    return 0;
  }

  // 为共享页分配空间
  #ifdef LAB_PGTBL 
  if ((p->usyscall = (struct usyscall*)kalloc()) == 0) 
  {
    freeproc(p);
    release(&p->lock);
    return 0;
  }
  p->usyscall->pid = p->pid;
  #endif
  // ...
}
  • proc 有一个字段名为 pagetable,用于存放页表的地址。页表的初始化位于函数 proc_pagetable 中,通过 mappages 在页表中注册新的 PTE,参考 TRAPFRAME 的方式,将 USYSCALL 映射到 p->usyscall 中
    • 注意:这里的flag 位不仅仅要置位 PTE_R,还要置位 PTE_U,否则用户态无权访问
// kernel/proc.c
pagetable_t
proc_pagetable(struct proc *p)
{
  pagetable_t pagetable;
  // ...
  // map the trapframe page just below the trampoline page, for
  // trampoline.S.
  if(mappages(pagetable, TRAPFRAME, PGSIZE,
              (uint64)(p->trapframe), PTE_R | PTE_W) < 0){
    uvmunmap(pagetable, TRAMPOLINE, 1, 0);
    uvmfree(pagetable, 0);
    return 0;
  }

  #ifdef LAB_PGTBL
  // 尝试将进程的usyscall地址映射到用户空间的USYSCALL
  if(mappages(pagetable, USYSCALL, PGSIZE,(uint64)p->usyscall, PTE_R | PTE_U) < 0)
  {
    uvmunmap(pagetable, USYSCALL, 1, 0);
    uvmfree(pagetable, 0);
  }
  #endif

  return pagetable;
}
  • 新增完映射后,我们需要在进程 free 时对其解映射,同样参考 TRAPFRAME 的代码
// kernel/proc.c
void
proc_freepagetable(pagetable_t pagetable, uint64 sz)
{
  uvmunmap(pagetable, TRAMPOLINE, 1, 0);
  uvmunmap(pagetable, TRAPFRAME, 1, 0);
  #ifdef LAB_PGTBL
  uvmunmap(pagetable, USYSCALL, 1, 0);
  #endif
  uvmfree(pagetable, sz);
}
  • 前面分配了空间,用完以后肯定要释放,因此需要在 freeproc 处将其释放,同样参考trapframe的实现
// kernel/proc.c
static void
freeproc(struct proc *p)
{
  if(p->trapframe)
    kfree((void*)p->trapframe);
  p->trapframe = 0;
  
  #ifdef LAB_PGTBL
  if (p->usyscall)
      kfree((void*)p->usyscall);
  p->usyscall = 0;
  #endif
  // ...
}

Print a page table (easy)

  • 实现一个内核函数 vmprint,其接收一个 pagetable,能够将其中所有的可用 PTE 的信息全部打印出来
  • 利用DFS实现(因为就是三级页表,所以直接用三层for循环嵌套也可以实现)
    • 在多级页表中,中间层的页表条目通常只设置存在位,而最底层的页表条目会设置存在位和权限位,由此可以作为递归终止判断条件(这在下面的结果中也可以得到印证)
// kernel/vm.c
void
vmprint_dfs(pagetable_t pagetable, uint depth)
{
  static char* prefix[] = {"","..",".. ..",".. .. .."};

  for (int i = 0; i < 512; i++) 
  {
    pte_t pte = pagetable[i];
    if (pte & PTE_V) 
    {
      pte_t child = PTE2PA(pte);
      printf("%s%d: pte %p pa %p\n", prefix[depth], i, pte, child);
      // 确保有下一级页表(而不是指向实际的物理地址)
      if (child && ((pte & (PTE_R | PTE_W | PTE_X))==0)) 
        vmprint_dfs((pagetable_t)child, depth + 1);
    }
  }
}

void
vmprint(pagetable_t pagetable)
{
  printf("page table %p\n", pagetable);
  vmprint_dfs(pagetable, 1);
}
  • 在 exec 中当 pid 为1时调用 vmprint
// kernel/exec.c
int
exec(char *path, char **argv)
{
  // ...
  if(p->pid == 1)
    vmprint(p->pagetable);
  // ...
}
  • make qemu 时即可打印出首进程的页表信息。
xv6 kernel is booting

hart 1 starting
hart 2 starting
page table 0x0000000087f6b000
..0: pte 0x0000000021fd9c01 pa 0x0000000087f67000
.. ..0: pte 0x0000000021fd9801 pa 0x0000000087f66000
.. .. ..0: pte 0x0000000021fda01b pa 0x0000000087f68000
.. .. ..1: pte 0x0000000021fd9417 pa 0x0000000087f65000
.. .. ..2: pte 0x0000000021fd9007 pa 0x0000000087f64000
.. .. ..3: pte 0x0000000021fd8c17 pa 0x0000000087f63000
..255: pte 0x0000000021fda801 pa 0x0000000087f6a000
.. ..511: pte 0x0000000021fda401 pa 0x0000000087f69000
.. .. ..509: pte 0x0000000021fdcc13 pa 0x0000000087f73000
.. .. ..510: pte 0x0000000021fdd007 pa 0x0000000087f74000
.. .. ..511: pte 0x0000000020001c0b pa 0x0000000080007000
init: starting sh
$ 

Detect which pages have been accessed (hard)

  • 实现一个系统调用 sys_pgaccess,从一个虚拟地址对应的 PTE 开始,往下搜索一定数量的被访问过的页表,并把结果通过 mask 的方式返回给用户。每当 sys_pgaccess 调用一次,页表被访问标志就要清 0
  • 怎么知道哪些页表被访问了?
    • 每个 PTE 有个 PTE_A 位,该位被置 1 则说明被访问过。置位操作有硬件完成,无需我们考虑。但是,硬件只能做到置位,无法做到复位。因此每次 sys_pgaccess 时要手动将 PTE_A 复位 0
  • 怎么通过虚拟地址依次遍历后续 PTE?
    • PTE 是连续的,一个 PTE 大小为 PGSIZE,因此只要将虚拟地址按 PGSIZE 累加即可得到后续的 PTE
walk(p->pagetable, va + i * PGSIZE, 0); // 后续第i个PTE
  • PTE_A 是第 6 位
    • image.png|475
  • sys_pgaccess,接收三个参数,分别为:起始虚拟地址、 遍历页数目、 用户存储返回结果的地址。因为其是系统调用,故参数的传递需要通过 argaddr、argint 来完成
    • 通过不断的 walk 来获取连续的 PTE,然后检查其 PTE_A 位,如果为 1 则记录在 mask 中,随后将 PTE_A 手动清 0。最后,通过 copyout 将结果拷贝给用户即可
// kernel/sysproc.c
int
sys_pgaccess(void)
{
  struct proc* p = myproc();

  uint64 va;              // 待检测页表起始地址
  int num_pages;          // 待检测页表的页数
  uint64 access_result;   // 记录检测结果掩码的地址
  uint64 result = 0;      // 临时记录结果

  // 从用户栈中获取参数
  argaddr(0, &va);  
  argint(1, &num_pages);
  argaddr(2, &access_result);

  if (num_pages <= 0 || num_pages > 512)
    return -1;

  // 遍历页表
  for (int i = 0; i < num_pages; i++)
  {
    pte_t* pte = walk(p->pagetable, va + i * PGSIZE, 0);
    if (pte && (*pte & PTE_V) && (*pte & PTE_A))
    {
      // 清除访问位
      *pte &= ~PTE_A;  
      result |= (1 << i);
    }
  }

  // 将检测结果写入用户栈
  copyout(p->pagetable, access_result, (char*)&result, sizeof(result));
  return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值