This experiment only needs to be modified in the kernel folder, mainly in proc.c, vm.c and defs.h files. You need to read the reference materials first and understand the relevant codes with several sections explaining the codes. In general, follow the prompts of the experiment can be completed step by step, but the experiment debugging up a little trouble.

Print a page table

This experiment is simple, just follow the instructions to write the freewalk() function in vm.c. But need to pay attention to, in the third level page table don’t need to decide (pte2 & (PTE_R | PTE_W | PTE_X)) = = 0. Note that the return value of PTE2PA() must be uint64. Pagetable_t cannot be used.

code

void vmprint(pagetable_t p)
{
  printf("page table %p\n", p);
  for(int i = 0; i < 512; i++){
    pte_t pte = p[i];
    if((pte & PTE_V) && (pte & (PTE_R|PTE_W|PTE_X)) == 0){
      uint64 pa = PTE2PA(pte);
      printf(".. %d: pte %p pa %p\n", i, pte, pa);
      pagetable_t p1 = (pagetable_t)pa;
      for(int j = 0; j < 512; j++){
        pte_t pte1 = p1[j];
        if((pte1 & PTE_V) && (pte1 & (PTE_R|PTE_W|PTE_X)) == 0){
          uint64 pa1 = PTE2PA(pte1);
          printf(".. ..%d: pte %p pa %p\n", j, pte1, pa1);
          pagetable_t p2 = (pagetable_t)pa1;
          for(int k = 0; k < 512; k++){
            pte_t pte2 = p2[k];
            if(pte2 & PTE_V){
              uint64 pa2 = PTE2PA(pte2);
              printf(".. . . %d: pte %p pa %p\n", k, pte2, pa2);
            }
          }
        }  
      }
    }
  }
}
Copy the code

The results

A kernel page table per process

Since each process needs to maintain a kernel pagetable, add a variable pagetable_t to the struct proc as prompted.

// 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 kernel_pagetable;// 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)
};
Copy the code

Then, we need to initialize the kernel page table for each process, switch to the kernel page table when the process is in the RUNNABLE state, and clear the kernel page table when the process is finished. To implement these requirements, you need to modify vm.c and proc.c.

Modify the vm. C

First, as prompted, we implement a function like kvminit() to create a kernel page table for each process. Unlike kvminit(), this function needs to return pagetable_t because myproc()->kernel_pagetable needs to be assigned. Note that you need to add a declaration of prockvminit() in defs.h to make it easier to call.

pagetable_t
prockvminit()
{
  pagetable_t p = (pagetable_t) kalloc();
  
  if(p == 0) return 0;
  
  memset(p, 0, PGSIZE);

  // uart registers
  prockvmmap(p, UART0, UART0, PGSIZE, PTE_R | PTE_W);

  // virtio mmio disk interface
  prockvmmap(p, VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);

  // CLINT
  prockvmmap(p, CLINT, CLINT, 0x10000, PTE_R | PTE_W);

  // PLIC
  prockvmmap(p, PLIC, PLIC, 0x400000, PTE_R | PTE_W);

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

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

Kvminit () needs to call kvmmap(), and kVMmap () uses kernel_pagetable by default, so we also need to implement a prockVMmap () function, pass the process kernel_pagetable, and then do the mapping.

void
prockvmmap(pagetable_t p, uint64 va, uint64 pa, uint64 sz, int perm)
{
  if(mappages(p, va, sz, pa, perm) ! =0)
    panic("prockvmmap");
}
Copy the code

In addition, when switching processes, kernel_pagetable of the process needs to be placed in the SATP register, and prockvminithart() should be written like kvminithart().

void 
prockvminithart(pagetable_t pagetable)
{
  w_satp(MAKE_SATP(pagetable));
  sfence_vma();
}
Copy the code

Modify the proc. C

First, you need to call prockvminit() to create the kernel page table for the process, and allocProc () should be modified as prompted to be called after the user page table for the process has been created. The kernel page table needs to be mapped to the process kernel stack. Copy the code in procinit() and modify it after the kernel page table is created. Note that the physical address of kSTACK in procinit() has just been allocated, so there is no need to allocate the physical address. Instead, call kvmpa() to calculate the physical address.

// An empty user page table.
  p->pagetable = proc_pagetable(p);
  if(p->pagetable == 0){
    freeproc(p);
    release(&p->lock);
    return 0;
  }
  
  // An proc kernel page table.
  p->kernel_pagetable = prockvminit();
  if(p->kernel_pagetable == 0){
    freeproc(p);
    release(&p->lock);
    return 0;
  }
  
  // Map kstack to kernel page table
  uint64 va = KSTACK((int) (p - proc));
  uint64 pa = kvmpa(va);
  prockvmmap(p->kernel_pagetable, va, pa, PGSIZE, PTE_R | PTE_W);
Copy the code

Next, the kernel page table of the RUNNABLE process needs to be placed in the SATP register in scheduler(), where prockvMinithart () is called.

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;
        prockvminithart(p->kernel_pagetable);
        swtch(&c->context, &p->context);
	
        // Process is done running for now.
        // It should have changed its p->state before coming back.
        kvminithart();
        c->proc = 0;

        found = 1;
      }
      release(&p->lock);
    }
#if! defined (LAB_FS)if(found == 0) {
      intr_on();
      asm volatile("wfi");
    }
#else
    ;
#endif
  }
}
Copy the code

Finally, we need to modify freeproc(), because there is no requirement to clear pages with physical addresses, so we refer to freewalk() under vm.c to remove panic, so that the program can run normally.

// free a proc kernel page table
// no memory page
void
free_proc_kernel_pagetable(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 & PTE_V) && (pte & (PTE_R|PTE_W|PTE_X)) == 0) {// this PTE points to a lower-level page table.
      uint64 child = PTE2PA(pte);
      free_proc_kernel_pagetable((pagetable_t)child);
      pagetable[i] = 0;
    }
  }
  kfree((void*)pagetable);
}
Copy the code

Simplify copyin/copyinstr

We need to call copyin_new() and copyinstr() functions in vmprint.c instead, and then modify fork, exec, and SBRK functions to make the program run normally. Copyin () and copyinstr() simply call copyin_new() and copyinstr_new(), but note that defs.h does not declare copyin_new() and 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);
}
Copy the code

The fork, exec, and SBRK changes when the user page table of the process is modified, and the kernel page table mapping of the process needs to be modified accordingly. Therefore, copy the user page table -> kernel page table first.

User page table -> Kernel page table

Uvmcopy () in vm.c creates a new physical page for the child process, copies the physical page mapped by the parent process user page table, and finally establishes a mapping between the new physical page and the child process user page table. See uVMCopy (), process user page table -> kernel page table instead of creating a new physical page, just get flags for each page entry and set up the mapping between the physical page and kernel page table. Note that uVMCopy () is a copy of the entire page, so only one parameter is required to pass in the size of the valid page entry in the page table, while the SBRK functionality requires only a mapping of the new physical page, so the changes are made accordingly. PTE_U in the flags of the kernel page table cannot be true, otherwise it cannot be read in kernel mode. In addition, when using Mappages () to create physical page and page entry mappings, panic:”remap” will appear, so use Mappages () to delete the kmappages() function of the panic condition.

int
kvmcopy(pagetable_t old, pagetable_t new, uint64 oldsz, uint64 newsz)
{
  pte_t *pte;
  uint64 pa, i;
  uint flags;
  
  oldsz = PGROUNDUP(oldsz);
  if(newsz <= oldsz) return 0;
  
  for(i = oldsz; i < newsz; i += PGSIZE){
    if((pte = walk(old, i, 0)) = =0)
      panic("kvmcopy: pte should exist");
    if((*pte & PTE_V) == 0)
      panic("kvmcopy: page not present");
    pa = PTE2PA(*pte);
    flags = PTE_FLAGS(*pte);
    flags &= (~PTE_U);
    if(kmappages(new, i, PGSIZE, pa, flags) ! =0){ goto err; }}return 0;

 err:
  uvmunmap(new.0, i / PGSIZE, 0);
  return -1;
}
Copy the code
int
kmappages(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;
    *pte = PA2PTE(pa) | perm | PTE_V;
    if(a == last)
      break;
    a += PGSIZE;
    pa += PGSIZE;
  }
  return 0;
}
Copy the code

fork

After the child has copied the parent’s physical page and created its own user page table, it can call kvmCopy (). For details, see uVMCopy () in fork().

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

  np->parent = p;
  
  // Copy child map from user to kernel
  if(kvmcopy(np->pagetable, np->kernel_pagetable, 0, np->sz) < 0){
    freeproc(np);
    release(&np->lock);
    return -1; }... }Copy the code

exec

Again, find where p->pagetable has been modified in exec() and call kvmCopy ().

int
exec(char *path, char **argv){...// Commit to the user image.
  oldpagetable = p->pagetable;
  p->pagetable = pagetable;
  p->sz = sz;
  // Copy to the proc kernel pagetable
  if(kvmcopy(pagetable, p->kernel_pagetable, 0, sz) == -1)
    goto bad;
  
  p->trapframe->epc = elf.entry;  // initial program counter = main
  p->trapframe->sp = sp; // initial stack pointer
  proc_freepagetable(oldpagetable, oldsz);
  
  
  if(p->pid == 1) vmprint(p->pagetable);
  
  return argc; // this ends up in a0, the first argument to main(argc, argv)

 bad:
  if(pagetable)
    proc_freepagetable(pagetable, sz);
  if(ip){
    iunlockput(ip);
    end_op();
  }
  return -1;
}
Copy the code

sbrk

Growproc () is called by sys_sbrk(), which is in proc.c. Because the function of SBRK is to allocate memory dynamically, it will increase when it is insufficient and decrease when it is idle, so it needs to be discussed separately. When n>0, that is, memory is added, so we need to prevent it from exceeding the PLIC limit, and then call kVMCopy () to establish the mapping between the newly allocated memory and the kernel page table. I would have considered calling uVMDealloc () directly when n<0 to reduce memory, which would have been more convenient, but running UserTests this way would have caused an error. After a careful study of uvmDealloc (), I realized that when this function calls uvmunmap(), the do_free parameter is always 1, that is, it will delete the physical page, and this step is already done by the user page table, do not want the kernel page table to do this again, so it will fail. In the end, I just called uvmunmap() as I did in uVMDealloc ().

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

  sz = p->sz;
  if(n > 0) {if(sz + n >= PLIC || (sz = uvmalloc(p->pagetable, sz, sz + n)) == 0) {
      return -1;
    }
    
    if(kvmcopy(p->pagetable, p->kernel_pagetable, p->sz, sz) == -1)
      return -1;
    
  } else if(n < 0){
    sz = uvmdealloc(p->pagetable, sz, sz + n);
    uvmunmap(p->kernel_pagetable, PGROUNDUP(sz), (PGROUNDUP(p->sz) - PGROUNDUP(sz)) / PGSIZE,0);
  }
  p->sz = sz;
  return 0;
}
Copy the code

userinit()

Finally, as prompted, the user page table that userinit() just created also needs to be synchronized to the kernel page table.

// 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;
  
  // user page to kernel page
  kvmcopy(p->pagetable, p->kernel_pagetable, 0, PGSIZE);

  // 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);
}
Copy the code

Make grade