MIT6.S081 Lab3 Page tables

lab1、2不是太难,lab 3太变态了,github上记一下代码,源代码地址 :https://github.com/CodePpoi/mit-lab

参考博客 : https://blog.csdn.net/u013577996/article/details/109582932?utm_medium=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7EOPENSEARCH%7Edefault-1.base&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7EOPENSEARCH%7Edefault-1.base

每个进程都有自己的页表,里面有很多页表项(即PTE)

Print a page table

1. 在defs.h里面声明vmprint()

void vmprint(pagetable_t);

2. 在vm.c里面实现vmprint

void
vmprint_level(pagetable_t pagetable, uint64 level)
{
  // there are 2^9 = 512 PTEs in a page table.
  for(int i = 0; i < 512; i++){
    pte_t pte = pagetable[i];
    uint64 child = PTE2PA(pte);
    if((pte & PTE_V) && (pte & (PTE_R|PTE_W|PTE_X)) == 0){
      // this PTE points to a lower-level page table.
      for(int j = 0; j < level; j++) {
        printf(".. ");
      }
      printf("..%d: pte %p pa %p\n", i, pte, child);
      vmprint_level((pagetable_t)child, level + 1);
    } else if(pte & PTE_V){
      for(int j = 0; j < level; j++) {
        printf(".. ");
      }
      printf("..%d: pte %p pa %p\n", i, pte, child);
    }
  }
}


void
vmprint(pagetable_t pagetable) {
  printf("page table %p\n", pagetable);
  vmprint_level(pagetable,(uint64) 0);
}

3.在exec.c里面调用vmprint

p->trapframe->epc = elf.entry;  // initial program counter = main
  p->trapframe->sp = sp; // initial stack pointer
  proc_freepagetable(oldpagetable, oldsz);

  if(p->pid==1) vmprint(p->pagetable);  //只要加这一行
  return argc; // this ends up in a0, the first argument to main(argc, argv)

至此完成

A kernel page table per process

1.在proc.h文件的proc结构体中添加pagetable_t:

pagetable_t kernel_pagetable

2. 找到allocproc,使用的命令是:

find . -name "*.c" | xargs grep "allocproc"

那么allocproc到底是干什么的呢,就是先去进程表找到一个unused进程,然后给该进程分配pid,trapframe 页表

3. 新增内核版本的kvminit,kvminit0

pagetable_t
kvminit0()
{
  pagetable_t kpt;
  kpt = (pagetable_t) kalloc();
  memset(kpt, 0, PGSIZE);

  // uart registers
  uvmmap(kpt, UART0, UART0, PGSIZE, PTE_R | PTE_W);

  // virtio mmio disk interface
  uvmmap(kpt, VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);

  // CLINT
  uvmmap(kpt, CLINT, CLINT, 0x10000, PTE_R | PTE_W);

  // PLIC
  uvmmap(kpt, PLIC, PLIC, 0x400000, PTE_R | PTE_W);

  // map kernel text executable and read-only.
  uvmmap(kpt, KERNBASE, KERNBASE, (uint64)etext-KERNBASE, PTE_R | PTE_X);

  // map kernel data and the physical RAM we'll make use of.
  uvmmap(kpt, (uint64)etext, (uint64)etext, PHYSTOP-(uint64)etext, PTE_R | PTE_W);

  // map the trampoline for trap entry/exit to
  // the highest virtual address in the kernel.
  uvmmap(kpt, TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);
  return kpt;
}

用户页表和内核页表两者之间的区别是什么? 从上图可以看出,内核页表中虚拟地址和物理地址是一样的,不过有一点比较奇怪的就是,既然一样,那为啥不直接用物理地址? 我觉得是因为,用户页表用的是虚拟地址,为了统一,所以内核也用的虚拟地址,那么为啥用户地址不直接用物理地址呢?是为了隔离,让不同的程序之间不会相互干扰,而内核页表为什么要有这些mapping呢?

为什么用户的(物理)地址空间小,而内核的地址空间在比较大的位置, 看下图,右下侧CLINT下面,boot ROM上面的就是用户地址空间,而内核的kernel text(代码),kernel data(运行时需要的数据,比如变量值),就在物理地址0x80000000开始,所以看起来内核地址空间比较大

"在完成了虚拟到物理地址的翻译之后,如果得到的物理地址大于0x80000000会走向DRAM芯片,如果得到的物理地址低于0x80000000会走向不同的I/O设备。"

"当你对主板上电,主板做的第一件事情就是运行存储在boot ROM中的代码,当boot完成之后,会跳转到地址0x80000000,操作系统需要确保那个地址有一些数据能够接着启动操作系统。"

MIT6.S081 Lab3 Page tables_第1张图片

4. 以及uvmmap 

void
uvmmap(pagetable_t pgt,uint64 va, uint64 pa, uint64 sz, int perm)
{
  if(mappages(pgt, va, sz, pa, perm) != 0)
    panic("uvmmap");
}

6. 然后让kernel/proc.c里的allocproc去调用kvminit0

//试图分配一张页表给p
p->kernel_pagetable = kvminit0();
//如果(比如因为内存不够),分配不成功
  if(p->kernel_pagetable == 0){
    //那么销毁这个进程
    freeproc(p);
    release(&p->lock);
    return 0;
  }

7. 因为所有的stack分配都是在procinit里面的,所以需要把procinit的一部分移动到到allocproc,先注释procinit的代码:

void
procinit(void)
{
  struct proc *p;

  initlock(&pid_lock, "nextpid");
  for(p = proc; p < &proc[NPROC]; p++) {
      initlock(&p->lock, "proc");

      // Allocate a page for the process's kernel stack.
      // Map it high in memory, followed by an invalid
      // guard page.
      /*char *pa = kalloc();
      if(pa == 0)
        panic("kalloc");
      uint64 va = KSTACK((int) (p - proc));
      kvmmap(va, (uint64)pa, PGSIZE, PTE_R | PTE_W);
      p->kstack = va;
     */
  }
  //kvminithart();
}
char *pa = kalloc();
      if(pa == 0)
        panic("kalloc");
      uint64 va = KSTACK((int) (p - proc));
      uvmmap(p->kernel_pagetable, va, (uint64)pa, PGSIZE, PTE_R | PTE_W);
      p->kstack = va;


memset(&p->context, 0, sizeof(p->context));
  p->context.ra = (uint64)forkret;

8. 修改kernel/proc.c里scheduler的内容,主要是设置kernel page table到satp寄存器

  if(p->state == RUNNABLE) {
        // Switch to chosen process.  It is the process's job
        // to release its lock and then reacquire it
        // before jumping back to us.
        p->state = RUNNING;
        c->proc = p;
        w_satp(MAKE_SATP(p->kernel_pagetable));
        sfence_vma();

        swtch(&c->context, &p->context);

        kvminithart();// 必须加这一行,不然usertest会非常慢
/***其实就是对应下面注释报错的两行,但是kernel_pagetable在当前c文件没有,所以报错了
kvminithart()
{
  w_satp(MAKE_SATP(kernel_pagetable));
  sfence_vma();
}**/


        // Process is done running for now.
        // It should have changed its p->state before coming back.
        c->proc = 0;

        found = 1;
      }
      release(&p->lock);
    }
#if !defined (LAB_FS)
    if(found == 0) {
      intr_on();
      //w_satp(MAKE_SATP(kernel_pagetable));
      //sfence_vma(); 这两行会报错
      asm volatile("wfi");
    }

9.修改kernel/proc.c的freeproc

if(p->pagetable)
    proc_freepagetable(p->pagetable, p->sz);
  if(p->kernel_pagetable)
    proc_kernel_freepagetable(p->kernel_pagetable, p->kstack);
  p->pagetable = 0;
  p->kernel_pagetable = 0;

4.x都看完了

5.3 gdb 没看,后面5.x都没看

Simplify copyin/copyinstr 

1.在defs.h里面添加copyin_new和copyinstr_new声明

int             copyin_new(pagetable_t, char *, uint64, uint64);
int             copyinstr_new(pagetable_t, char *, uint64, uint64);

2.将vm.c里面的copyin转换成copyin_new

return copyin_new(pagetable, dst, srcva, len);

return copyinstr_new(pagetable, dst, srcva, len);

4. 在vm.c的kvminit0里面注释掉uvmmap,


//  uvmmap(kpt, CLINT, CLINT, 0x10000, PTE_R | PTE_W);

4.修改kernel/proc.c的fork函数

然后其实就是把用户空间的页表,复制到内核空间的页表中去,不过有一点我不明白,为什么在用户空间就是虚拟地址,而复制到内核空间,就直接是物理地址了?这搞毛啊, 这个原因就是,因为用户地址空间比较小,而对于地址小于PHYSTOP的虚拟地址,其虚拟地址与物理地址的值是一样的

"这意味着左侧低于PHYSTOP的虚拟地址,与右侧使用的物理地址是一样的。"

还有PLIC,这个到底是用来干嘛的?  终端控制器,不能被用户地址空间覆盖,但是为啥CLINT就能被覆盖??? 嗯 我的理解是,全局内核空间表需要这个,但是各个进程的内核空间表就不需要这个映射了 "因为要保证每个进程的内核页表中低于PLIC的地址,作为用户空间地址使用,而CLINT的地址小于PLIC地址,故每个进程的内核页表不能映射CLINT。"

pte_t *pte, *kernel_pte;

  // Allocate process.
  if((np = allocproc()) == 0){
    return -1;
  }

  // Copy user memory from parent to child.
  if(uvmcopy(p->pagetable, np->pagetable, p->sz) < 0){
    freeproc(np);
    release(&np->lock);
    return -1;
  }

  //这一行就是复制进程的pagetable到内核pagetable,并且置位PTU_U
  //不过我有个疑问,为啥父进程的kernel pagetable就不需要复制? 
  //因为之前已经复制了?那么第一个进程肯定要复制进程的table到kernel pagetable对吧
  for (int j = 0; j < p->sz; j += PGSIZE) {
    pte =  walk(np->pagetable, j, 0); // 遍历p的页表,得到pte
    kernel_pte = walk(np->kernel_pagetable, j, 1); // 遍历kernel页表,如果没有该页,则分配一页
    *kernel_pte = (*pte) & ~PTE_U; // 内核必须设置~PTE_U, 不然内核无法使用
  }

先看看walk代码,到底啥意思:

// Return the address of the PTE in page table pagetable
// that corresponds to virtual address va.  If alloc!=0,
// create any required page-table pages.
//
// The risc-v Sv39 scheme has three levels of page-table
// pages. A page-table page contains 512 64-bit PTEs.
// A 64-bit virtual address is split into five fields:
//   39..63 -- must be zero.
//   30..38 -- 9 bits of level-2 index.
//   21..29 -- 9 bits of level-1 index.
//   12..20 -- 9 bits of level-0 index.
//    0..11 -- 12 bits of byte offset within the page.
pte_t *
walk(pagetable_t pagetable, uint64 va, int alloc) //walk应该就是遍历的意思
{
  if(va >= MAXVA)
    panic("walk");

  for(int level = 2; level > 0; level--) {
    pte_t *pte = &pagetable[PX(level, va)];
    if(*pte & PTE_V) { //如果PTE是valid有效的,那么把pte对应的物理页表赋给pagetable
      pagetable = (pagetable_t)PTE2PA(*pte);
    } else { // 如果没有找到有效页表
      if(!alloc || (pagetable = (pde_t*)kalloc()) == 0) //并且调用我们的方法没有让我们分配,或者分配的时候内存不够,导致kalloc失败,直接返回
        return 0;
      memset(pagetable, 0, PGSIZE); // 对新分配的页表全部置0
      *pte = PA2PTE(pagetable) | PTE_V; //将这个页表添加到PTE,置为valid
    }
  }
  return &pagetable[PX(0, va)]; // 返回物理页表对应的地址,PX到底是个啥,应该是放到第几级页表,va不是虚拟地址,我觉得是偏移量,相对于pagetable的偏移量,因为有可能传va=0进来,所以这个肯定是偏移量
}

再看看walkaddr, 其实就是通过虚拟地址va返回物理地址pa

// Look up a virtual address, return the physical address,
// or 0 if not mapped.
// Can only be used to look up user pages.
uint64
walkaddr(pagetable_t pagetable, uint64 va)
{
  pte_t *pte;
  uint64 pa;

  if(va >= MAXVA)
    return 0;

  pte = walk(pagetable, va, 0);
  if(pte == 0)
    return 0;
  if((*pte & PTE_V) == 0)
    return 0;
  if((*pte & PTE_U) == 0)
    return 0;
  pa = PTE2PA(*pte);
  return pa;
}

5.修改exec.c的文件,添加对sz不能超过PLIC的判断:

for(i=0, off=elf.phoff; i= PLIC) {
      goto bad;
    }

并且释放kenerl pagetable,复制新的user pagetable到kenel pagetable:

uvmunmap(p->kernel_pagetable, 0 , PGROUNDUP(oldsz)/PGSIZE, 0);
//proc_kernel_freepagetable(p->kernel_pagetable, p->kstack, oldsz);
  for (int j = 0; j < sz; j += PGSIZE) {
      pte =  walk(pagetable, j, 0);
      kernel_pte = walk(p->kernel_pagetable, j, 1);
      *kernel_pte = (*pte) & ~PTE_U;
  }

6. 修改srbk代码:

uint64
sys_sbrk(void)
{
  int addr;
  int n;
  struct proc * p = myproc();
  pte_t *pte, *kernel_pte;

  if(argint(0, &n) < 0)
    return -1;
  addr = myproc()->sz;

  if(addr + n >= PLIC) {
    return -1;
  }
  if(growproc(n) < 0)
    return -1;

  if(n > 0) {
    for (int j = addr; j < addr + n; j += PGSIZE) {
      pte =  walk(p->pagetable, j, 0);
      kernel_pte = walk(p->kernel_pagetable, j, 1);
      *kernel_pte = (*pte) & ~PTE_U;
  } else {
    for (j = addr - PGSIZE; j >= addr + n, j -= PGSIZE) {
      uvmunmap(p->kernel_pagetable, j, 1, 0);
    }
 
    //uvmdealloc(p->kernel_pagetable, addr, addr + n); //这一行可能有错误
  }
  }
  return addr;
}

//测试不过记得改掉uvmdealloc那一行

修改proc_kernel_freepagetable文件

void
proc_kernel_freepagetable(pagetable_t pagetable, uint64 kstack, uint64 sz)
{
  // uart registers
  uvmunmap(pagetable, UART0, 1, 0);

  // virtio mmio disk interface
  uvmunmap(pagetable, VIRTIO0, 1, 0);

  // CLINT
  //uvmunmap(pagetable, CLINT, 0x10000/PGSIZE, 0);

  // PLIC
  uvmunmap(pagetable, PLIC, 0x400000/PGSIZE, 0);

  // map kernel text executable and read-only.
  uvmunmap(pagetable, KERNBASE, ((uint64)etext-KERNBASE)/PGSIZE, 0);

  // map kernel data and the physical RAM we'll make use of.
  uvmunmap(pagetable, (uint64)etext, (PHYSTOP-(uint64)etext)/PGSIZE, 0);


  uvmunmap(pagetable, TRAMPOLINE, 1, 0);

  uvmunmap(pagetable, 0, PGROUNDUP(sz)/PGSIZE, 0);
 // uvmunmap(pagetable, TRAPFRAME, 1, 0);
  uvmfree0(pagetable, kstack, 1);
}

在这些都搞完以后,启动的时候报错 kerneltrap, scause是000d,查了8.1对应的trap表,发现是load page fault,页错误

MIT6.S081 Lab3 Page tables_第2张图片

然后我想了下,之前自己写博客的时候也记录了一下"fork"的时候,有注释"第一个进程一定要把pagetable复制到kernel pagetable",而userinit就是第一个进程,所以我们应该在这个进程把pagetable复制到kernel pagetable,而且lab3的问题也有提醒我们

7.修改userinit代码如下:

pte_t *pte, *kernel_pte;
  p = allocproc();
  initproc = p;

  // allocate one user page and copy init's instructions
  // and data into it.
  uvminit(p->pagetable, initcode, sizeof(initcode));
  p->sz = PGSIZE;

  for (int j = 0; j < p->sz; j += PGSIZE) {
    pte =  walk(p->pagetable, j, 0); // 遍历p的页表,得到pte
    kernel_pte = walk(p->kernel_pagetable, j, 1); // 遍历kernel页表,如果没有该页,则分配一页
    *kernel_pte = (*pte) & ~PTE_U; // 内核必须设置~PTE_U, 不然内核无法使用
  }

结果如下:

MIT6.S081 Lab3 Page tables_第3张图片

总结:

1. 解决问题的时候,边写博客记录下自己做了什么,是非常有必要的,因为这样会让你觉得做事很有条理,而且记录下来以后,对之前做了什么印象更深,那么排查问题的时候,更容易回想起之前的坑

2. 要解决问题,必须先明白问题是什么,其实我一开始就看了scause是000d,但误以为d代表的是12,Instruction page fault,后面仔细想了想是d =13. 然后才解决了kerneltrap的问题

3. 还有就是学习方法的问题,我是先学了trap那一章节,然后边写的Lab3,如何看scause寄存器的值,也是从trap那一章,才知道怎么看的,所以我觉得,学习的时候,不懂的直接跳过,多往后看,视野开阔了,回头看自然懂了(如果我只盯着pagetable章节,那看到scause就不知道是啥了)

4. 解决一个问题的办法其实会有很多种,不要陷在一个坑里出不来,就是比如上面scause的问题,一开始我本来打算通过gdb调试,去定位问题,这就不得不下载riscv64-elf-gdb并安装,还要配置,这就很麻烦了,但是后面仔细看了看scause报错原因,回想了下自己写的博客,就猜测是第一个进程的页表没有正确复制到内核页表,所以解决问题要多想想不同的办法

你可能感兴趣的:(网络编程和多线程,linux)