Speed up system calls (easy)

要求

一些操作系统(比如Linux)通过在用户与内核之间只读的一块区域共享数据来加速系统调用。这能消减执行这些系统调用时用户态与内核态切换的开销。为了帮助你们理解如何在页表中插入映射,你的第一个任务是为getpid实现这种优化。

每当一个进程被创建时,在USYSCALL(在memlayout.h中定义)位置建立一个只读内存页的映射。在这页开始,存储一个struct usyscall,并初始化该结构体以保存当前进程ID。实验中的ugetpid()已在用户空间提供且会自动使用USYSCALL映射。

提示:

  • 你可以在kernel/proc.cproc_pagetable()中实现映射
  • 选择合适的权限置位以满足用户只读此页
  • mappages()是有用的工具
  • 切记在allocproc()中开辟内存页的空间并初始化
  • 确保在freeproc()中释放该内存页

问题:

哪个(哪些)系统调用能通过这个共享内存页改善性能?如何实现?

实现

步骤:

  1. struct proc中添加struct usyscall指针字段

    struct proc {
      ...  // 其他字段不变
      struct usyscall* usyscall; // 添加`struct usyscall`指针字段
    };
  2. proc_pagetable()中添加usyscall物理地址到虚拟地址的映射:

    pagetable_t proc_pagetable(struct proc *p) {
      ...
      // 这一段是源码目的是添加p->trapframe物理地址到
      // 虚拟地址TRAPFRAME的映射,为我们提供了很好的参考
      if(mappages(pagetable, TRAPFRAME, PGSIZE,
           (uint64)(p->trapframe), PTE_R | PTE_W) < 0){
         uvmunmap(pagetable, TRAMPOLINE, 1, 0);
         uvmfree(pagetable, 0);
         return 0;
      }
      // 参考上边,添加usyscall内存页的映射
      // 权限要求用户只读,因此置位PTE_R | PTE_U
      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 pagetable;
    }
  3. 同样,建立映射后还需要有对应的位置解除映射(在free_pagetable()中)

    void proc_freepagetable(pagetable_t pagetable, uint64 sz) {
      uvmunmap(pagetable, USYSCALL, 1, 0); // 参考下边添加USYSCALL解除映射命令
      uvmunmap(pagetable, TRAMPOLINE, 1, 0);
      uvmunmap(pagetable, TRAPFRAME, 1, 0);
      uvmfree(pagetable, sz);
    }
  4. allocproc()中开辟空间并初始化

    static struct proc* allocproc(void) {
      ... 
      // Allocate a trapframe page.
      if((p->trapframe = (struct trapframe *)kalloc()) == 0) {
         freeproc(p);
         release(&p->lock);
         return 0;
      }
      // 参考上边例子开辟usyscall内存页并初始化
      if ((p->usyscall = (struct usyscall*) kalloc()) == 0) {
         freeproc(p);
         release(&p->lock);
         return 0;
      }
      p->usyscall->pid = p->pid;
      ...
    }
  5. freeproc()中释放内存页

    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;
      ...
    }

结果

运行结果:
image.png

pgacess是要在下边实验中实现的功能,所以还不能通过

测试结果:
image.png

Print a page table (easy)

要求

为使RISC-V页表可视化且可能在后续实验中提供帮助,你的第二个任务是实现打印页表内容的功能

定义函数vmprint(),读取一个pagetable_t参数,并按照下列格式打印页表信息。在exec.creturn argc命令前插入if(p->pid==1) vmprint(p->pagetable)命令来打印第一个进程的页表。

格式:

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

提示:

  • 你可在kernel/vm.c中实现vmprint()
  • 使用在kernel/riscv.h最后定义的宏
  • 函数freewalk具有参考价值
  • defs.h中声明vmprint
  • 在printf中使用%p打印64位PTE和地址

实现

阅读freewalk源码

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

可得到信息:

  • 一个页表有512个PTE(页表项)
  • 通过pte & PTE_V判断一个PTE是否有效
  • 通过(pte & (PTE_R|PTE_W|PTE_X)) == 0判断一个PTE是否指向下一个页表
  • PTE2PA这个宏可帮助我们计算PTE的物理地址

vmprint实现思路:

  • 除第一行外,可通过递归打印PTE及其物理地址
  • 只打印有效PTE,通过pte & PTE_V判断
  • 递归终止条件:只对指向下个页表的PTE做递归,通过(pte & (PTE_R|PTE_W|PTE_X)) == 0判断

由此,可借助printPTE实现vmprint:

void printPTE(pagetable_t pt, int depth) {
  int i, j;
  static char* dots = " ..";
  for (i = 0; i < 512; ++i) {
    if (pt[i] & PTE_V) {  // 若PTE可用则打印
      uint64 pa = PTE2PA(pt[i]);
      for (j = 0; j < depth; ++j) printf(dots);
      printf("%d: pte %p pa %p\n", i, pt[i], pa);
      if ((pt[i] & (PTE_R|PTE_W|PTE_X)) == 0) {
        // 若PTE不可读不可写不可知执行说明指向下一个页表
        // 因此需要递归打印,深度+1
        printPTE((pagetable_t)pa, depth + 1);
      }
    }
  }
}

void vmprint(pagetable_t pt) {
  printf("page table %p\n", pt);
  printPTE(pt, 1);
}

结果

运行结果:
image.png
测试结果:
image.png

Detect which pages have been accessed (hard)

要求

为xv6实现一个新功能:通过检查 RISC-V 页表中的访问位,检测并报告用户空间访问了哪些页。

实现系统调用pgacess,报告访问过哪些页面。该系统调用需要三个参数:(1)第一个用户需要检测的页面的起始虚拟地址;(2)需要检测的页面数量;(3)一个指向缓存的用户地址,用以储存位掩码结果。

提示:

  • user/pgtlbtest.c中了解如何使用 pgaccess
  • kernel/sysproc.c中实现sys_pgacess,可用argaddr()argint()传递参数
  • 对于位掩码,可保存在内核临时缓存区,在获取正确的位掩码之后通过copyout()拷贝给用户
  • 可设置扫描页面数的上限
  • kernel/vm.c中的walk()有助于找到正确的PTEs(页表项)
  • kernel/riscv.h 定义访问位PTE_A,参考手册确定其值
  • 在检查一个页面的PTE_A位是否被设置后,切记要恢复它。否则无法检测自上次pgacess之后用户是否再次访问它
  • vmprint()可能对debug有帮助

实现

  1. 根据user/pgtlbtest.cpgaccess的调用和user/user.h中的声明可知该函数的返回值和参数类型。

    int pgaccess(void *base, int len, void *mask);
  2. 由此可知在sys_pgacess()中需要传递三个参数,依次分别为64位地址、32位整型、64位地址:(此处我将内核pagacess函数参数的定义与用户保持一致)

    int sys_pgaccess(void) {
        // lab pgtbl: your code here.
        int npage;
        uint64 start_addr, res_addr;
        argaddr(0, &start_addr);
        argint(1, &npage);
        argaddr(2, &res_addr);
        return pgaccess(start_addr, npage, res_addr);
    }
  3. 通过阅读walk源码得知该函数的作用:通过给定的虚拟地址和页表指针,可返回该虚拟地址在该页表中对应的PTE的指针(物理地址)
  4. 由此可借助walk函数实现pgaccess

    int pgaccess(uint64 start_addr, int npage, uint64 res_addr) {
        int i;
        uint res = 0;
        pte_t* pte;
        if (start_addr > MAXVA) {
            panic("pgaccess: too large virtual address");
        } 
        if (npage > 32) {
            panic("too many pages");
        }
        struct proc* p = myproc();
        for (i = 0; i < npage; ++i) {
            pte = walk(p->pagetable, start_addr + i * PGSIZE, 0);
            // 通过walk获得遍历的虚拟地址在页表中的PTE
            // 若该PTE的置位可行且有PTE_A则代表被访问过
            if ((*pte & PTE_V) && (*pte & PTE_A)) {
                res |= (1 << i);
                *pte ^= PTE_A;  // 切记将PTE_A位恢复
            }
        }
        // 通过copyout将结果拷贝给用户地址
        return copyout(p->pagetable, res_addr, (char*)&res, sizeof(res));
    }
  5. 关于PTE_A的定义:由下图可知PTE_A应当对应索引为6的比特位,因此在riscv.h中添加#define PTE_A (1L << 6)
    image.png

结果

运行结果:
image.png
可以看到这次pgaccess_test就ok了
测试结果:
image.png

总结

make grade结果

image.png

个人收获

  1. 内核为每个进程维护的结构体struct proc中储存了一个页表指针(该指针是物理地址),这个页表中存储的东西被称为PTE(页表项)
  2. PTE存储的信息包括物理页表号(PPN)和一些权限置位。PPN表示一个物理地址,权限职位表示该地址权限和种类(比如不可读不可写不可执行时,这个地址指向下一级页表)
    image.png
  3. 在xv6中一个虚拟地址包含的信息:三级页表的索引和一个偏置:
    image.png
  4. 由一个虚拟地址到物理地址的过程:

    • 通过satp寄存器获取L2级页表地址,并通过虚拟地址中L2级页表索引获取PTE
    • 该PTE对应的物理物理地址即下一级页表指针,同理一直到获取L0级页表PTE
    • L0级页表PTE对应物理地址+虚拟地址中的偏置级该虚拟地址对应的物理地址

Longfar
1 声望4 粉丝