MIT 6.S081 2021: Lab page tables

Speed up system calls

这个实验的目的是要“加速系统调用”,怎么加速呢?在内核和用户程序之间创建一个共享的只读页,这样内核往这个页里写入数据的时候,用户程序就可以不经复杂的系统调用直接读取它了。实验要求,把一个只读页从USYSCALL(memlayout.h中定义的一个虚拟地址)映射的内核的某一个地方,并在页的起始处存储一个结构体struct usyscall。提示说"ugetpid() has been provided on the userspace side and will automatically use the USYSCALL mapping",看一下ugetpid()这个函数。

 


 

图里面#ifdef 下面是灰的,不用管它,Makefile里面已经设置了相应的CFLAGS,编译的时候会自动加上这个LAB_PGTBL宏。

很显然,ugetpid()直接从USYSCALL这个地址读数据,因此我们需要把usyscall结构写到此页表的开头。

看一下USYSCALL是什么东西。trampoline这个词的本意是蹦床,在这里,它是用来进行trap to the kernel操作的。下面的trapframe是保存了进程的一些参数。这个USYSCALL是紧邻trapframe下端的一页。(xv6手册第三章里面有用户进程和内核的内存分布图):


然后提示说在proc_pagetable里面设置映射。这个proc_pagetable里面有两个显然是在进行map操作的函数,看来对USYSMAP的映射就在这里进行:

 


 

问题是到底把USYSCALL映射到哪儿呢?看xv6手册里的这段话"When creating each process, xv6 allocates a page for the process’s trapframe, and arranges for it always to be mapped at user virtual address TRAPFRAME, which is just below TRAMPOLINE."xv6是先给trapframe分配一块内存再把TRAPFRAME映射到它上面。看一下allocproc(),这个程序首先循环搜索进程表,搜索到UNUSED进程就为其分配内存,然后给进程表p赋各种值。重点看这一段:


很显然p->trapframe就是在这里初始化的,在allocproc()初始化之后在proc_pagetable()之中映射。我们可以仿照trapframe的操作,在struct proc中添加一个参数struct usyscall *usyspage,然后用kalloc()分配一页内存,地址指向usyspage,并把该进程的pid存到页表中。稍后我们就把用户内存中的USYSCALL映射到这里。

  //给这个usyscall分配一个页面
  if((p->usyspage = (struct usyscall *)kalloc()) == 0){
    freeproc(p);
    release(&p->lock);
    return 0;
  }
​
  p->usyspage->pid=p->pid;

usyspage有了值,就可以做映射操作了。这里要求只读页,因此把权限设成PTE_R,另外还要加上PTE_U,xv6手册里表明,不加PTE_U的页默认在supervisor mode里运行:

//map usyscall
  if(mappages(pagetable, USYSCALL, PGSIZE,
              (uint64)(p->usyspage), PTE_R | PTE_U) < 0){
    uvmunmap(pagetable, TRAMPOLINE, 1, 0);
    uvmunmap(pagetable, TRAPFRAME, 1, 0);
    uvmfree(pagetable, 0);
    return 0;
  }

注意,如果mappage失败的话,要撤销前面TRAMPOLINE和TRAPFRAME的映射。

然后做好释放,仿照freeproc里对trapframe里的操作来释放usyspage:

  //记得释放usyscall
  if(p->usyspage)
    kfree((void*)p->usyspage);
  p->usyspage = 0;

还要务必记得修改下面这个proc_freepagetable函数,加上对USYSCALL的操作:

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

Print a page table

提示中说到: The function freewalk may be inspirational.仿照vm.c里面的freewalk()直接写代码:

void vmprint(pagetable_t pagetable,int count)//count应该为0
{
  // there are 2^9 = 512 PTEs in a page table.
  if(count==0)
  {
    printf("page table %p\n",pagetable);
  }
  int arg_tmp=count+1;
  if(count!=3)
  {
    for(int i = 0; i < 512; i++){
    pte_t pte = pagetable[i];
    if(pte & PTE_V)
    {
      // this PTE points to a lower-level page table.
      uint64 child = PTE2PA(pte);
      
      if(count==0)
      {
        printf("..");
      }
      else if(count==1)
      {
        printf(".. ..");
      }
      else if(count==2)
      {
        printf(".. .. ..");
      }
      printf("%d: pte %p pa %p\n",i,pte,child);
​
      vmprint((pagetable_t)child,arg_tmp);
    }
    else
    {
      continue;
    } 
  }
  }
  
  return;
}

1.xv6使用的是三级页表。xv6访问页的时候,首先从指向第一级页表的寄存器satp开始(类似的东西在x86架构里叫CR3寄存器),使用虚拟地址的30~38位在页表里定位一项;如果该项的PTE_V这一位是1,则此页表项是有效的,可以根据此页表项内的地址访问下一级页表。依次类推,直到搜索到第三级,获取物理地址。由此可见,三级页表其实是一棵深度为3的树,我们可以使用BFS搜索来遍历这棵树。

2.使用BFS,遍历每个节点上的所有叶子(也就是页表项)。如果叶子的PTE_V为0,直接跳过;如果为1,先用宏PTE2PA把表项转换成物理地址,再递归调用这个地址。

3.使用一个计数变量count来记录递归深度,初始必须置为0,由于树的深度最大只有3,则count==等于3时直接返回。不过看这门课的录像,是实现了一个只传入页表物理地址的vmprint。我猜应该是声明了一个全局变量,进入函数时变量+1,退出时将这个变量-1。

 

Detecting which pages have been accessed

不要被标题后面那个红色的hard吓住,这道题其实不难!

实验要求:从一个用户页表地址开始,搜索所有被访问过的页并返回一个bitmap来显示这些页是否被访问过。比如说,如果第二个页被访问过了,bitmap里从右往左数第二个bit就是1。如果你用过sys/select.h里面的select()函数的话,就会知道里面的几个fd_set类型的参数就是bitmap。

那么这个“accessed(read or write)”是怎么回事呢?传给sys_pgaccess()的第一个参数是用户指针,即图中的buf。所谓的access就是直接对页进行写入,往*(buf+30*PGSIZE)等几个位置写入数据:


 

那么PTE_A是从哪里来的呢?看一下这句话:”The RISC-V hardware page walker marks these bits in the PTE whenever it resolves a TLB miss.“很显然,这个PTE_A位是由硬件设置的,所以我们只需要检测它就可以了。代码如下:

int
sys_pgaccess(void)
{
  // lab pgtbl: your code here.
  //先提取一下参数
  struct proc* p =myproc();
  uint64 usrpge_ptr;//待检测页表起始指针
  int npage;//待检测页表个数
  uint64 useraddr;//稍后写入用户内存
  argaddr(0,&usrpge_ptr);
  argint(1,&npage);
  argaddr(2,&useraddr);
  if(npage>64)
  {
    return -1;
  }
  uint64 bitmap=0;
  uint64 mask=1;
  uint64 complement=PTE_A;
  complement=~complement;
  int count=0;
​
  for(uint64 page =usrpge_ptr;page<usrpge_ptr+npage*PGSIZE;page+=PGSIZE)
  {
    pte_t* pte = walk(p->pagetable,page,0);
    if(*pte&PTE_A)
    {
      bitmap=bitmap|(mask<<count);
      *pte=(*pte)&complement;
    }
    count++;
    //printf("bitmap:%p\n",bitmap);
​
  }
  copyout(p->pagetable,useraddr,(char*)&bitmap,sizeof(bitmap));
​
  return 0;
}

1.首先用argint和argaddr传入三个参数。初始化bitmap,这里我用一个uint64变量来作为bitmap。

2.设置一个mask,用来修改bitmap里面的位;设置一个complement,PTE_A置0,其他位置1,用来清空原来PTE里的PTE_A位。设置一个计数器count,记录正在检查第几个页表。

3.遍历传入的页,使用walk函数找到对应的PTE,如果PTE_A存在,则将mask左移count位和bitmap做与运算(逻辑运算基本知识,a|0=a,a|1=1,因此bitmap其他位不变,唯独第count位一定被置为1),存回bitmap中。再清除PTE表中的PTE_A位,*pte和complement进行或运算(a&0=0,a&1=a,因此因此*pte其他位不变,唯独PTE_A位一定被置为0)。最后使用copyout写回即可。

posted @ 2021-11-19 22:40  LunaCancer  阅读(3089)  评论(1编辑  收藏  举报