6.S081学习记录-lab3

一、Print a page table

编写一个打印页表内容的函数。

xv6采用三级页表,将三级页表展开可以得到一个页表树,因此可以通过深度优先遍历这个页表树来打印出整个页表结构。

// 递归函数处理页表树,level表示层数
void vmprintrun(pagetable_t pagetable, int level)
{
  // there are 2^9 = 512 PTEs in a page table.
  for(int i = 0; i < 512; i++){
    pte_t pte = pagetable[i];
    // 只处理有效PTE
    if(pte & PTE_V){
      // 打印前置..
      for(int j = 0; j <= level - 1; ++j)
        printf(".. ");
      printf("..");
      printf("%d: pte %p pa %p\n", i, pte, PTE2PA(pte));
      
      // 遍历下一级页表
      if(level < 2){
        uint64 child = PTE2PA(pte);
        vmprintrun((pagetable_t)child, level + 1);
      }
    }
  }
}

void vmprint(pagetable_t pagetable)
{
  // 显示pagetable的参数
  printf("page table %p\n", pagetable);
  vmprintrun(pagetable, 0);
}

在exec.c中的return argc之前插入if(p->pid==1)  vmprint(p->pagetable),以打印第一个进程的页表。

现在编译运行xv6会打印如下输出来描述第一个进程的页表:

page table 0x0000000087f6e000
..0: pte 0x0000000021fda801 pa 0x0000000087f6a000
.. ..0: pte 0x0000000021fda401 pa 0x0000000087f69000
.. .. ..0: pte 0x0000000021fdac1f pa 0x0000000087f6b000
.. .. ..1: pte 0x0000000021fda00f pa 0x0000000087f68000
.. .. ..2: pte 0x0000000021fd9c1f pa 0x0000000087f67000
..255: pte 0x0000000021fdb401 pa 0x0000000087f6d000
.. ..511: pte 0x0000000021fdb001 pa 0x0000000087f6c000
.. .. ..510: pte 0x0000000021fdd807 pa 0x0000000087f76000
.. .. ..511: pte 0x0000000020001c0b pa 0x0000000080007000

二、A kernel page table per process

xv6为每个进程维护一个页表,用来描述进程的用户地址空间,同时整个xv6操作系统维护一个内核页表。当CPU进入内核态时,使用内核页表完成虚拟地址到物理地址的映射。如果内核需要使用在系统调用中传递的用户指针(例如,传递给write()的缓冲区指针)时,内核必须首先将指针指向的虚拟地址转换为物理地址。

本节和下一节的任务就是解决上述问题,使得内核可以直接解引用用户指针。

本节任务是修改内核,给每一个进程生成一个自己的内核页表,让每一个进程在内核中执行时使用它自己的内核页表的副本,每个进程的内核页表都应当与现有的的全局内核页表完全一致。(本节中的内核页表内容是暂时的,到下一节还会添加用户空间的映射到内核页表中,使内核可以解引用用户指针)

1、在struct proc(kernel/proc.h)中新增内核页表字段。

// Per-process state
struct proc {
  // ..
  pagetable_t kernel_pagetable;// proc's kernel pagetable  
};

2、仿造kvminit函数(kernel/vm.c)初始化内核页表

调用mappages函数(kernel/vm.c)添加跟全局内核页表一样的va->pa映射。

pagetable_t
proc_kvminit(struct proc *p)
{
  // 分配一页内存来存储进程p的内核页表
  pagetable_t p_kernel_pagetable = (pagetable_t) kalloc();
  if(p_kernel_pagetable == 0)
    return 0;
  memset(p_kernel_pagetable, 0, PGSIZE);

  // uart registers
  if(mappages(p_kernel_pagetable, UART0, PGSIZE, UART0, PTE_R | PTE_W) != 0)
    panic("proc_kvminit");

  // virtio mmio disk interface
  if(mappages(p_kernel_pagetable, VIRTIO0, PGSIZE, VIRTIO0, PTE_R | PTE_W) != 0)
    panic("proc_kvminit");

  // CLINT
  if(mappages(p_kernel_pagetable, CLINT, 0x10000, CLINT, PTE_R | PTE_W) != 0)
    panic("proc_kvminit");

  // PLIC
  if(mappages(p_kernel_pagetable, PLIC, 0x400000, PLIC, PTE_R | PTE_W) != 0)
    panic("proc_kvminit");

  // map kernel text executable and read-only.
  if(mappages(p_kernel_pagetable, KERNBASE, (uint64)etext-KERNBASE, KERNBASE, PTE_R | PTE_X) != 0)
    panic("proc_kvminit");

  // map kernel data and the physical RAM we'll make use of.
  if(mappages(p_kernel_pagetable, (uint64)etext, PHYSTOP-(uint64)etext, (uint64)etext, PTE_R | PTE_W) != 0)
    panic("proc_kvminit");

  // map the trampoline for trap entry/exit to
  // the highest virtual address in the kernel.
  if(mappages(p_kernel_pagetable, TRAMPOLINE, PGSIZE, (uint64)trampoline, PTE_R | PTE_X) != 0)
    panic("proc_kvminit");
  return p_kernel_pagetable;
}

3、使每一个进程的内核页表中都一个该进程的内核栈映射。

将procinit函数中映射内核栈的代码迁移到allocproc函数(kernel/proc.h)中

在allocproc函数中生成进程的内核页表,然后添加关于该进程对应的内核栈映射。

found:
  p->pid = allocpid();

  // Allocate a trapframe page.
  if((p->trapframe = (struct trapframe *)kalloc()) == 0){
    release(&p->lock);
    return 0;
  }

  // An empty user page table.
  p->pagetable = proc_pagetable(p);
  if(p->pagetable == 0){
    freeproc(p);
    release(&p->lock);
    return 0;
  }

  p->kernel_pagetable = proc_kvminit(p);
  if(p->kernel_pagetable == 0){
    freeproc(p);
    release(&p->lock);
    return 0;
  }
  // 设置该进程对应的内核栈,此处代码从procinit中迁移过来
  // 将procinit中这些代码注释
  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;
  
  // Set up new context to start executing at forkret,
  // which returns to user space.
  memset(&p->context, 0, sizeof(p->context));
  p->context.ra = (uint64)forkret;
  p->context.sp = p->kstack + PGSIZE;

  return p;

4、修改scheduler函数(kernel/proc.c)加载进程的内核页表到核心的satp寄存器。

for(;;){
    // Avoid deadlock by ensuring that devices can interrupt.
    intr_on();
    
    int found = 0;
    for(p = proc; p < &proc[NPROC]; p++) {
      acquire(&p->lock);
      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);

        // 进程运行完后切换到全局kernel_pagetable
        kvminithart();

        // 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) {
      // 没有进程运行时使用全局kernel_pagetable
      kvminithart();
      intr_on();
      asm volatile("wfi");
    }

kvmpa函数(kernel/vm.c)完成内核虚拟地址到物理地址的转换,因此需要更改其中用到的内核页表。

// vm.c中加上2行头文件
#include "spinlock.h"
#include "proc.h"

uint64
kvmpa(uint64 va)
{
  uint64 off = va % PGSIZE;
  pte_t *pte;
  uint64 pa;
  
  // 内核页表设置为当前进程的内核页表
  pte = walk(myproc()->kernel_pagetable, va, 0);
  if(pte == 0)
    panic("kvmpa");
  if((*pte & PTE_V) == 0)
    panic("kvmpa");
  pa = PTE2PA(*pte);
  return pa+off;
}

5、释放进程的内核页表。

freeproc函数(kernel/proc.c)中添加如下代码:

if(p->kernel_pagetable)
    proc_free_kernel_pagetable(p->kernel_pagetable, p->kstack);
  p -> kernel_pagetable = 0;

proc_free_kernel_pagetable函数(kernel/proc.c)调用uvmunmap函数(kernel/vm.c)解除映射关系,调用uvmfree函数(kernel/vm.c)释放内核页表所占物理内存。

void
proc_free_kernel_pagetable(pagetable_t pagetable, uint64 kstack)
{
  uvmunmap(pagetable, UART0, 1, 0);

  uvmunmap(pagetable, VIRTIO0, 1, 0);

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

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

  uvmunmap(pagetable, KERNBASE, ((uint64)etext-KERNBASE) / PGSIZE, 0);

  uvmunmap(pagetable, (uint64)etext, (PHYSTOP-(uint64)etext) / PGSIZE, 0);

  uvmunmap(pagetable, TRAMPOLINE, 1, 0);

  // 该进程私有的内核栈释放物理内存
  uvmunmap(pagetable, kstack, 1, 1);

  // 释放页表所占物理内存
  uvmfree(pagetable, 0);
}

三、Simplify copyin/copyinstr

本节实验内容是继续上一节的工作,将用户空间的映射添加到每个进程的内核页表中,使得内核可以完成对用户虚拟地址的映射,允许copyin/copyinstr函数可以直接对用户指针解引用。

1、将copyin/copyinstr函数主体替换成对copyin_new和copyinstr_new的调用

copyin_new和copyinstr_new函数已经给出,位于kernel/vmcopyin.c中,将2个函数的声明添加到kernel/defs.h中,以便后面调用。

//vmcopyin.c
int             copyin_new(pagetable_t, char *, uint64, uint64);
int             copyinstr_new(pagetable_t, char *, uint64, uint64);

2、实现一个函数,将进程的用户页表项复制到内核页表中

调用walk函数(kernel/vm.c)找到所有用户页表项,再次调用walk函数在内核页表上分配相同的pte。(注意需要去除PTE_U标志,因为在内核模式下,无法访问设置了PTE_U的页面)

// 将每个进程的用户地址映射添加到内核页表中
void
uptbl_to_kptbl(pagetable_t kernel_pagetable, pagetable_t user_pagetable, uint64 srcva, uint64 sz)
{
  srcva = PGROUNDUP(srcva);
  for(uint64 i = srcva; i < srcva + sz; i += PGSIZE)
  {
    pte_t* user_pte = walk(user_pagetable, i, 0);       // 找到i对应的pte
    pte_t* kernel_pte = walk(kernel_pagetable, i, 1);   // 为i分配pte
    if(!user_pte || !kernel_pte)
      panic("uptbl_to_kptbl");
    *kernel_pte = (*user_pte) & (~PTE_U);                 // 去除PTE_U标志
  }
}

userinit函数(kernel/proc.c)中调用uptbl_to_kptbl函数,为第一个进程的内核页表添加用户空间的映射。

p->sz = PGSIZE;

uptbl_to_kptbl(p->kernel_pagetable, p->pagetable, 0, p->sz);

// prepare for the very first "return" from kernel to user.
p->trapframe->epc = 0;      // user program counter
p->trapframe->sp = PGSIZE;  // user stack pointer

3、修改内核页表初始化和释放函数

在xv6中用户进程的最大大小限制为小于内核的最低虚拟地址。内核启动后,在XV6中该地址是0x0C000000,即PLIC寄存器的地址。由于CLINT的地址低于0x0C000000,因此初始化内核页表时不能添加CLINT的映射。将proc_kvminit函数(kernel/vm.c)这部分代码注释。

// virtio mmio disk interface
if(mappages(p_kernel_pagetable, VIRTIO0, PGSIZE, VIRTIO0, PTE_R | PTE_W) != 0)
  panic("proc_kvminit");
  
// 用户进程的最大大小限制为小于内核的最低虚拟地址。内核启动后,在XV6中该地址是0xC000000,即PLIC寄存器的地址
// CLINT
// if(mappages(p_kernel_pagetable, CLINT, 0x10000, CLINT, PTE_R | PTE_W) != 0)
//  panic("proc_kvminit");
 
// PLIC
if(mappages(p_kernel_pagetable, PLIC, 0x400000, PLIC, PTE_R | PTE_W) != 0)
  panic("proc_kvminit");

同时proc_free_kernel_pagetable函数(kernel/proc.c)中也要做相应修改,由于内核页表中加入了用户空间的映射,因此释放内核页表时也要解除这一部分映射。

void
proc_free_kernel_pagetable(pagetable_t pagetable, uint64 kstack, uint64 sz)
{
  uvmunmap(pagetable, UART0, 1, 0);
 
  uvmunmap(pagetable, VIRTIO0, 1, 0);
 
  // uvmunmap(pagetable, CLINT, 0x10000 / PGSIZE, 0);
 
  uvmunmap(pagetable, PLIC, 0x400000 / PGSIZE, 0);
 
  uvmunmap(pagetable, KERNBASE, ((uint64)etext-KERNBASE) / PGSIZE, 0);
 
  uvmunmap(pagetable, (uint64)etext, (PHYSTOP-(uint64)etext) / PGSIZE, 0);

  uvmunmap(pagetable, TRAMPOLINE, 1, 0);

  // 该进程私有的内核栈释放物理内存
  uvmunmap(pagetable, kstack, 1, 1);

  // 去除内核页表上所有关于用户空间的映射
  uvmunmap(pagetable, 0, PGROUNDUP(sz)/PGSIZE, 0);

  // 释放页表所占物理内存
  uvmfree(pagetable, 0);
}

4、修改fork()、exec()和sbrk()

fork函数(kernel/proc.c):

np->sz = p->sz;

// 调用uptbl_to_kptbl函数,复制用户页表pte
uptbl_to_kptbl(np->kernel_pagetable, np->pagetable, 0, np->sz);

np->parent = p;

exec函数(kernel/exec.c):

if(copyout(pagetable, sp, (char *)ustack, (argc+1)*sizeof(uint64)) < 0)
    goto bad;

// 释放旧的内核页表pte
uvmunmap(p->kernel_pagetable,0,PGROUNDUP(oldsz)/PGSIZE,0);
uptbl_to_kptbl(p->kernel_pagetable, pagetable, 0, sz);

// arguments to user main(argc, argv)
// argc is returned via the system call return
// value, which goes in a0.
p->trapframe->a1 = sp;

sbrk函数(kernel/sysproc.c):

由于sbrk设计到用户虚拟地址空间的扩大和缩小,因此就存在pte的新增和释放。空间扩大时,应该添加对应的pte;空间缩小时,应该释放对应的pte。同时空间扩大时注意用户进程不能超过PLIC这一限制。

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

  if(argint(0, &n) < 0)
    return -1;

  // 用户进程虚拟地址不能超过PLIC
  if(PGROUNDUP(p->sz + n) >= PLIC) 
    return -1;
  
  addr = p->sz;
  if(growproc(n) < 0)
    return -1;
  // n大于0时需要添加映射
  if(n > 0){
    uptbl_to_kptbl(p->kernel_pagetable, p->pagetable, addr, n);
  }
  else{
    // 释放对应pte
    // [addr + n,addr)
    for(uint64 i = PGROUNDUP(addr + n); i < PGROUNDUP(addr); i += PGSIZE){
      uvmunmap(p->kernel_pagetable, i, 1, 0);
    }
  }
  return addr;
}

代码写完后,运行make clean,然后make grade。(注意answers-pgtbl.txt需要自己添加,不然通不过,虽然我也不知道answers-pgtbl.txt内容应该是什么。。)

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值