MIT6.S081 Lab3:Page tables

实验目的

In this lab you will explore page tables and modify them to simplify the functions that copy data from user space to kernel space.

Print a page table[easy]

提示

  • 在kernel/vm.c中定义vmprint()函数
  • 使用kernel/risc-v.h中最后的宏定义
  • 可以参考freewalk函数
  • 在kernel/defs.h定义vmprint()原型,以便exec调用
  • 使用%p打印64位十六进制PTEs和地址

背景知识

  • 相关类型定义:typedef uint64 pte_t; typedef uint64 *pagetable_t;
  • 一些重要的宏定义
//  kernel//riscv.h
// 每一个物理页的大小
#define PGSIZE 4096 // bytes per page
// 物理页的offset
#define PGSHIFT 12  // bits of offset within a page

#define PGROUNDUP(sz)  (((sz)+PGSIZE-1) & ~(PGSIZE-1))
#define PGROUNDDOWN(a) (((a)) & ~(PGSIZE-1))
// shift a physical address to the right place for a PTE.
//取出物理地址转换为对应的页表项
#define PA2PTE(pa) ((((uint64)pa) >> 12) << 10)
//通过页表得到对应的物理页对应的编号
#define PTE2PA(pte) (((pte) >> 10) << 12)
// 取出页表项中对应的标志位
#define PTE_FLAGS(pte) ((pte) & 0x3FF)

// extract the three 9-bit page table indices from a virtual address.
#define PXMASK          0x1FF // 9 bits
#define PXSHIFT(level)  (PGSHIFT+(9*(level)))
#define PX(level, va) ((((uint64) (va)) >> PXSHIFT(level)) & PXMASK)

#define PTE_V (1L << 0) // valid
#define PTE_R (1L << 1)
#define PTE_W (1L << 2)
#define PTE_X (1L << 3)
#define PTE_U (1L << 4) // 1 -> user can access
//kernel/exec.c
if(p->pid == 1) {
	vmprint(p->pagetable);
}

实验过程
先来看一下freewalk函数,它首先会遍历整个页表。当遇到有效的页表项并且不在最后一层的时候,它会递归调用。PTE_V是用来判断页表项是否有效,而(pte & (PTE_R|PTE_W|PTE_X)) == 0则是用来判断是否不在最后一层。因为最后一层页表中页表项中W位,R位,X位起码有一位会被设置为1。注释里面说所有最后一层的页表项已经被释放了,所以遇到不符合的情况就panic(“freewalk: leaf”)。

// kernel/vm.c
// Recursively free page-table pages.
// All leaf mappings must already have been removed.
void
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 & PTE_V) && (pte & (PTE_R|PTE_W|PTE_X)) == 0){
      // this PTE points to a lower-level page table.
      uint64 child = PTE2PA(pte);
      freewalk((pagetable_t)child);
      pagetable[i] = 0;
    } else if(pte & PTE_V){
      panic("freewalk: leaf");
    }
  }
  kfree((void*)pagetable);
}

1.参考freewalk,写出_vmprint()函数

/**
 * @param pagetable 所要打印的页表
 * @param level 页表的层级
 */
void
_vmprint(pagetable_t pagetable, int level){
  // there are 2^9 = 512 PTEs in a page table.
  for(int i = 0; i < 512; i++){     //因为要打印所有pte,所以遍历512个页表项
    pte_t pte = pagetable[i];
    // PTE_V is a flag for whether the page table is valid
    if(pte & PTE_V){
      for (int j = 0; j < level; j++){
        if (j) printf(" ");
        printf("..");
      }
      uint64 child = PTE2PA(pte);
      printf("%d: pte %p pa %p\n", i, pte, child);
      if((pte & (PTE_R|PTE_W|PTE_X)) == 0){   //如果不是最后一级,就进入递归
        // this PTE points to a lower-level page table.
        _vmprint((pagetable_t)child, level + 1);
      }
    }
  }
}

/**
 * @brief vmprint 打印页表
 * @param pagetable 所要打印的页表
 */
void
vmprint(pagetable_t pagetable){
  printf("page table %p\n", pagetable);
  _vmprint(pagetable, 1);
}

2.然后再函数声明

// kernel/defs.h
void _vmprint(pagetable_t, int);
void vmprint(pagetable_t, int);

3.在exec.c中对第一个进程调用打印函数

// kernel/exec.c
if(p->pid == 1) {
	vmprint(p->pagetable);
}
return argc;

实验结果
在这里插入图片描述

A kernel page table per process[hard]

xv6 原本的设计是,用户进程在用户态使用各自的用户态页表,但是一旦进入内核态(例如使用了系统调用),则切换到内核页表(通过修改 satp 寄存器,trampoline.S)。然而这个内核页表是全局共享的,也就是全部进程进入内核态都共用同一个内核态页表.
本实验的主要目的是让每个进程都有自己的内核页表,这样在内核中执行时使用它自己内核页表的副本,为第三个实验做准备。
背景知识

  • Xv6只有一个内核页表,只要在内核中执行,就会使用它。内核页表是物理地址的直接映射,因此内核虚拟地址 x 映射到物理地址 x。 Xv6还有一个单独的页表,用于每个进程的用户地址空间,只包含该进程的用户内存的映射,从虚拟地址0开始。因为内核页表不包含这些映射,所以用户地址在内核中是无效的。因此,当内核需要使用系统调用中传递的用户指针(例如,传递给 write ()的缓冲区指针)时,内核必须首先将指针转换为物理地址。本节和下一节的目标是允许内核直接解引用用户指针,两个实验共同完成这个功能。
  • walk函数
// Return the address of the PTE in page table pagetable
// that corresponds to virtual address va.  If alloc!=0,
// create any required page-table pages.
//
// The risc-v Sv39 scheme has three levels of page-table
// pages. A page-table page contains 512 64-bit PTEs.
// A 64-bit virtual address is split into five fields:
//   39..63 -- must be zero.
//   30..38 -- 9 bits of level-2 index.
//   21..29 -- 9 bits of level-1 index.
//   12..20 -- 9 bits of level-0 index.
//    0..11 -- 12 bits of byte offset within the page.
pte_t *
walk(pagetable_t pagetable, uint64 va, int alloc)    //va : virtual addr,
{
  if(va >= MAXVA)
    panic("walk");

  for(int level = 2; level > 0; level--) {
    pte_t *pte = &pagetable[PX(level, va)];
    if(*pte & PTE_V) {
      pagetable = (pagetable_t)PTE2PA(*pte);    //*pte contains the next page table's physical address
    } else {  //PTE无效
      if(!alloc || (pagetable = (pde_t*)kalloc()) == 0)
        return 0;
      memset(pagetable, 0, PGSIZE);
      *pte = PA2PTE(pagetable) | PTE_V;
    }
  }
  return &pagetable[PX(0, va)];
}
  • mappages函数,从va和pa开始,创建PTES从而建立映射
// Create PTEs for virtual addresses starting at va that refer to
// physical addresses starting at pa. va and size might not
// be page-aligned. Returns 0 on success, -1 if walk() couldn't
// allocate a needed page-table page.
int
mappages(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;
    if(*pte & PTE_V)
      panic("remap");
    *pte = PA2PTE(pa) | perm | PTE_V;
    if(a == last)
      break;
    a += PGSIZE;
    pa += PGSIZE;
  }
  return 0;
}

实验过程
1.在进程的结构体中,添加这个内核页表

//kernel/proc.h
// 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;        //process 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)
  pagetable_t kernelpgtbl;     // Kernel page table (在 proc 中添加该 field)
};

2.在vm.c中添加新的方法proc_kpt_init,该方法用于在allocproc 中初始化进程的内核页表。这个函数还需要一个辅助函数uvmmap,该函数和kvmmap方法几乎一致,不同的是kvmmap是对Xv6的内核页表进行映射,而uvmmap将用于进程的内核页表进行映射。
内核需要依赖内核页表内一些固定的映射的存在才能正常工作,例如 UART 控制、硬盘界面、中断控制等。而 kvminit 原本只为全局内核页表 kernel_pagetable 添加这些映射。

// Just follow the kvmmap on vm.c
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 kernel page table for the process
pagetable_t
proc_kpt_init(){
  pagetable_t kernelpt = uvmcreate();
  if (kernelpt == 0) return 0;
  uvmmap(kernelpt, UART0, UART0, PGSIZE, PTE_R | PTE_W);
  uvmmap(kernelpt, VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);
  uvmmap(kernelpt, CLINT, CLINT, 0x10000, PTE_R | PTE_W);
  uvmmap(kernelpt, PLIC, PLIC, 0x400000, PTE_R | PTE_W);
  uvmmap(kernelpt, KERNBASE, KERNBASE, (uint64)etext-KERNBASE, PTE_R | PTE_X);
  uvmmap(kernelpt, (uint64)etext, (uint64)etext, PHYSTOP-(uint64)etext, PTE_R | PTE_W);
  uvmmap(kernelpt, TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);
  return kernelpt;
}

3.上面的内核页表映射函数创建好后,在allocproc.c中调用

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

// Init the kernal page table
p->kernelpt = proc_kpt_init();
if(p->kernelpt == 0){
  freeproc(p);
  release(&p->lock);
  return 0;
}

4.至此可以创建进程间相互独立的内核页表了,还有个东西要处理:内核栈。
原本的 xv6 设计中,所有处于内核态的进程都共享同一个页表,即意味着共享同一个地址空间。由于 xv6 支持多核/多进程调度,同一时间可能会有多个进程处于内核态,所以需要对所有处于内核态的进程创建其独立的内核态内的栈,也就是内核栈,供给其内核态代码执行过程。
xv6 在启动过程中,会在 procinit() 中为所有可能的 64 个进程位都预分配好内核栈 kstack,具体为在高地址空间里,每个进程使用一个页作为 kstack,并且两个不同 kstack 中间隔着一个无映射的 guard page 用于检测栈溢出错误。
在 xv6 原来的设计中,内核页表本来是只有一个的,所有进程共用,所以需要为不同进程创建多个内核栈,并 map 到不同位置(见 procinit() 和 KSTACK 宏)。而我们的新设计中,每一个进程都会有自己独立的内核页表,并且每个进程也只需要访问自己的内核栈,而不需要能够访问所有 64 个进程的内核栈。所以可以将所有进程的内核栈 map 到其各自内核页表内的固定位置(不同页表内的同一逻辑地址,指向不同物理内存)。
为了确保每一个进程的内核页表都关于该进程的内核栈有一个映射。我们需要将procinit方法中相关的代码迁移到allocproc方法中。

//剪切到allocproc.c
// 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));
uvmmap(p->kernelpt, va, (uint64)pa, PGSIZE, PTE_R | PTE_W);
p->kstack = va;

5.我们需要修改scheduler()来加载进程的内核页表到SATP寄存器。提示里面请求阅读kvminithart();
然后在scheduler()内调用即可(在切换进程前),但在结束的时候,需要切换回原先的kernel_pagetable。直接调用调用上面的kvminithart()就能把Xv6的内核页表加载回去。

//kernel/vm.c
// Store kernel page table to SATP register
void
proc_inithart(pagetable_t kpt){
  w_satp(MAKE_SATP(kpt));
  sfence_vma();
}
//kernel/proc.c  scheduler()
p->state = RUNNING;
c->proc = p;

// Store the kernal page table into the SATP
proc_inithart(p->kernelpt);

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

// Come back to the global kernel page table
kvminithart();

6.接下来还有些有关进程页表操作的函数的修改,在freeproc中释放一个进程的内核页表。首先释放页表内的内核栈,调用uvmunmap可以解除映射,最后的一个参数(do_free)为1的时候,会释放实际内存。

//proc.c
// free the kernel stack in the RAM
uvmunmap(p->kernelpt, p->kstack, 1, 1);
p->kstack = 0;

然后释放进程的内核页表,先在kernel/proc.c里面添加一个方法proc_freekernelpt。如下,历遍整个内核页表,然后将所有有效的页表项清空为零。如果这个页表项不在最后一层的页表上,需要继续进行递归。

//proc.c
void
proc_freekernelpt(pagetable_t kernelpt)
{
  // similar to the freewalk method
  // 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){
        uint64 child = PTE2PA(pte);
        proc_freekernelpt((pagetable_t)child);
      }
    }
  }
  kfree((void*)kernelpt);
}

7.修改vm.c中的kvmpa,将原先的kernel_pagetable改成myproc()->kernelpt,使用进程的内核页表。

#include "spinlock.h" 
#include "proc.h"

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

8.最后,将上面的函数声明

//kernel/defs.h
// vm.c
void            kvminit(void);
pagetable_t     proc_kpt_init(void); // 用于内核页表的初始化
void            kvminithart(void); 
void            procinithart(pagetable_t); // 将进程的内核页表保存到SATP寄存器

实验结果

$ make qemu
$ usertests

在这里插入图片描述

Simplify (copyin/copyinstr)[hard]

内核的copyin函数读取用户指针指向的内存。为了实现这个功能,它将指针转换为内核可以直接解引用的物理地址,通过walk遍历进程列表。在这个实验中,你的工作是:为每个进程中的内核页表(上个实验已经建立)添加用户映射,使得copyin(以及类似的字符串拷贝copyinstr)直接解引用用户指针。
这种方案依赖于用户虚拟地址范围,不与内核用于自己的指令和数据的虚拟地址范围重叠。Xv6对于用户地址空间使用从零开始的虚拟地址,幸运的是,内核的内存从较高的地址开始。但是,该方案确实将用户进程的最大大小限制为小于内核的最低虚拟地址。启动内核之后,这个地址是 xv6中的0xC000000,这是 PLIC 寄存器的地址; 请参见 kernel/vm.c 中的 kvminit ()、 kernel/memlayout.h 和文本中的图3-4。您需要修改 xv6,以防止用户进程增长超过 PLIC 地址。
在上一个实验中,已经使得每一个进程都拥有独立的内核态页表了,这个实验的目标是,在进程的内核态页表中维护一个用户态页表映射的副本,这样使得内核态也可以对用户态传进来的指针(逻辑地址)进行解引用。这样做相比原来 copyin 的实现的优势是,原来的 copyin 是通过软件模拟访问页表的过程获取物理地址的,而在内核页表内维护映射副本的话,可以利用 CPU 的硬件寻址功能进行寻址,效率更高并且可以受快表加速。

提示

  • copyin_new(), copyinstr_new()是已经写好的函数,用它们分别替换copyin()和copyinstr()
  • 任何需要更改内核页表的地方,也需要同步更改进程用户空间的映射。这些位置在fork(), exec(), and sbrk()
  • 在第一个进程中也不要忘了上述映射,因为第一个进程并不是由fork()创建。参见userinit()
  • 注意,一个标志位为PTE_U的页表项是无法在内核态访问的,因此在映射时对这种情况要处理,实际就是将PTE_U对应位置零
  • 注意映射空间不要超过PLIC限制

实验过程
1.将用户空间的映射添加到每个进程的内核页表,就是将进程的页表复制一份到进程的内核页表。先创建一个复制函数

void
u2kvmcopy(pagetable_t pagetable, pagetable_t kernelpt, uint64 oldsz, uint64 newsz){
  pte_t *pte_from, *pte_to;
  oldsz = PGROUNDUP(oldsz);
  for (uint64 i = oldsz; i < newsz; i += PGSIZE){
    if((pte_from = walk(pagetable, i, 0)) == 0)
      panic("u2kvmcopy: src pte does not exist");
    if((pte_to = walk(kernelpt, i, 1)) == 0)
      panic("u2kvmcopy: pte walk failed");
    uint64 pa = PTE2PA(*pte_from);
    uint flags = (PTE_FLAGS(*pte_from)) & (~PTE_U);
    *pte_to = PA2PTE(pa) | flags;
  }
}

2.然后在内核中更改进程的用户映射的每一步,(fork(),exec(),和sbrk()),都复制一份到进程的内核页表。
注意:实验中提示内核启动后,能够用于映射程序内存的地址范围是 [0,PLIC),我们将把进程程序内存映射到其内核页表的这个范围内,首先要确保这个范围没有和其他映射冲突。
查阅 xv6 book 可以看到,在 PLIC 之前还有一个 CLINT(核心本地中断器)的映射,该映射会与我们要 map 的程序内存冲突。查阅 xv6 book 的 Chapter 5 以及 start.c 可以知道 CLINT 仅在内核启动的时候需要使用到,而用户进程在内核态中的操作并不需要使用到该映射。
所以在proc_kpt_init()函数中不用映射CLINT,但在内核启动的时候需要 CLINT 映射存在,故在 kvminit() 中,需要给全局内核页表映射 CLINT。

//exec.c
int
exec(char *path, char **argv){
  sp = sz;
  stackbase = sp - PGSIZE;

  // 添加复制逻辑
  u2kvmcopy(pagetable, p->kernelpt, 0, sz);

  // Push argument strings, prepare rest of stack in ustack.
  for(argc = 0; argv[argc]; argc++) {

//fork.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;
  }
  np->sz = p->sz;
  // 复制到新进程的内核页表
  u2kvmcopy(np->pagetable, np->kernelpt, 0, np->sz);

//sbrk(), 在kernel/sysproc.c里面找到sys_sbrk(void),可以知道只有growproc是负责将用户内存增加或缩小 n 个字节。以防止用户进程增长到超过PLIC的地址,我们需要给它加个限制。
int
growproc(int n)
{
  uint sz;
  struct proc *p = myproc();
  sz = p->sz;
  if(n > 0){
    // 加上PLIC限制
    if (PGROUNDUP(sz + n) >= PLIC){
      return -1;
    }
    if((sz = uvmalloc(p->pagetable, sz, sz + n)) == 0) {
      return -1;
    }
    // 复制一份到内核页表
    u2kvmcopy(p->pagetable, p->kernelpt, sz - n, sz);
  } else if(n < 0){
    sz = uvmdealloc(p->pagetable, sz, sz + n);
  }
  p->sz = sz;
  return 0;
}

3.替换掉copyin(),copyinstr(),替换函数已经在vmcopyin.c

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

4.对于 init 进程,由于不像其他进程,init 不是 fork 得来的,所以需要在 userinit 中也添加同步映射的代码。

//kernel/proc.c
// Set up first user processoid
userinit(void)
struct proc *p;
//(+)
pte_t *pte,*kernelpte;

p = alocproc();initproc = p;
// allocate one user page and copy init's instructions/ and data into it .
uvminit(p->pagetable, initcode, sizeof(initcode));
P->SZ = PGSIZE;

//(+)//copy user mapping to process kernel page table
pte = walk(p->pagetable, 00);
kernelpte = walk(p->kernelpt,01);
*kernelPte = (*pte) & ~PTE U;

/ prepare for the very first "return" from kernel to user.p->trapframe->epc = 0;user program counterp->trapframe->sp = PGSIZE; // user stack pointer
"initcode",sizeof(p->name));safestrcpy(p->name,p->cwd = namei("/");
p->state = RUNNABLE;
}

5.最后在kernel/defs.h中声明

// vmcopyin.c
int             copyin_new(pagetable_t, char *, uint64, uint64);
int             copyinstr_new(pagetable_t, char *, uint64, uint64);
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值