MIT6.S081 Lab3: page tables

目录

前言:

3.1 Speed up system calls (easy)

3.1.1 实现USYSCALL映射

3.2 Print a page table (easy)

3.3 Detecting which pages have been accessed (hard

前言:

        本节实验和页表有关,分为两个easy和一个hard的实验,难度较高,需要事先对页表的相关知识与实验代码有一定的了解,否则做出来可能比较艰难。

3.1 Speed up system calls (easy)

        这一小节的实验室要我们实现共享的内存空间,可以将一些进程的内核数据放在该空间供用户使用,节省了系统调用从用户态转换到内核态的时间。

3.1.1 实现USYSCALL映射

        按照实验指导书上的内容,我们的需要实现的共享内存空间就是USYSCALL结构体,可以在kernel/memlayout.h中找到这个结构体的定义:

// kernel/memlayout.c

#ifdef LAB_PGTBL
#define USYSCALL (TRAPFRAME - PGSIZE)

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

        由于实验只要求加快getpid()的速度,因此此时USYSCALL结构体中只有进程号pid。

        为了能将进程中的内存空间和USYSCALL映射,我们需要先在进程的proc结构体中添加一个字段表示映射的内存空间:

struct proc {
  struct spinlock lock;
  ......
  struct trapframe *trapframe; // data page for trampoline.S
  struct usyscall *usyscall;   // data page for usyscall
  ......
};

        接着,我们需要再proc_pagetable()中实现对共享内存的映射,这一部分,我们可以参考这个函数中其他映射的实现:

pagetable_t
proc_pagetable(struct proc *p)
{
  pagetable_t pagetable;

  // An empty page table.
  pagetable = uvmcreate();
  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){
    uvmfree(pagetable, 0);
    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);
    uvmfree(pagetable, 0);
    return 0;
  }

  return pagetable;
}

         可以看到,正如实验指导书中所说,mappages()就是用来实现映射的,该函数的第一个参数是进程的页表,第二个参数为映射到的内存空间,第三个参数是页面大小,第四个参数被映射的内存空间,第五个参数表示的是该内存空间的权限,这个可以去查看xv6手册。此外,我们还可以发现,如果映射失败进入if处理语句后,会调用uvmunmap()和uvmfree()进行映射空间的释放,因此我们也需要在失败逻辑中对其进行释放,综上所述,映射的相关代码如下:

pagetable_t
proc_pagetable(struct proc *p)
{
  pagetable_t pagetable;

  ......
  // 对共享空间进行映射
  if (mappages(pagetable, USYSCALL, PGSIZE,
              (uint64)(p->usyscall), PTE_R | PTE_U) < 0) {
    // 如果映射失败,则将之前映射的空间释放
    uvmunmap(pagetable, TRAPFRAME, 1, 0);
    uvmunmap(pagetable, TRAMPOLINE, 1, 0);
    uvmfree(pagetable, 0);
    return 0;
  }
  return pagetable;
}

         其中,PTE_R表示可读,PTE_U表示允许用户态下访问该空间。

        接着,需要在allocproc()中为proc结构体中新增的字段usyscall分配空间,只要仿照为trapframe分配空间的代码就可以实现这一步:

static struct proc*
allocproc(void)
{
  struct proc *p;
  ......
found:
  p->pid = allocpid();
  p->state = USED;

  // Allocate a trapframe page.
  if((p->trapframe = (struct trapframe *)kalloc()) == 0){
    freeproc(p);
    release(&p->lock);
    return 0;
  }

  // Allocate a usyscall page.
  if ((p->usyscall = (struct usyscall *)kalloc()) == 0) {
    freeproc(p);
    release(&p->lock);
    return 0;
  }
  p->usyscall->pid = p->pid;
  
  ......

  return p;
}

        最后,实现释放usyscall空间的代码,同样模仿一下就能写出: 

// kerne/proc.c

static void
freeproc(struct proc *p)
{
  if(p->trapframe)
    kfree((void*)p->trapframe);
  p->trapframe = 0;
  if(p->usyscall)
    kfree((void*)p->usyscall);
  p->usyscall = 0;
  ......
}

void
proc_freepagetable(pagetable_t pagetable, uint64 sz)
{
  uvmunmap(pagetable, USYSCALL, 1, 0);
  uvmunmap(pagetable, TRAMPOLINE, 1, 0);
  ......
}

3.2 Print a page table (easy)

        本小节要实验打印输入的页表,将页表按照树状结构打印出来,只需打印用到的页表项,不需要打印没有被使用的。

        按照实验指导书的提示,我们首先需要定义并实现一个名为vmprint()的函数,该函数接受一个作为参数的页表,并将该页表打印出来:

// kernel/defs.h
// vm.c
void            kvminit(void);
......
void            vmprint(pagetable_t);
void            vmprint_level(pagetable_t, int);

// kernel/vm.c

void vmprint(pagetable_t pagetable) {
  // 使用%p打印16进制的pte和地址
  printf("page table %p\n", pagetable);
  // 实际打印页表的函数
  vmprint_level(pagetable, 1);
}

         按照提示,我们可以先看看kernel/vm.c中的freewalk函数,从中学习如何实现打印页表的函数vmprint_level():

// 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_V是用来判断页表项是否有效,而(pte & (PTE_R|PTE_W|PTE_X)) == 0则是用来判断是否不在最后一层。因为最后一层页表中页表项中W位,R位,X位起码有一位会被设置为1。注释里面说所有最后一层的页表项已经被释放了,所以遇到不符合的情况就panic("freewalk: leaf")

        仿造这个函数,我们很轻易的就能实现vmprint_level():

void vmprint_level(pagetable_t pagetable, int depth)
{
  // 根据当前深度填充打印出来的内容
  static char* pre[] = {
      "",
      "..",
      ".. ..",
      ".. .. .."
  };
  // 如果深度过小或过大直接打印错误
  if (depth <= 0 || depth >= 4) {
    panic("vmprint_helper: depth not in {1, 2, 3}");
  }
  for (int i = 0; i < 512; i++) {
    // pte表示页表pagetable的第i个页表项
    pte_t pte = pagetable[i];
    // 如果页面有效
    if (pte & PTE_V) {
      printf("%s%d: pte %p pa %p\n", pre[depth], i, pte, PTE2PA(pte));
      // 如果页面还有下一层索引
       if ((pte & (PTE_R|PTE_W|PTE_X)) == 0) {
        uint64 child = PTE2PA(pte);
        vmprint_level((pagetable_t)child, depth+1);
      }
    }
  }
}

3.3 Detecting which pages have been accessed (hard)

        这节实验是要实现一个函数,该函数返回页表中哪些页表页被访问过,并将这些页表页的访问位复位成0。

        首先按照实验指导书的提示,先定义一个字段PTE_A,该字段用来表示页表页是否被访问过,同时,根据xv6指导书,我们可以知道页面的标志位中,第6位表示页面是否被访问过:

         因此,新增的字段应该如下表示:

// kernel/riscv.h

#define PTE_V (1L << 0) // valid
......
#define PTE_A (1L << 6) // 表示页表页是否被访问过

        接下来,需要实现sys_paccess(),这个函数的功能主要是接收用户空间传进来的参数、检查页面是否被访问过、将结果返回给用户空间。 

       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.

        根据上述实验指导书的提示,我们可以知道,这个系统调用有三个参数,第一个参数是用户页面的起始虚拟地址,第二个参数是需要检查的页数,第三位是一个掩码,如果有页面被访问过,我们需要修改这个掩码来标识。对于检查页面是否被访问,通过循环来检查,每次获取下一个页面的地址进行判断。最后,将结果返回给用户空间可以通过之前用过的一个函数copyout()来实现,具体可以参考lab2中的用法。sys_paccess()的代码如下:

// kernel/sysproc.c

int
sys_pgaccess(void) {
  uint64 addr;
  int len;
  uint64 bitmask;
  // 接收用户空间传进来的参数
  if (argaddr(0, &addr) < 0) {
    return -1;
  }
  if (argint(1, &len) < 0) {
    return -1;
  }
  if (argaddr(2, &bitmask) < 0) {
    return -1;
  }
  // 防止给的参数页表长度过大或过小
  if (len > 32 || len < 0) {
    return -1;
  }

  // 存储访问结果,以掩码表示
  int count = 0;
  struct proc *p = myproc();
  for (int i = 0; i < len; ++i) {
    // 获取下一页的地址
    int va = addr + i*PGSIZE;
    // 查看该页面是否被访问
    int abit = vm_pgaccess(p->pagetable, va);
    // 修改结果
    count = count | abit << i;
  }
  
  // 将值复制给bitmask,返回给用户空间
  if (copyout(p->pagetable, bitmask, (char*)&count, sizeof count) < 0) {
    return -1;
  }
  return 0;
} 

int vm_pgaccess(pagetable_t pagetable, uint64 va)
{
  pte_t *pte;

  if(va >= MAXVA)
    return 0;
  // walk()为实验指导书中提供的函数,可以返回当前页面的pte
  pte = walk(pagetable, va, 0);
  if (pte == 0) {
    return 0;
  }
  
  if ((*pte & PTE_A) != 0) {
    // 如果当前页面被访问过则将PTE_A复位
    *pte = *pte & (~PTE_A);
    return 1;
  }
  return 0;
}

        

        

                

              

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值