MIT-6.S081实验三学习记录

lab3内容

实验三由三个部分:

  1. 实现一个函数vmprint(),要求其能够打印页表内容
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

可以看出格式就如下,第一行打印出页表地址,然后不断递归把所有的pte项打印出来。
然后实验提示可以根据freewalk函数来修改。

  1. 第二个部分是为每一个进程创建一个内核页表,但是要注意,每个进程的内核页表是在进程分配到内存后才创建。而内核自身的页表初始化不能被破坏。

  2. 第三个部分是在第二个实验的基础上,将用户的内核页表建立映射。这样的话可以简化用户寻址需要不断进入内核的过程,从而提升速度。

1、Print a page table (easy)

根据提示,我们应该把vmprint()函数放到/kernel/vm.c中,而且提示我们参考freewalk函数来改。
下面是freewalk函数

## /kernel/vm.c
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);
}

可以看到,这个函数告诉了我们如何去递归遍历获取所有的页表项内容,但由于这里要递归获取子项,所以我们先把页表首地址打印出来,然后定义pgtblprint函数来打印子项。

## /kernel/vm.c
void vmprint(pagetable_t pagetable){
  printf("page table %p\n", pagetable);  //print the address of pagetable
  pgtblprint(pagetable, 0);
}
void
pgtblprint (pagetable_t pagetable,int depth)
{
  printf("page table %p\n", pagetable);  //print the address of pagetable
  // there are 2^9 = 512 PTEs in a page table.
  for(int i = 0; i < 512; i++){
    pte_t pte = pagetable[i];
    //PTE_V 有效(被使用的),且pte项不为空,说明时有效的PTE,可以打印;
    if(pte & PTE_V){
      printf("..");
      for(int j = 0; j < depth; j++){
      	printf(" ..");
      	}
      //分别是页数、pte、pa
      printf("%d: pte %p pa %p\n", i, pte, PTE2PA(pte));
    
    //递归获取PTE项的数目,并且打印
    //如果 PTE 的读、写、执行权限均为 0,说明这是一个指向下一级页表的 PTE,depth+1。
    if((pte & (PTE_R|PTE_W|PTE_X)) == 0){
      	uint64 child = PTE2PA(pte);
      	pgtblprint((pagetable_t)child, depth+1);
    	}
    }
  }
  return;
}

接下来就是把pgtblprintvmprint两个函数加到defs.h即可。

## /kernel/defs.h
// vm.c
void            kvminit(void);
void            kvm_pt_init(pagetable_t);
...
int             copyinstr(pagetable_t, char *, uint64, uint64);
void		 vmprint(pagetable_t); //加在这里
void		 pgtblprint (pagetable_t,int) //加在这里

需要注意,是要在exec返回之前打印,因此,在其返回之前调用vmprint函数即可。

## /kernel/exec.c
int
exec(char *path, char **argv)
{
  .......
  // Commit to the user image.
  oldpagetable = p->pagetable;
  p->pagetable = pagetable;
  p->sz = sz;
  p->trapframe->epc = elf.entry;  // initial program counter = main
  p->trapframe->sp = sp; // initial stack pointer
  proc_freepagetable(oldpagetable, oldsz);
  
  vmprint(p->pagetable); //在返回之前打印页表
  return argc; // this ends up in a0, the first argument to main(argc, argv)
  ......

然后使用测试脚本运行一下,通过没问题:

jimmy@ubuntu:~/xv6-test/xv6-labs-2020$ ./grade-lab-pgtbl pte printout
make: 'kernel/kernel' is up to date.
== Test pte printout == pte printout: OK (1.4s) 

2、A kernel page table per process (hard)

没修改前,xv6只有一个全局的内核页表,所有的用户进程想要使用,必须进入内核,然后使用这个全局共享页表。然后这个实验就是要求能够为每个进程创建一个内核页表。
根据提示,首先在struct proc(proc.h)的结构体里加一项进程的内核页表。

## /kernel/proc.h
struct proc {
  struct spinlock lock;
  ......
  pagetable_t kpt_proc; //kernel pagetable of process
  ......
};

然后修改虚拟内存初始化的一些函数。由于要初始化内核自身的内核页表,又要添加进程的内核页表,且这两个相差不大,所以干脆把初始化函数相同的部分写到一起,方便使用。

所以直接复制kvminit() 里面的内容,新建函数 ***pagetable_t kvminit_newpgtbl()***用来分配页表内存以及初始化映射。注意不要把CLINT复制过去,从进程的进程页表复制到进程的内核页表,最多不超过PLIC,然后可以看到在PLIC下面只有CLINT,也就是说这里可以给进程随便用,而不需要分配一个CLINT映射。

在这里插入图片描述

## /kernel/vm.c
//用于所有的内核页表映射初始化函数
pagetable_t
kvminit_newpgtbl()
{
  pagetable_t kpt = (pagetable_t) kalloc();
  memset(kpt, 0, PGSIZE);
    // uart registers
  kvmmap(kpt, UART0, UART0, PGSIZE, PTE_R | PTE_W);
  
  // virtio mmio disk interface
  kvmmap(kpt, VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);
  
  // PLIC
  kvmmap(kpt, PLIC, PLIC, 0x400000, PTE_R | PTE_W);

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

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

然后把原来的kvminit() 修改为只初始化内核态的内核页表,这里记得把CLINT初始化。

## /kernel/vm.c
void
kvminit()
{
  kernel_pagetable = kvminit_newpgtbl(); 
  // CLINT
  kvmmap(kernel_pagetable, CLINT, CLINT, 0x10000, PTE_R | PTE_W);
}

需要注意,因为原来kvmmap的代码是只初始化内核态页表的,所以做一些修改。

## /kernel/vm.c
//将原来的kernel_pagetable 改为 pt,这样可以为所有需要初始化的内核页表建立映射
void 
kvmmap(pagetable_t pt, uint64 va, uint64 pa, uint64 sz, int perm) //change for lab 3
{ 
  if(mappages(pt, va, sz, pa, perm) != 0)
    panic("kvmmap");
}

然后把kvminit_newpgtbl()void kvminit()kvmmap() 三个函数加到defs.h中。
接下来,就要在进程初始化时,把该进程的内核页表初始化。所以修改allocproc函数。

## proc.h
static struct proc*
allocproc(void)
{
  struct proc *p;
  ......
  // An empty user page table.
  p->pagetable = proc_pagetable(p);
  if(p->pagetable == 0){
    freeproc(p);
    release(&p->lock);
    return 0;
  }
  
  //这里需要为每个进程分配内存,并且初始化页表
  p->pt_proc = kvminit_newpgtbl();
  if(p->kpt_proc == 0){
    freeproc(p);
    release(&p->lock);
    return 0;
  }
  ...
  return p;
}

但是这样还不行,课上讲过CPU里面有一个SATP寄存器,里面会放页表的地址,因此,为了让进程用到自己分配的内核页表,需要在调度器将 CPU 交给进程执行之前,切换到该进程对应的内核页表。

## proc.h
void
scheduler(void)
{
	  ......
      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;
        
        //在这里把SATP寄存器设置为进程自己的内核页表,然后清除快表缓存
        w_satp(MAKE_SATP(p->pt_proc)); 
        sfence_vma();
        
        swtch(&c->context, &p->context);
        
        kvminithart(); //切换回全局内核页表
        ......
}

目前,给一个进程分配内核页表,并且建立好初始化映射的过程就结束了。接下来需要在释放的时候把该进程对应的内核页表也释放掉。所以还要修改freeproc 函数。需要注意, 我们不需要去回收内核页表所映射到的物理地址. 因为物理地址是全局共享的(进程没有自己的内核态页表时用的可是全局的内核页表)。

## proc.h
static void
freeproc(struct proc *p)
{
  ......
  //只释放虚拟内存而不释放物理内存
  // 释放进程的内核栈(
  //注意,这里虽然传入了p->kpt_proc,但是这个只是为了寻址用的,因为之前都用的全局内核页表
  void *kstack_pa = (void *)kvmpa(p->kpt_proc, p->kstack);
  kfree(kstack_pa);
  p->kstack = 0;
  
  //释放内核页表的虚拟内存,而不释放物理内存
  //不可用proc_freepagetable()
  kvm_free_kernelpt(p->kpt_proc);
  p->pt_proc = 0;
  p->state = UNUSED;
}

因此接下来就定义一下kvm_free_kernelpt 函数,这个函数作用上面也说了,就是仅仅释放内核页表的虚拟地址,而不释放物理地址,这个就参考freewalk 来遍历每个PTE,然后释放就行。

## vm.c
void
kvm_free_kernelpt(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];
    uint64 child = PTE2PA(pte);
    if((pte & PTE_V) && (pte & (PTE_R|PTE_W|PTE_X)) == 0){
      kvm_free_kernelpt((pagetable_t)child);
      pagetable[i] = 0;
    }
  }
  kfree((void*)pagetable);
}

如果编译报错的话,往往是一些修改了的函数参数,比如:

// virtio_disk.c
#include "proc.h" // 添加头文件引入

// ......
void
virtio_disk_rw(struct buf *b, int write)
{
// ......
disk.desc[idx[0]].addr = (uint64) kvmpa(myproc()->kpt_proc, (uint64) &buf0); // 调用 myproc(),获取进程内核页表
// ......
}

所以总结一下,就是首先,给每个进程的结构体加一个pagetable变量,用于存每个进程的内核页表。然后修改全局内核页表的初始化函数,让其能够为每个进程与内核本身初始化内核页表,尤其是建立映射。之后在进程初始化时,调用相应的内核页表初始化函数初始化,而且需要修改CPU的SATP寄存器,防止进程继续使用全局的内核页表。最后在进程结束后,释放掉相应的内核页表虚拟地址,而不是物理地址(因为这是全局共享的)。

3、Simplify copyin/copyinstr (hard)

根据之前的阐述,这个实验是为了体现出为每个进程添加内核页表的好处,即每个进程不需要通过软件进入内核,然后读取共享的内核页表,而是使用每个进程自己的内核页表副本去寻址。这个实验的目标是,在进程的内核态页表中维护一个用户态页表映射的副本,这样使得内核态也可以对用户态传进来的指针(逻辑地址)进行解引用。这样做相比原来 copyin 的实现的优势是,原来的 copyin 是通过软件模拟访问页表的过程获取物理地址的,而在内核页表内维护映射副本的话,可以利用 CPU 的硬件寻址功能进行寻址,效率更高并且可以受快表加速。

要实现这样的效果,我们需要在每一处内核对用户页表进行修改的时候,将同样的修改也同步应用在进程的内核页表上,使得两个页表的程序段(0 到 PLIC 段)地址空间的映射同步。

首先按照要求,需要把vm.c里面的copyin()copyinstr() 函数替换为vmcopyin.c里面的copyin_newcopyinstr_new函数。
所以,首先在defs.h文件中将copyin_newcopyinstr_new加入声明。

## kernel/defs.h
...
//vmcopyin.c
int copyin_new(pagetable_t, char*, uint64 , uint64);
int copyinstr_new(pagetable_t, char*, uint64 , uint64);
...

然后再vm.c 中把原来的copyin()copyinstr() 注释掉,重新定义为:

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

接下来的函数用于做一个映射的副本,将 src 页表的一部分页映射关系拷贝到 dst 页表中。
tips:其实,上一个实验就是在做内核页表的副本,每个进程可以通过进入内核接触到内核态页表(慢),也可以直接访问内核页表的副本(快)。

## kernel/vm.c
int
kvmcopymappings(pagetable_t src, pagetable_t dst, uint64 start, uint64 sz)
{
  pte_t *pte;
  uint64 pa, i;
  uint flags;

  // PGROUNDUP: prevent re-mapping already mapped pages (eg. when doing growproc)
  for(i = PGROUNDUP(start); i < start + sz; i += PGSIZE){
    if((pte = walk(src, i, 0)) == 0)
      panic("PTE is not existed");
    if((*pte & PTE_V) == 0)
      panic("PTE is not valid");
    pa = PTE2PA(*pte); //obtain physical adderss
    // `& ~PTE_U` 表示将该页的权限设置为非用户页
    // 必须设置该权限,RISC-V 中内核是无法直接访问用户页的。
    flags = PTE_FLAGS(*pte) & ~PTE_U;
    if(mappages(dst, i, PGSIZE, pa, flags) != 0){
      goto err;
    }
  }
 return 0;
 err:
 //如果映射复制有误,就把dst删掉
  uvmunmap(dst, PGROUNDUP(start), (i - PGROUNDUP(start)) / PGSIZE, 0);
  return -1;
}

然后使用uvmcopy函数,在fork(), exec(), sbrk()userinit() 的相应位置进行pagetable的同步。

int
fork(void)
{
  ...
  np->sz = p->sz;
  //将np->pagetable复制到np->kpt_proc
  if(kvmcopymappings(np->pagetable, np->kpt_proc, 0, np->sz) != 0){
    freeproc(np);
    release(&np->lock);
    return -1;
  }
  ...
  return pid;
}

接下来修改exec.c,主要是一个判断当前地址最高处有没有超过PLIC的限制,然后更新进程内核页表的映射,

int
exec(char *path, char **argv)
{
  	...
    uint64 sz1;
    if((sz1 = uvmalloc(pagetable, sz, ph.vaddr + ph.memsz)) == 0)
      goto bad;
    //sz1是请求分配PTE和物理内存后,并从旧地址分配完物理内存到新地址,返回现在的进程大小,然后用它和PLIC做一个判断
    if(sz1 >= PLIC){ // if sz1 >= PLIC, it is bad
    	goto bad;
    }
    ...
  p = myproc();
  uint64 oldsz = p->sz;
	...
	......
  //更新映射,并且把现在全局态内核页表的映射copy到内核页表的副本
  uvmunmap(p->pt_proc, 0, PGROUNDUP(oldsz)/PGSIZE, 0);
  kvmcopymappings(pagetable, p->kpt_proc, 0, sz);
  ...
  ......
  return -1;
}

因为在增加或减少用户内存时,页表也会发生相应的变化,因此需要让副本同步全局内核页表的信息。

// 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){
    uint64 newsz;
    if((newsz = uvmalloc(p->pagetable, sz, sz + n)) == 0) {
      return -1;
    }
    // 把内核页表副本也更新,同步全局内核页表
    if(kvmcopymappings(p->pagetable, p->kpt_proc, sz, n) != 0) {
      uvmdealloc(p->pagetable, newsz, sz);
      return -1;
    }
    sz = newsz;

  } else if(n < 0){
    uvmdealloc(p->pagetable, sz, sz + n);

    sz = kvmdealloc(p->kpt_proc, sz, sz + n);
  }
  p->sz = sz;
  return 0;
}

对于user的init进程来说,由于是单独初始化的,所以需要在这里也做一个同步

## /kernel/proc.c
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;
  //在这里加上即可
  kvmcopymappings(p->pagetable, p->kpt_proc, 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);
}

最后使用脚本测试,得到下面的结果,说明测试成功了。

jimmy@ubuntu:~/xv6-test/xv6-labs-2020$ ./grade-lab-pgtbl
make: 'kernel/kernel' is up to date.
== Test pte printout == pte printout: OK (1.4s) 
== Test answers-pgtbl.txt == answers-pgtbl.txt: OK 
== Test count copyin == count copyin: OK (0.9s) 
== Test usertests == (152.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

总结

这个实验虽然代码不多,但是需要对于页表的使用流程有个清楚的了解,然后大部分的函数也是参考vm.c 里面的函数改写的。
[1]: https://pdos.csail.mit.edu/6.S081/2020/labs/pgtbl.html
[2]: https://zhuanlan.zhihu.com/p/336091300#Simplify%20copyin/copyinstr%20(hard)
[3]: https://blog.miigon.net/posts/s081-lab3-page-tables/

  • 28
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值