Lab 3

vmprint(easy)

首先看一个 & 运算,&运算是按位运算。freewalk函数里面有这样一个判断,PTE_V = 1L<<0,如果pet与PTE_V进行&运算,PTE_V就会补0 , 0...01&pte,这样只有pte的最后一位为1,&运算才会为1,这样才能通过if判断。也就是说只有设置了有效位的页表项才能通过这个if判断。

if((pte & PTE_V))
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

第一行显示vmprint的参数。之后的每行对应一个PTE,包含树中指向页表页的PTE。每个PTE行都有一些“..”的缩进表明它在树中的深度。每个PTE行显示其在页表页中的PTE索引、PTE比特位以及从PTE提取的物理地址。不要打印无效的PTE。在上面的示例中,顶级页表页具有条目0和255的映射。条目0的下一级只映射了索引0,该索引0的下一级映射了条目0、1和2。

每一个page directory里面的每一项有效的pte都可能指向一个新的page directory,也就是上面的 顶级页表的 0 和 255 指向的page directory可以是不同的。

接下来的事情就很简单了:

// vmprint for lab3 1
void
vmprint(pagetable_t pagetable) {
  printf("page table %p\n",pagetable);
  dovmprint(pagetable,1);
}

void 
dovmprint(pagetable_t pagetable,int level) {
  for(int i = 0;i < 512; i++) {
    pte_t pte = pagetable[i];
    // PTE_V: 1
    if((pte & PTE_V)){
      // switch (level)
      // {
      // case 1:
      //   printf("..");
      //   break;
      // case 2:
      //   printf(".. ..");
      //   break;
      // default:
      //   printf(".. .. ..");
      //   break;
      // }
      for(int j = 1;j <= level;j++) {
        if (j == level) {
          printf("..");
        }else {
          printf(".. ");
        }
      }
      pagetable_t child = (pagetable_t)PTE2PA(pte);
      printf("%d: pte %p pa %p\n",i,pte,child);
      // 只有三级页表,如果再递归就错了。
      if (level < 3) {
        dovmprint(child,level+1);
      }
    }
  }
}
== Test pte printout == 
$ make qemu-gdb
pte printout: OK (3.6s) 

A kernel page table per process (hard)

Xv6有一个单独的用于在内核中执行程序时的内核页表。内核页表直接映射(恒等映射)到物理地址,也就是说内核虚拟地址x映射到物理地址仍然是xXv6还为每个进程的用户地址空间提供了一个单独的页表,只包含该进程用户内存的映射,从虚拟地址0开始。因为内核页表不包含这些映射,所以用户地址在内核中无效。因此,当内核需要使用在系统调用中传递的用户指针(例如,传递给write()的缓冲区指针)时,内核必须首先将指针转换为物理地址。本节和下一节的目标是允许内核直接解引用用户指针。

YOUR JOB

你的第一项工作是修改内核来让每一个进程在内核中执行时使用它自己的内核页表的副本。修改struct proc为每一个进程维护一个内核页表修改调度程序使得切换进程时也切换内核页表。对于这个步骤,每个进程的内核页表都应当与现有的的全局内核页表完全一致。如果你的usertests程序正确运行了,那么你就通过了这个实验。

  • struct proc中为进程的内核页表增加一个字段

  • 为一个新进程生成一个内核页表的合理方案是实现一个修改版的kvminit,这个版本中应当创造一个新的页表而不是修改kernel_pagetable。你将会考虑allocproc中调用这个函数

  • 确保每一个进程的内核页表都关于该进程的内核栈有一个映射。在未修改的XV6中,所有的内核栈都在procinit中设置。你将要把这个功能部分或全部的迁移到allocproc

  • 修改scheduler()来加载进程的内核页表到核心的satp寄存器(参阅kvminithart来获取启发)。不要忘记在调用完w_satp()后调用sfence_vma()

  • 没有进程运行时scheduler()应当使用kernel_pagetable

  • freeproc中释放一个进程的内核页表

  • 你需要一种方法来释放页表,而不必释放叶子物理内存页面。

  • 调式页表时,也许vmprint能派上用场

  • 修改XV6本来的函数或新增函数都是允许的;你或许至少需要在kernel/vm.c*和**kernel/proc.c*中这样做(但不要修改*kernel/vmcopyin.c*, *kernel/stats.c*, *user/usertests.c*, 和*user/stats.c***)

  • 页表映射丢失很可能导致内核遭遇页面错误。这将导致打印一段包含sepc=0x00000000XXXXXXXX的错误提示。你可以在*kernel/kernel.asm*通过查询XXXXXXXX来定位错误。

lab的要求我感觉每句话都要细细推敲,这个实验好难啊,再后面那个也挺难....。

这个实验其实就是要求我们为每一个进程维护一个内核页表,使得在该进程切入内核态时,可以直接使用进程的内核页表,而不用去使用vm.c中公用的内核页表,这样就可以在用户态和内核态中均实现虚拟内存。在源码中对应可参照的就是维护一个用户进程页表和初始的内核页表。

添加proc字段

按Hints第一步要求加上个kernelpt字段即可。

// Per-process state
struct proc {
  struct spinlock lock;

  // p->lock must be held when using these:
  enum procstate state;        // Process state
  struct proc *parent;         // Parent process
  void *chan;                  // If non-zero, sleeping on chan
  int killed;                  // If non-zero, have been killed
  int xstate;                  // Exit status to be returned to parent's wait
  int pid;                     // Process ID

  // these are private to the process, so p->lock need not be held.
  uint64 kstack;               // Virtual address of kernel stack
  uint64 sz;                   // Size of process memory (bytes)
  pagetable_t pagetable;       // User page table
  pagetable_t kernelpt;        // Kernel page table
  struct trapframe *trapframe; // data page for trampoline.S
  struct context context;      // swtch() here to run process
  struct file *ofile[NOFILE];  // Open files
  struct inode *cwd;           // Current directory
  char name[16];               // Process name (debugging)
};

修改版kvminit

Hint2:为一个新进程生成一个内核页表的合理方案是实现一个修改版的kvminit,这个版本中应当创造一个新的页表而不是修改kernel_pagetable

// 为一个新进程生成一个内核页表的合理方案是实现一个修改版的kvminit,这个版本中应当创造一个新的页表而不是修改kernel_pagetable
pagetable_t newkvmpt() {
  pagetable_t kernelpt;
  kernelpt = uvmcreate();
  if (kernelpt == 0) return 0;
  // 下面仿照kvminit写,不过是要为proc的kernel page table进行初始化
  ukvmmap(kernelpt, UART0, UART0, PGSIZE, PTE_R | PTE_W);
  ukvmmap(kernelpt, VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);
  ukvmmap(kernelpt, CLINT, CLINT, 0x10000, PTE_R | PTE_W);
  ukvmmap(kernelpt, PLIC, PLIC, 0x400000, PTE_R | PTE_W);
  ukvmmap(kernelpt, KERNBASE, KERNBASE, (uint64)etext-KERNBASE, PTE_R | PTE_X);
  ukvmmap(kernelpt, (uint64)etext, (uint64)etext, PHYSTOP-(uint64)etext, PTE_R | PTE_W);
  ukvmmap(kernelpt, TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);
  return kernelpt;
}

// add mapping to userproc's kernel pagetable
void ukvmmap(pagetable_t kpt,uint64 va, uint64 pa, uint64 sz, int perm)
{
  if(mappages(kpt, va, sz, pa, perm) != 0)
    panic("uvmmap");
}

allocproc调用newkvmpt并添加映射

Hint2:你将会考虑allocproc中调用这个函数

Hint3:确保每一个进程的内核页表都关于该进程的内核栈有一个映射。在未修改的XV6中,所有的内核栈都在procinit中设置。你将要把这个功能部分或全部的迁移到allocproc

Hint3中提示要把 procinit 的部分功能迁移到allocproc中,其实就是要把添加映射那部分拿过来修改一下。

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

可以看上面procinit()的代码。在三段注释下面的代码中,为每个进程申请一个页大小的栈段kstack,在内存中把它映射到高处,上面是一个守护页,防止栈溢出影响其他进程,在xv6那本书的图里。

static struct proc*
allocproc(void)
{
  struct proc *p;

  for(p = proc; p < &proc[NPROC]; p++) {
    acquire(&p->lock);
    if(p->state == UNUSED) {
      goto found;
    } else {
      release(&p->lock);
    }
  }
  return 0;

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;
  }
  
  // An kernel page table for user proc
  p->kernelpt = newkvmpt();
  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));
    // va -> pa
  ukvmmap(p->kernelpt,va,(uint64)pa,PGSIZE,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;
}

修改scheduler

Hint4:修改scheduler()来加载进程的内核页表到核心的satp寄存器(参阅kvminithart来获取启发)。不要忘记在调用完w_satp()后调用sfence_vma()

需要在切换进程前:加载进程的内核页表到核心的satp寄存器

在进程被切掉后:回归原来的内核状态

void
scheduler(void)
{
  struct proc *p;
  struct cpu *c = mycpu();
  
  c->proc = 0;
  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;
        // 在swtch前将内核页表存入satp寄存器
        w_satp(MAKE_SATP(p->kernelpt));
        sfence_vma();

        swtch(&c->context, &p->context);
          
		// 在进程结束运行后恢复原来的内核页表
        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) {
      intr_on();
      asm volatile("wfi");
    }
#else
    ;
#endif
  }
}

修改freeproc

Hint5: 在freeproc中释放一个进程的内核页表

// free a proc structure and the data hanging from it,
// including user pages.
// p->lock must be held.
static void
freeproc(struct proc *p)
{
  if(p->trapframe)
    kfree((void*)p->trapframe);
  p->trapframe = 0;
  // 如果p->kstack 有映射有内容,就需要释放对应的物理页面,然后清空p->kstack
  if(p->kstack) {
    // 找到最后一级的PTE
    pte_t* pte = walk(p->kernelpt,p->kstack,0);
    if (pte == 0)
      panic("freeproc");
    kfree((void *)PTE2PA(*pte));
  }
  p->kstack = 0;
  if(p->pagetable)
    proc_freepagetable(p->pagetable, p->sz);
  if(p->kernelpt)
    proc_freekernelpt(p->kernelpt);
  p->pagetable = 0;
  p->kernelpt = 0;
  p->sz = 0;
  p->pid = 0;
  p->parent = 0;
  p->name[0] = 0;
  p->chan = 0;
  p->killed = 0;
  p->xstate = 0;
  p->state = UNUSED;
}

// 看一下 PTE2PA的写法:右移10位是把10个标志位去掉,变成44位。再左移12位,也就是添上12位0,就成56位的物理地址。
#define PTE2PA(pte) (((pte) >> 10) << 12)

还有一个就是要释放掉内核页表,需要递归调用清楚三级子页表:

Hint6:你需要一种方法来释放页表,而不必释放叶子物理内存页面。

// 仿造 freewalk()
void proc_freekernelpt(pagetable_t kernelpt) {
  // there are 2^9 = 512 PTEs in a page table.
  for(int i = 0; i < 512; i++){
    pte_t pte = kernelpt[i];
      // 有效才需清除
    if(pte & PTE_V){
      kernelpt[i] = 0;
      if ((pte & (PTE_R|PTE_W|PTE_X)) == 0) {
        // this PTE points to a lower-level page table.
        uint64 child = PTE2PA(pte);
        proc_freekernelpt((pagetable_t)child);
      }
    } else if(pte & PTE_V){
      panic("proc_freekernelpt: leaf");
    }
  }
  kfree((void*)kernelpt);
}

修改kvmpa

最后make qemu的时候报错了... panic:kvmpa

找不到到底哪错了,百度一下果然很多人在这一步卡了,需要修改一下vm.c的kvmpa函数:(提示也没说,太坑了)

// translate a kernel virtual address to
// a physical address. only needed for
// addresses on the stack.
// assumes va is page aligned.
uint64
kvmpa(uint64 va)
{
  uint64 off = va % PGSIZE;
  pte_t *pte;
  uint64 pa;
  pte = walk(myproc()->kernelpt, va, 0);
  if(pte == 0)
    panic("kvmpa");
  if((*pte & PTE_V) == 0)
    panic("kvmpa");
  pa = PTE2PA(*pte);
  return pa+off;
}

final:

$ ./usertests
usertests starting
test execout: OK
test copyin: OK
test copyout: OK
test copyinstr1: OK
test copyinstr2: OK
test copyinstr3: OK
test truncate1: OK
test truncate2: OK
test truncate3: OK
test reparent2: OK
test pgbug: OK
test sbrkbugs: usertrap(): unexpected scause 0x000000000000000c pid=3235
            sepc=0x0000000000005406 stval=0x0000000000005406
usertrap(): unexpected scause 0x000000000000000c pid=3236
            sepc=0x0000000000005406 stval=0x0000000000005406
OK
test badarg: OK
test reparent: OK
test twochildren: OK
test forkfork: OK
test forkforkfork: OK
test argptest: OK
test createdelete: OK
test linkunlink: OK
test linktest: OK
test unlinkread: OK
test concreate: OK
test subdir: OK
test fourfiles: OK
test sharedfd: OK
test exectest: OK
test bigargtest: OK
test bigwrite: OK
test bsstest: OK
test sbrkbasic: OK
test sbrkmuch: OK
test kernmem: usertrap(): unexpected scause 0x000000000000000d pid=6215
            sepc=0x000000000000201a stval=0x0000000080000000
usertrap(): unexpected scause 0x000000000000000d pid=6216
            sepc=0x000000000000201a stval=0x000000008000c350
usertrap(): unexpected scause 0x000000000000000d pid=6217
            sepc=0x000000000000201a stval=0x00000000800186a0
usertrap(): unexpected scause 0x000000000000000d pid=6218
            sepc=0x000000000000201a stval=0x00000000800249f0
usertrap(): unexpected scause 0x000000000000000d pid=6219
            sepc=0x000000000000201a stval=0x0000000080030d40
usertrap(): unexpected scause 0x000000000000000d pid=6220
            sepc=0x000000000000201a stval=0x000000008003d090
usertrap(): unexpected scause 0x000000000000000d pid=6221
            sepc=0x000000000000201a stval=0x00000000800493e0
usertrap(): unexpected scause 0x000000000000000d pid=6222
            sepc=0x000000000000201a stval=0x0000000080055730
usertrap(): unexpected scause 0x000000000000000d pid=6223
            sepc=0x000000000000201a stval=0x0000000080061a80
usertrap(): unexpected scause 0x000000000000000d pid=6224
            sepc=0x000000000000201a stval=0x000000008006ddd0
usertrap(): unexpected scause 0x000000000000000d pid=6225
            sepc=0x000000000000201a stval=0x000000008007a120
usertrap(): unexpected scause 0x000000000000000d pid=6226
            sepc=0x000000000000201a stval=0x0000000080086470
usertrap(): unexpected scause 0x000000000000000d pid=6227
            sepc=0x000000000000201a stval=0x00000000800927c0
usertrap(): unexpected scause 0x000000000000000d pid=6228
            sepc=0x000000000000201a stval=0x000000008009eb10
usertrap(): unexpected scause 0x000000000000000d pid=6229
            sepc=0x000000000000201a stval=0x00000000800aae60
usertrap(): unexpected scause 0x000000000000000d pid=6230
            sepc=0x000000000000201a stval=0x00000000800b71b0
usertrap(): unexpected scause 0x000000000000000d pid=6231
            sepc=0x000000000000201a stval=0x00000000800c3500
usertrap(): unexpected scause 0x000000000000000d pid=6232
            sepc=0x000000000000201a stval=0x00000000800cf850
usertrap(): unexpected scause 0x000000000000000d pid=6233
            sepc=0x000000000000201a stval=0x00000000800dbba0
usertrap(): unexpected scause 0x000000000000000d pid=6234
            sepc=0x000000000000201a stval=0x00000000800e7ef0
usertrap(): unexpected scause 0x000000000000000d pid=6235
            sepc=0x000000000000201a stval=0x00000000800f4240
usertrap(): unexpected scause 0x000000000000000d pid=6236
            sepc=0x000000000000201a stval=0x0000000080100590
usertrap(): unexpected scause 0x000000000000000d pid=6237
            sepc=0x000000000000201a stval=0x000000008010c8e0
usertrap(): unexpected scause 0x000000000000000d pid=6238
            sepc=0x000000000000201a stval=0x0000000080118c30
usertrap(): unexpected scause 0x000000000000000d pid=6239
            sepc=0x000000000000201a stval=0x0000000080124f80
usertrap(): unexpected scause 0x000000000000000d pid=6240
            sepc=0x000000000000201a stval=0x00000000801312d0
usertrap(): unexpected scause 0x000000000000000d pid=6241
            sepc=0x000000000000201a stval=0x000000008013d620
usertrap(): unexpected scause 0x000000000000000d pid=6242
            sepc=0x000000000000201a stval=0x0000000080149970
usertrap(): unexpected scause 0x000000000000000d pid=6243
            sepc=0x000000000000201a stval=0x0000000080155cc0
usertrap(): unexpected scause 0x000000000000000d pid=6244
            sepc=0x000000000000201a stval=0x0000000080162010
usertrap(): unexpected scause 0x000000000000000d pid=6245
            sepc=0x000000000000201a stval=0x000000008016e360
usertrap(): unexpected scause 0x000000000000000d pid=6246
            sepc=0x000000000000201a stval=0x000000008017a6b0
usertrap(): unexpected scause 0x000000000000000d pid=6247
            sepc=0x000000000000201a stval=0x0000000080186a00
usertrap(): unexpected scause 0x000000000000000d pid=6248
            sepc=0x000000000000201a stval=0x0000000080192d50
usertrap(): unexpected scause 0x000000000000000d pid=6249
            sepc=0x000000000000201a stval=0x000000008019f0a0
usertrap(): unexpected scause 0x000000000000000d pid=6250
            sepc=0x000000000000201a stval=0x00000000801ab3f0
usertrap(): unexpected scause 0x000000000000000d pid=6251
            sepc=0x000000000000201a stval=0x00000000801b7740
usertrap(): unexpected scause 0x000000000000000d pid=6252
            sepc=0x000000000000201a stval=0x00000000801c3a90
usertrap(): unexpected scause 0x000000000000000d pid=6253
            sepc=0x000000000000201a stval=0x00000000801cfde0
usertrap(): unexpected scause 0x000000000000000d pid=6254
            sepc=0x000000000000201a stval=0x00000000801dc130
OK
test sbrkfail: usertrap(): unexpected scause 0x000000000000000d pid=6266
            sepc=0x0000000000003e7a stval=0x0000000000012000
OK
test sbrkarg: OK
test validatetest: OK
test stacktest: usertrap(): unexpected scause 0x000000000000000d pid=6270
            sepc=0x0000000000002188 stval=0x000000000000fbc0
OK
test opentest: OK
test writetest: OK
test writebig: OK
test createtest: OK
test openiput: OK
test exitiput: OK
test iput: OK
test mem: OK
test pipe1: OK
test preempt: kill... wait... OK
test exitwait: OK
test rmdot: OK
test fourteen: OK
test bigfile: OK
test dirfile: OK
test iref: OK
test forktest: OK
test bigdir: OK
ALL TESTS PASSED

Simplify copyin/copyinstr(hard)

内核的copyin函数读取用户指针指向的内存。它通过将用户指针转换为内核可以直接解引用的物理地址来实现这一点。这个转换是通过在软件中遍历进程页表来执行的。在本部分的实验中,您的工作是将用户空间的映射添加到每个进程的内核页表(上一节中创建),以允许copyin(和相关的字符串函数copyinstr直接解引用用户指针

YOUR JOB

将定义在kernel/vm.c中的copyin的主题内容替换为对copyin_new的调用(在kernel/vmcopyin.c中定义);对copyinstrcopyinstr_new执行相同的操作。为每个进程的内核页表添加用户地址映射,以便copyin_newcopyinstr_new工作。如果usertests正确运行并且所有make grade测试都通过,那么你就完成了此项作业。

此方案依赖于用户的虚拟地址范围不与内核用于自身指令和数据的虚拟地址范围重叠。Xv6使用从零开始的虚拟地址作为用户地址空间,幸运的是内核的内存从更高的地址开始。然而,这个方案将用户进程的最大大小限制为小于内核的最低虚拟地址。内核启动后,在XV6中该地址是0xC000000,即PLIC寄存器的地址;请参见kernel/vm.c中的kvminit()kernel/memlayout.h和文中的图3-4。您需要修改xv6,以防止用户进程增长到超过PLIC的地址。

Hints:

  • 先用对copyin_new的调用替换copyin(),确保正常工作后再去修改copyinstr

  • 在内核更改进程的用户映射的每一处,都以相同的方式更改进程的内核页表。包括fork(), exec(), 和sbrk().

  • 不要忘记在userinit的内核页表中包含第一个进程的用户页表

  • 用户地址的PTE在进程的内核页表中需要什么权限?(在内核模式下,无法访问设置了PTE_U的页面)

  • 别忘了上面提到的PLIC限制

替换copyin*_new

在vm.c中替换copyin和copyinstr。需要把两个函数定义写到 defs.h

// Copy from user to kernel.
// Copy len bytes to dst from virtual address srcva in a given page table.
// Return 0 on success, -1 on error.
int
copyin(pagetable_t pagetable, char *dst, uint64 srcva, uint64 len)
{
  return copyin_new(pagetable, dst, srcva, len);
}

// Copy a null-terminated string from user to kernel.
// Copy bytes to dst from virtual address srcva in a given page table,
// until a '\0', or max.
// Return 0 on success, -1 on error.
int
copyinstr(pagetable_t pagetable, char *dst, uint64 srcva, uint64 max)
{
  return copyinstr_new(pagetable, dst, srcva, max);
}

通用的copy函数

在hint2中提示我们需要更改fork,exec,sbrk这些函数,所以可以提取一个公用的将用户空间的映射添加到每个进程的内核页表的函数:

void u2kcopy(pagetable_t pagetable,pagetable_t kernelpt,uint64 start,uint64 sz) {
  pte_t *upte,*kpte;
  uint64 va;

  if (start > sz) {
    return ;
  }

  start = PGROUNDUP(start);
  // 每次将一页添加进内核页表
  for(va = start;va < sz;va += PGSIZE) {
    if ((upte = walk(pagetable,va,0))==0 || (kpte = walk(kernelpt,va,1)) == 0) {
      panic("u2kcopy: walk error");
    }
    *kpte = *upte & ~(PTE_U | PTE_X | PTE_W);
  }
}

看一下PGROUNDUP的作用:

PGROUNDUP和PGROUNDDOWN是将地址发送到PGSIZE的倍数的宏。这些通常用于获得页面对齐的地址。 PGROUNDUP将把地址四舍五入到PGSIZE的更高倍数,而PGROUNDDOWN会将其舍入到PGSIZE的较低倍数。让我们采取一个实例,如果PGROUNDUP被调用PGSIZE 1KB的系统与地址620

PGROUNDUP(620)上==>((620 +(1024 - 1))&〜(1023))= => 1024 因此地址620被向上舍入到1024

类似地,对于PGROUNDDOWN考虑

PGROUNDDOWN(2400)==>(2400 &〜(1023))==> 2048

#define PGROUNDUP(sz)  (((sz)+PGSIZE-1) & ~(PGSIZE-1))

修改fork等函数

对fork()的修改:

int
fork(void)
{
  int i, pid;
  struct proc *np;
  struct proc *p = myproc();

  // 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;
  }
  np->sz = p->sz;

  np->parent = p;

  // copy saved user registers.
  *(np->trapframe) = *(p->trapframe);

  // Cause fork to return 0 in the child.
  np->trapframe->a0 = 0;

  // increment reference counts on open file descriptors.
  for(i = 0; i < NOFILE; i++)
    if(p->ofile[i])
      np->ofile[i] = filedup(p->ofile[i]);
  np->cwd = idup(p->cwd);
    
  // np是子进程,需要将fork出的子进程也一样处理。把用户进程页表copy到kernel页表中
  // np->sz = p->sz 是进程的内存大小,Xv6使用从零开始的虚拟地址作为用户地址空间
  u2kcopy(np->pagetable,np->kernelpt,0,np->sz);
    
  safestrcpy(np->name, p->name, sizeof(p->name));

  pid = np->pid;

  np->state = RUNNABLE;

  release(&np->lock);

  return pid;
}

对exec()的修改:

int
exec(char *path, char **argv)
{
  ......

  // Allocate two pages at the next page boundary.
  // Use the second as the user stack.
  sz = PGROUNDUP(sz);
  uint64 sz1;
    // 分配给sz1两页的内存大小
  if((sz1 = uvmalloc(pagetable, sz, sz + 2*PGSIZE)) == 0)
    goto bad;
    // sz就是两页大
  sz = sz1;
  uvmclear(pagetable, sz-2*PGSIZE);
  sp = sz;
  stackbase = sp - PGSIZE;
	// 在uvmclear后再添加进kernel pt中
  u2kcopy(pagetable,p->kernelpt,0,sz);

  // Push argument strings, prepare rest of stack in ustack.
  for(argc = 0; argv[argc]; argc++) {
    if(argc >= MAXARG)
      goto bad;
    sp -= strlen(argv[argc]) + 1;
    sp -= sp % 16; // riscv sp must be 16-byte aligned
    if(sp < stackbase)
      goto bad;
    if(copyout(pagetable, sp, argv[argc], strlen(argv[argc]) + 1) < 0) // Copy from kernel to user.
      goto bad;
    ustack[argc] = sp;
  }
  ustack[argc] = 0;

  ......
}

对sbrk()的修改:通过系统调用可以看到sys_sbrk是调用growproc()的

Xv6使用从零开始的虚拟地址作为用户地址空间,幸运的是内核的内存从更高的地址开始。然而,这个方案将用户进程的最大大小限制为小于内核的最低虚拟地址内核启动后,在XV6中该地址是0xC000000,即PLIC寄存器的地址;请参见kernel/vm.c中的kvminit()kernel/memlayout.h和文中的图3-4。您需要修改xv6,以防止用户进程增长到超过PLIC的地址

// Grow or shrink user memory by n bytes.
// Return 0 on success, -1 on failure.
int
growproc(int n)
{
  uint sz;
  struct proc *p = myproc();

  sz = p->sz;
  if(n > 0){
      // n>0时,再加上是有可能会增长到超过PLIC的地址的,这样就可能渗透到内核了
    if (PGROUNDUP(sz + n) >= PLIC)
      return -1;
    if((sz = uvmalloc(p->pagetable, sz, sz + n)) == 0) {
      return -1;
    }
    u2kcopy(p->pagetable,p->kernelpt,sz-n,sz);
  } else if(n < 0){
    sz = uvmdealloc(p->pagetable, sz, sz + n);
  }
  p->sz = sz;
  return 0;
}

对userinit()的修改:

hint3:不要忘记在userinit的内核页表中包含第一个进程的用户页表

// Set up first user process.
void
userinit(void)
{
  struct proc *p;

  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;
  u2kcopy(p->pagetable,p->kernelpt,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

  safestrcpy(p->name, "initcode", sizeof(p->name));
  p->cwd = namei("/");

  p->state = RUNNABLE;

  release(&p->lock);
}

这样就ok啦,执行make grade测试或者make qemu ; ./usertests 测试:

== Test pte printout == 
$ make qemu-gdb
pte printout: OK (3.5s) 
== Test answers-pgtbl.txt == answers-pgtbl.txt: OK 
== Test count copyin == 
$ make qemu-gdb
count copyin: OK (1.1s) 
== Test usertests == 
$ make qemu-gdb
(172.9s) 
== Test   usertests: copyin == 
  usertests: copyin: OK 
== Test   usertests: copyinstr1 == 
  usertests: copyinstr1: OK 
== Test   usertests: copyinstr2 == 
  usertests: copyinstr2: OK 
== Test   usertests: copyinstr3 == 
  usertests: copyinstr3: OK 
== Test   usertests: sbrkmuch == 
  usertests: sbrkmuch: OK 
== Test   usertests: all tests == 
  usertests: all tests: OK 
== Test time == 
time: OK 
Score: 66/66

Last updated