MIT 6.S081Lab3: page tables

Pgtbl

  • Print a page table
  • A kernel page table per process
  • Simplify copyin/copyinstr

This Lab simply optimizes the page table function of the system, so that the program can directly parse the pointer of the user mode when the program is in the kernel mode.

The author took about 8 hours

Print a page table

The first part is to add a function vmprint to the system to print a given page table. This function receives a parameter pagetable (the physical address of the root page table), and recursively traverses the entire page. table, print valid table entries.
Refer to the freewalk function (defined in kernel/vm.c:331), traverse 512 entries each time, if the entry is valid, print the relevant information (what level , the number of items, the pte content and the physical address corresponding to the pte content), and if it is a first-level and second-level page table, continue to recurse until the third-level page table returns. The reference code is as follows:

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

void
vmprint(pagetable_t pgtbl)
{<!-- -->
  printf("page table %p\\
", pgtbl);
  vmprint_helper(pgtbl, 1);
}

A kernel page table per process

As the title, the content of the second part is to add a separate copy of the kernel page table for each process, paving the way for the next section to directly dereference the user mode pointer.

First, you need to add a field to the process structure (defined in kernel/proc.h) struct proc to maintain a copy of the kernel page table, as shown in the following figure

Then, since we need to initialize a copy of the kernel page table for each process when allocating processes, we need to refer to the kvminit function (defined in kernel/vm .c:66), write a function proc_kvminit that initializes the copy of the kernel page table in the process, the code is as follows. The content of this function is basically the same as the kvminit function. The uvmmap is similar to the kvmmap function (defined in kernel/vm.c:171), which maps a given virtual address and physical address range , the only difference is that the former modifies the incoming specified page table instead of just the global kernel page table.

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

/*
 * create a direct-map page table for the given process.
 */
pagetable_t
proc_kvminit()
{<!-- -->
  pagetable_t pgtbl = (pagetable_t) kalloc();
  if (pgtbl == 0) return 0;
  memset(pgtbl, 0, PGSIZE);
  
  // uart registers
  uvmmap(pgtbl, UART0, UART0, PGSIZE, PTE_R | PTE_W);

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

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

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

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

  // map kernel data and the physical RAM we'll make use of.
  uvmmap(pgtbl, (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(pgtbl, TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);

  return pgtbl;
}

Then call the proc_kvminit function defined above in the allocproc function (defined in kernel/proc.c:95) to realize initialization during process allocation In-process copy of the kernel page table. At the same time, it is also necessary to move the initialization code segment of the page table entry corresponding to the process kernel stack in procinit to the allocproc function, as shown below. What needs to be noted here is that the modification of the process context field in the original code must be placed at the bottom. (I don’t know why for the time being, I will make up the reason when I know it)

...

  // initialize the process kernel page table
  p->kernel_pagetable = proc_kvminit();
  if(p->kernel_pagetable == 0){<!-- -->
    freeproc(p);
    release( &p->lock);
    return 0;
  }

  // initialize the process kernel stack in kernel process kernel page table
  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;

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

Next, in the scheduler function (defined in kernel/proc.c:489), when the scheduling process is executed, the kernel page table corresponding to the process is loaded into the satp register , and call sfence_vma to refresh. After the process is executed, the page table is called to switch back to the global kernel page table. The code segment is shown below.

 // load process's kernel page table and flush the TLB
    w_satp(MAKE_SATP(p->kernel_pagetable));
    sfence_vma();

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

    // load kernel page table when process done
    kvminithart();

Finally, the copy of the kernel page table maintained by the process needs to be released in the freeproc function (defined in kernel/proc.c:155). It is necessary to release the kernel stack physical space maintained by the kernel page table in the process, and call the uvmunmap function (defined in kernel/vm.c:230 ) can be. At the same time, it is also necessary to destroy the maintained kernel page table copy. Since the freewalk function only destroys the first-level and second-level page table entries, you need to write a similar function to destroy the third-level page. Table entries, as shown below.

// Recursively free process's kernel page-table pages.
void
proc_freewalk(pagetable_t pagetable)
{<!-- -->
  // there are 2^9 = 512 PTEs in a page table.
  for(int i = 0; i < 512; i ++ ){<!-- -->
    pte_t pte = pagetable[i];
    if((pte & amp; PTE_V) & amp; & amp; (pte & amp; (PTE_R|PTE_W|PTE_X)) == 0){<!-- -->
      // this PTE points to a lower-level page table.
      uint64 child = PTE2PA(pte);
      proc_freewalk((pagetable_t)child);
      pagetable[i] = 0;
    } else if(pte & PTE_V){<!-- -->
      pagetable[i] = 0;
    }
  }
  kfree((void*)pagetable);
}

The relevant code segment in the freeproc function is as follows

 // free process's kernel page table
  uvmunmap(p->kernel_pagetable, p->kstack, 1, 1);
  p->kstack = 0;
  proc_freewalk(p->kernel_pagetable);
  p->kernel_pagetable = 0;

Simplify copyin/copyinstr

What needs to be implemented in this part is to add the user space mapping of each process to the kernel page table copy maintained by the process (created in the previous section), since the virtual address of the user space starts from 0, and the virtual address of the kernel starts from higher (the document says it is PLIC, but Figure 3.3 in xv6book starts from CLIT, I don’t know why), so some virtual space is left for user space mapping (0~PLIC-1) .
We need the kernel page maintained for the process in the fork function, exec function, growproc function and userinit function The userspace mapping is added to the table, since these functions change the user mapping.

First, I defined a function uvm2ukvm following the uvmcopy function (defined in kernel/vm.c:384), which receives two pages One is the user process page table, the other is the kernel page table maintained in the user process, and receives the start virtual address and the end virtual address to be mapped, and copies the user space virtual address in this range to the kernel page maintained by the process table. Note that the PTE_U flag needs to be set to 0, otherwise the kernel cannot access it.

void uvm2ukvm(pagetable_t upgtbl, pagetable_t ukpgtbl, uint64 st, uint64 ed)
{<!-- -->
  pte_t *pte_u, *pte_uk;
  uint64 pa, i;
  uint flags;

  for (i = st; i < ed; i + = PGSIZE) {<!-- -->
    if((pte_u = walk(upgtbl, i, 0)) == 0)
      panic("uvm2ukvm: pte_u should exist");
    if((*pte_u & PTE_V) == 0)
      panic("uvm2ukvm: page not present");
    pa = PTE2PA(*pte_u);
    flags = PTE_FLAGS(*pte_u);
    flags & amp;= (~PTE_U);

    if((pte_uk = walk(ukpgtbl, i, 1)) == 0)
      panic("uvm2ukvm: pte_uk should exist");
    *pte_uk = PA2PTE(pa) | flags;
  }
}

In the fork function (defined in kernel/proc.c:289), call the above function and add a line of code.

...
  // 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;

  uvm2ukvm(np->pagetable, np->kernel_pagetable, 0, np->sz);
  ...

In the exec function (defined in kernel/exec.c:13), just add the previous line of code in the same way.

...
  // Commit to the user image.
  oldpagetable = p->pagetable;
  p->pagetable = pagetable;
  p->sz = sz;
  uvm2ukvm(p->pagetable, p->kernel_pagetable, 0, sz);
  p->trapframe->epc = elf.entry; // initial program counter = main
  p->trapframe->sp = sp; // initial stack pointer
  proc_freepagetable(oldpagetable, oldsz);
  ...

In the growproc function, when applying for memory growth, it is necessary to judge whether the upper bound of the virtual address after growth exceeds the starting address of the PLIC, and if so, return -1, otherwise, the above function will be called to increase Copy the address range to the kernel page table maintained by the process.

...
    if (PGROUNDUP(sz + n) > PLIC) {<!-- -->
      return -1;
    }
    if((sz = uvmalloc(p->pagetable, sz, sz + n)) == 0) {<!-- -->
      return -1;
    }
    uvm2ukvm(p->pagetable, p->kernel_pagetable, sz - n, sz);
  ...

When the process page table is initialized for the first time in the userinit function, it is also copied.

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

Finally, change the content in the copyin function and copyinstr function body to call the copyin_new and copyinstr_new functions.