MIT 6.S081 lab3-pgtbl

实验一  Print a page table (easy)

实验一其实比较简单,就是根据输出的格式要去打印pagetable中有效的页表项,一个dfs就可以解决了。

根据提示,首先在kernel / exec.c文件中添加对页表打印mprint方法的调用。

int
exec(char *path, char **argv)
{
  // ...
  if(p->pid==1) 
    vmprint(p->pagetable);
  return argc; // this ends up in a0, the first argument to main(argc, argv)
}

然后在kernel / vm.c中添加vmprint方法

void dfs(pagetable_t pagetable, int level)
{
  if(level < 0)
    return;

  pte_t pte;
  for(int i = 0;i < 512; i ++){
    if(pagetable[i] & PTE_V){
      for(int j = 2; j >= level; j --){
        if(j != 2) printf(" ");
        printf("..");
      }

      pte = PTE2PA(pagetable[i]);
      printf("%d: pte %p pa %p\n",i,pagetable[i],pte);
      dfs((pagetable_t) pte, level - 1);
    }
  }
}

void
vmprint(pagetable_t pagetable){
  printf("page table %p\n",pagetable);
  dfs(pagetable, 2);
}

最后在kernel / defs.h中添加vmprint的声明

void            vmprint(pagetable_t);

实验二  A kernel page table per process (hard)

实验二是为了给每个进程添加一个内核页表,其实一开始在写这个实验的时候一直没有理解为什么要这样,后来到实验三才明白,实验一和实验二都是在为实验三铺路。

首先为了给每个进程添加一个单独的内核页表,在proc结构体中添加kernel_pagetable属性

struct proc {
  // ... 
  pagetable_t pagetable;       // User page table
  pagetable_t kernel_pagetable;
  // ...             
};

在kernel / vm.c 中仿照kvminit()函数添加一个创建内核页表的函数,并在defs.h中添加函数声明(PS:这里没有映射CLINT暂时未知,根据实验三的提示,内核态的虚拟地址是从PLIC开始的,没有映射CLINT)

// vm.c
pagetable_t
create_kpagetable()
{
  pagetable_t pagetable = (pagetable_t) kalloc();
  memset(pagetable, 0, PGSIZE);

  // uart registers
  mappages(pagetable, UART0, PGSIZE, UART0, PTE_R | PTE_W);

  // virtio mmio disk interface
  mappages(pagetable, VIRTIO0, PGSIZE, VIRTIO0, PTE_R | PTE_W);

  // PLIC
  mappages(pagetable,PLIC, 0x400000, PLIC, PTE_R | PTE_W);

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

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

  // map the trampoline for trap entry/exit to
  // the highest virtual address in the kernel.
  mappages(pagetable,TRAMPOLINE, PGSIZE, (uint64)trampoline, PTE_R | PTE_X);
  return pagetable;
}
// defs.h
pagetable_t     create_kpagetable(void);

然后在allocproc函数中,该函数是初始化所有进程的函数,所以在函数需要添加对每个进程内核页表的初始化,即对create_kpagetable调用,另外每个进程的内核栈也改为在allocproc函数中实现。

static struct proc*
allocproc(void)
{
  // ...
  p->pagetable = proc_pagetable(p);
  if(p->pagetable == 0){
    freeproc(p);
    release(&p->lock);
    return 0;
  }

  p->kernel_pagetable = create_kpagetable();
  if(p->pagetable == 0){
    freeproc(p);
    release(&p->lock);
    return 0;
  }

  char *pa = kalloc();
  if(pa == 0)
    panic("kalloc");
  uint64 va = KSTACK((int) (p - proc));
  mappages(p->kernel_pagetable, va, PGSIZE, (uint64)pa, PTE_R | PTE_W);
  p->kstack = va;
  
  memset(&p->context, 0, sizeof(p->context));
  p->context.ra = (uint64)forkret;
  p->context.sp = p->kstack + PGSIZE;

  return p;
}
void
procinit(void)
{
  struct proc *p;

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

然后在进程切换时,同时更改satp寄存器中的值,即每个进程的内核页表地址,根据提示,没有进程运行时scheduler()应当使用kernel_pagetable

void
scheduler(void)
{
  // ... 
  p->state = RUNNING;
  c->proc = p;
  w_satp(MAKE_SATP(p->kernel_pagetable));
  sfence_vma();
  swtch(&c->context, &p->context);

  kvminithart();
  
  // ...
}

最后在释放进程时,需要同时释放内核页表和内核栈

// defs.h
void            freewalk(pagetable_t pagetable);

// vm.c
static void
freeproc(struct proc *p)
{
  if(p->trapframe)
    kfree((void*)p->trapframe);
  p->trapframe = 0;
  if(p->pagetable)
    proc_freepagetable(p->pagetable, p->sz);
  if(p->kstack)
    uvmunmap(p->kernel_pagetable, p->kstack, 1, 1);
  if(p->kernel_pagetable)
    proc_freekpagetable(p->kernel_pagetable, p->sz);

  p->kstack = 0;
  p->kernel_pagetable=0;
  // ...
}

void
proc_freekpagetable(pagetable_t pagetable, uint64 sz)
{
  uvmunmap(pagetable, UART0, 1, 0);
  uvmunmap(pagetable, VIRTIO0, 1, 0);
  uvmunmap(pagetable, PLIC, PGROUNDUP(0x400000)/PGSIZE, 0);
  uvmunmap(pagetable, KERNBASE, PGROUNDUP((uint64)etext-KERNBASE)/PGSIZE, 0);
  uvmunmap(pagetable, (uint64)etext, PGROUNDUP(PHYSTOP-(uint64)etext)/PGSIZE, 0);
  uvmunmap(pagetable, TRAMPOLINE, 1, 0);
  //uvmunmap(pagetable, 0, PGROUNDUP(sz) / PGSIZE, 0);
  freewalk(pagetable);
}

实验三 Simplify copyin/copyinstr(hard)

其实实验三才是本次实验的想要添加的功能,因为内核空间的虚拟地址和用户空间的虚拟地址不在同一个页表中,所以用户空间的虚拟地址,在内核空间下是无法直接使用的,必须先根据用户页表转化为物理地址,因此我们需要在内核态将用户态的虚拟地址空间在内核页表中也做映射,这样我们就可以在内核态可以直接访问用户态的地址。

首先在vm.c中添加用户态虚拟地址映射复制到内核页表的函数,同时在defs.h中添加函数的声明

// defs.h 
int             kvmcopy(pagetable_t, pagetable_t, uint64, uint64);

// vm.c
int
kvmcopy(pagetable_t user,pagetable_t kernel, uint64 oldsz, uint64 newsz)
{
  pte_t *from, *to;
  if(newsz > PLIC)
    return -1;

  oldsz = PGROUNDUP(oldsz);
  for(uint64 i = oldsz; i < newsz; i += PGSIZE){
    from = walk(user, i, 0);
    if(!(*from & PTE_V))
      panic("kvmcopy: from pte is not valid");

    to = walk(kernel, i, 1);

    *to = *from & (~PTE_U);
  }
  return 0;
}

同时添加取消映射的函数,这在之后的步骤中会用到

// defs.h
uint64            kvmdealloc(pagetable_t, uint64, uint64)

// vm.c
uint64
kvmdealloc(pagetable_t pagetable, uint64 oldsz, uint64 newsz)
{
  if(newsz >= oldsz)
    return oldsz;

  if(PGROUNDUP(newsz) < PGROUNDUP(oldsz)){
    int npages = (PGROUNDUP(oldsz) - PGROUNDUP(newsz)) / PGSIZE;
    uvmunmap(pagetable, PGROUNDUP(newsz), npages, 0);
  }

  return newsz;
}

根据提示,只有在exec(),fork(),sbrk()三个函数中,以及第一次创建的进程userinit中,在内核页表中添加用户页表项的映射

  1. exec

该方法是用户态进程调用exec系统调用执行特定程序的方法,需要注意的是这里需要释放原内核页表中用户页表映射表项

// exec.c

int
exec(char *path, char **argv)
{
  // ...
  safestrcpy(p->name, last, sizeof(p->name));

    //unmap previous user mapping in kernel pagetable
  uvmunmap(p->kernel_pagetable, 0, PGROUNDUP(p->sz) / PGSIZE, 0);

  //add user mapping into kernel pagetable
  if(kvmcopy(pagetable, p->kernel_pagetable, 0, sz) < 0)
    goto bad;

  // Commit to the user image.
  oldpagetable = p->pagetable;
  // ...
}

        2. fork

// proc.c

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

          3. sbrk

sys_sbrk本质上执行的是 proc.c / growproc函数

int
growproc(int n)
{
  uint sz;
  struct proc *p = myproc();

  sz = p->sz;
  if(n > 0){
    if((sz = uvmalloc(p->pagetable, sz, sz + n)) == 0) {
      return -1;
    }
    if(kvmcopy(p->pagetable, p->kernel_pagetable, p->sz, p->sz + n) < 0) {
      return -1;
    }
  } else if(n < 0){
    sz = uvmdealloc(p->pagetable, sz, sz + n);
    kvmdealloc(p->kernel_pagetable, p->sz, p->sz + n);
  }
  p->sz = sz;
  return 0;
}

最后在释放内核页表时,添加上释放用户页表表项的过程

void
proc_freekpagetable(pagetable_t pagetable, uint64 sz)
{
  uvmunmap(pagetable, UART0, 1, 0);
  uvmunmap(pagetable, VIRTIO0, 1, 0);
  uvmunmap(pagetable, PLIC, PGROUNDUP(0x400000)/PGSIZE, 0);
  uvmunmap(pagetable, KERNBASE, PGROUNDUP((uint64)etext-KERNBASE)/PGSIZE, 0);
  uvmunmap(pagetable, (uint64)etext, PGROUNDUP(PHYSTOP-(uint64)etext)/PGSIZE, 0);
  uvmunmap(pagetable, TRAMPOLINE, 1, 0);
  uvmunmap(pagetable, 0, PGROUNDUP(sz) / PGSIZE, 0);
  freewalk(pagetable);
}

更换copyin和copyinstr为copyin_new和copyinstr_new的调用

int
copyin(pagetable_t pagetable, char *dst, uint64 srcva, uint64 len)
{
  return copyin_new(pagetable,dst,srcva,len);
}

int
copyinstr(pagetable_t pagetable, char *dst, uint64 srcva, uint64 max)
{
  return copyinstr_new(pagetable,dst,srcva,max);
}

总节

在本次实验中踩了不少啃,包括不限于在改变释放进程所占用内存freeproc()函数时,因为入参p不是当前cpu中正在运行的函数,所以在释放内核页表中用户页表部分使用了myproc()->sz,导致没有用户页表部分没有释放全,所以在freewalk中报错;另外实验二一开始没有理解实验的意图,导致去除了全局内核页表而无法触发时钟中断,另外还有一些比较傻的错误。本次实验加深了用户页表和内核页表的理解以及各自地址空间内容的理解,并且在跟代码的过程中,对risc-v的一些非通用寄存器有了些理解。

另外不理解的地方还有 1. 为什么每个进程中的内核页表不需要映射CLINT 2.时钟中断是如何触发的,以及触发后的过程。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值