xv6 risc-v 虚拟内存 code walk through

最近做lazy lab做的有点头大,突然发现是自己遗漏了一些内容,即lec5,6,因为刚开始看的时候很多看不懂,就跑去看书了,后来就一直没有回来看lecture

所以为了提高效率,应该快速的把书看一遍,然后回来看lecture,再带着问题去看书看代码

code walk through

kvminit

void kvminit()
{
  // 确定64bit的一级目录地址
  kernel_pagetable = (pagetable_t) kalloc();
  memset(kernel_pagetable, 0, PGSIZE);

  // 一对一的IO映射 1 page
  kvmmap(UART0, UART0, PGSIZE, PTE_R | PTE_W);
  kvmmap(VIRTION(0), VIRTION(0), PGSIZE, PTE_R | PTE_W);
  kvmmap(VIRTION(1), VIRTION(1), PGSIZE, PTE_R | PTE_W);

  // CLINT 16 pages
  kvmmap(CLINT, CLINT, 0x10000, PTE_R | PTE_W);
  // PLIC 1024 pages
  kvmmap(PLIC, PLIC, 0x400000, PTE_R | PTE_W);

  // memlayout.h
  // #define KERNBASE 0x80000000L
  // #define PHYSTOP (KERNBASE + 128*1024*1024)  所以内核共映射了128M的内存
  // #define TRAMPOLINE (MAXVA - PGSIZE)

  // etext 在kernel.ld中,编译器根据代码量确定的对齐地址,ALGIN(0x1000),不懂意思
  kvmmap(KERNBASE, KERNBASE, (uint64)etext-KERNBASE, PTE_R | PTE_X);

  // 映射数据段和空闲内存
  kvmmap((uint64)etext, (uint64)etext, PHYSTOP-(uint64)etext, PTE_R | PTE_W);

  // 只有trampoline不是直接映射,实际的位置已经出现在代码段了,KERNBASE~etext。该段代码二次映射
  kvmmap(TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);
}

kvmmap

直接调用了mappages(pagetable,va,size,pa,permission) , 而mappages是建立va~va+sz的pte:通过kalloc得到物理地址,然后设置pte

int
mappages(pagetable_t pagetable, uint64 va, uint64 size, uint64 pa, int perm)
{
  uint64 a, last;
  pte_t *pte;

  a = PGROUNDDOWN(va);
  last = PGROUNDDOWN(va + size - 1);
  for(;;){
    if((pte = walk(pagetable, a, 1)) == 0)
      return -1;
    if(*pte & PTE_V)
      panic("remap");
    *pte = PA2PTE(pa) | perm | PTE_V;
    if(a == last)
      break;
    a += PGSIZE;
    pa += PGSIZE;
  }
  return 0;
}

walk

其中walk通过虚拟地址,找到对应pte:

static pte_t * walk(pagetable_t pagetable, uint64 va, int alloc)
{
  if(va >= MAXVA)
    panic("walk");

  for(int level = 2; level > 0; level--) {
    pte_t *pte = &pagetable[PX(level, va)];
    if(*pte & PTE_V) {
      pagetable = (pagetable_t)PTE2PA(*pte);
    } else {
      // 分配新的内存给目录,并且初始化目录
      if(!alloc || (pagetable = (pde_t*)kalloc()) == 0)
        return 0;
      memset(pagetable, 0, PGSIZE);
      *pte = PA2PTE(pagetable) | PTE_V;
    }
  }
  return &pagetable[PX(0, va)];
}

模拟下图:

依次取出l2,l2,l0,最终拿到作为leaf(指向的不是页表页)的pte

其中如果某个pte不存在,那么根据alloc是否为0,如果为0,也就是不直接分配一个新页表页,那么直接返回0(代表没有映射),如果为1,那么新建一个页表页(分配物理内存),并初始化它(memset为0),以及修改指向它的pte的内容

procinit

在main.c中调用,为所有的进程分配内核栈以及相应guard page

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

其中的KSTACK是一个macro,对于第一个进程,得到的va是trampoline-2*pgsize,第二个是trampoline-4*pgsize,以此类推,如图

然后再依次将内核栈页建立pte(guard page不建立)

设置用户内存空间

allocproc

首先是找到一个state为unused的进程,然后设置pid,分配trapframe,设置页表,建立trampoline,trapframe的pte

fork

首先通过allocproc分配一个初始进程,然后调用uvmcopy(old pagetable,new pagetable,sz),从parent的用户内存复制到child

剩下的就是设置一些相关状态:

np->sz=p->sz

np->parent=p

另外,fork是怎么实现子进程和父进程返回值不同的呢,xv6的实现是父进程返回子进程的pid,设置子进程np->tf->a0 = 0,使其返回0

因为子进程和父进程有同样的fd,所以也要复制一份(要增加对应文件的ref count),设置子进程状态为runnable

uvmcopy

从0~sz,首先找到该地址对应的pte,然后判断是或valid,如果是,那么拿到对应的物理地址,分配物理内存,用memmove复制对应的物理内存,然后把新申请的物理内存建立pte

  for(i = 0; i < sz; i += PGSIZE){
    if((pte = walk(old, i, 0)) == 0)
      // panic("uvmcopy: pte should exist");
      continue;
    if((*pte & PTE_V) == 0)
      continue;
      // panic("uvmcopy: page not present");
    pa = PTE2PA(*pte);
    flags = PTE_FLAGS(*pte);
    if((mem = kalloc()) == 0)
      goto err;
    memmove(mem, (char*)pa, PGSIZE);
    if(mappages(new, i, PGSIZE, (uint64)mem, flags) != 0){
      kfree(mem);
      goto err;
    }
  }

exec

注意,exec一般是在fork之后运行的,并且是在子进程中运行的,所以myproc()实际上拿到的是子进程

fork之后,子进程的页表内容和父进程是相同的,现在要根据可执行文件的内容,建立一个新的页表,来代替老页表(和父进程相同的)

建立新页表,首先通过proc_pagetable获取一个只映射了trampoline和trapframe的页表,然后对于elf文件,一个个program header的加载进去

加载主要使用了两个函数:uvmmalloc(pagetable,oldsz,newsz),loadseg(pagetable,va,inode *,offset,sz)

uvmmalloc

为oldsz~newsz之间虚拟内存分配物理内存,并且建立pte,newsz不需要对齐(不过每次增长一个页,对不对齐好像也没区别?)

loadseg

static int loadseg(pagetable_t pagetable, uint64 va, struct inode *ip, uint offset, uint sz) {
  // ..省略声明和 va 对齐检查

  for(i = 0; i < sz; i += PGSIZE){
    pa = walkaddr(pagetable, va + i);
    if(sz - i < PGSIZE)
      n = sz - i;
    else
      n = PGSIZE;
    // 一批4096 bytes地读取,最后一批可能不足4096
    if(readi(ip, 0, (uint64)pa, offset+i, n) != n)
      return -1;
  }
  return 0;
}

猜你喜欢

转载自blog.csdn.net/RedemptionC/article/details/107709618