2020 6.s081——Lab3:page tables坐牢日记

层楼终究误少年

自由早晚乱余生

你我山前没相见

山后别相逢

——郭源潮

 完整代码见:SnowLegend-star/6.s081 at pgtbl (github.com)

Print a page table (easy)

这个lab的要求一目了然

  1. 返回第一个进程的页表
  2. 不打印无效的PTE(因为第一个进程的多数PTE是0)
  3. 参考freewalk

根据freewalk函数,我们很容易可以发现实现vmprint()需要用到递归的思路,同时上一级页表进入次级页表的流程也可以参考。我开始陷入了一个小误区:是否选择打印第i个页表项需要满足这个pte指向的下一级页表的所有pte均为0。那这样还是有点实现难度的,但freewalk的递归十分简单,遂让我开始想自己是否走入了误区。

看了几遍freewalk的实现之后,我突然发现一个事实:即如果第i个页表项不为0时,那它指向的次级页表的pte则必不全为0。而我则是走入了自底向上回溯的思维误区,直接看顶层的pte是否为0便可以判断要不要打印这个pte及它的子pte。

想到这一点就很容易实现vmprint()了。设置个全局变量level来记录递归层级来控制递归打印,递归过程中注意维护level即可。

vmprint的实现如下
 

//打印页表情况 递归
int flag=0; //记录递归深度
void in_vmprint(pagetable_t pagetable){
  int i, j;
    flag++; // 增加递归深度

    for (i = 0; i < 512; i++) {
        pte_t pte = pagetable[i];

        if (pte == 0) {
            continue; // 跳过空指针
        }

        uint64 child = PTE2PA(pte);

        // 打印格式输出
        for (j = 0; j < flag; j++) {
            printf("..");
            if (j < flag - 1) { // 最后一层不输出空格
                printf(" ");
            }
        }

        printf("%d: pte %p pa %p\n", i, pte, child); // 打印PTE的内容

        if ((pte & PTE_V) && ((pte & (PTE_R | PTE_W | PTE_X)) == 0)) {
            in_vmprint((pagetable_t)child); // 递归打印下一级页表
        }
    }

    flag--; // 减少递归深度
}

void vmprint(pagetable_t pagetable){
  //打印进程的根页表(root pagetable)地址(satp register中存的)
  printf("page table %p\n",pagetable);  //打印pagetable的地址
  in_vmprint(pagetable);
}

A kernel page table per process (hard)

难,确实难。在这里磨蹭了三天。

一开始真的是看不懂实验要求在说啥,最可恶的是因为有点疲倦看岔了前两个hint,搞得我在歪路上越走越远。其实当时就感觉这个hint比较反常,还以为是自己悟性不够。结果后面看了遍原文真他娘的中计了。下面这是什么牛马翻译。

还有上面这个from我没注意,以为是在kvminit()里面调用allocproc()。焯!事实证明疲倦状态是不适合学习的,贪图那点效率极低的学习时间只会让自己得不偿失。而且这个lab有大量需要自己添加或者修改的函数,第一次碰到这种修改源码的活儿,我心里多少有点发憷,写得束手束脚。就拿第二个hint来说,让实现一个修改版的kvmini(),我直接就在kvminit()内部改了。偏离了出发点,搞得后面实现其他函数的时候汗流浃背。可谓是“上梁不正下梁歪”。

Ok,现在开始分析给出的hint:

1、Add a field to struct proc for the process's kernel page table.

向struct proc内部添加一个成员用来表示内核副本。我记得system call tracing要求向proc内部添加个mask来保证mask可以在父子进程中传递,二者类似。

2、A reasonable way to produce a kernel page table for a new process is to implement a modified version of kvminit that makes a new page table instead of modifying kernel_pagetable. You'll want to call this function from allocproc.

让我们实现一个修改版的kvminit来初始化进程的内核副本(kvminit_modify)。这里要稍微修改下

kvmmap(pagetable_t kernel_pagetable,uint64 va, uint64 pa, uint64 sz, int perm)

原本的kvmmap直接调用全局内核页表变量进行映射,所以不需要传入页表参数。而我们的kvminit_modify()是针对每个进程的内核页表进行初始化,那这就势必要给kvmmap()传入相应进程的kernel_pagetable来进行初始化了。这里其实也来一个kvmmap_modify()比较好,我还是偷懒了。

3、Make sure that each process's kernel page table has a mapping for that process's kernel stack. In unmodified xv6, all the kernel stacks are set up in procinit. You will need to move some or all of this functionality to allocproc.

这个hint说的比较直白,直接把proinit用来初始化进程内核栈映射的代码搬到allocproc里面去就行。这里要记住很重要的一点

我们给进程的内核栈分配了va这个虚拟地址,并且把这个地址的映射关系存到了进程自己的内核页表里。但是!我们并没有把其他进程的内核栈映射存到当前进程的内核页表中。这说明当前进程的内核页表有一部分是空的,没有被映射

4、Modify scheduler() to load the process's kernel page table into the core's satp register (see kvminithart for inspiration). Don't forget to call sfence_vma() after calling w_satp().

修改scheduler()。这部分最是简单,代码虽然乍一看没那么好懂,但是操作却并不难。在合适的位置添加kvminithart()就可以了。

5、scheduler() should use kernel_pagetable when no process is running.

当没有进程运行时,scheduler()就不能在调用当前进程的内核页表了,故得重新利用整个大的全局内核页表。

6、Free a process's kernel page table in freeproc.

模仿释放进程的普通页表来释放内核页表。

7、You'll need a way to free a page table without also freeing the leaf physical memory pages.

这个“leaf physical memory pages”是困扰我最久的。一开始根本没看懂这个叶子物理内存是什么东西,问了下GPT在那里含糊其辞。可以理解为内存从vapa的映射涉及到四个物理地址:三级页表的pa、二级页表的pa、一级页表的pa、一级页表pte指向的物理内存。我们要释放的就是三个层级页表自身所占用的物理页。

我觉得还不如不加这句注释,因为进程的普通页表就是释放三层页表自身的物理地址。特地加这句hint反而会让人多想。

Hints大概就分析到这里,毫不夸张地说完全看懂hints这个lab就完成一半了。lab最难的部分应当要属内核页表的释放了,真的是改的人头昏眼花。

在修改procalloc()部分需要注意的一点是注意kvminithart()的使用。按理说每新建一个进程的时候就调用一遍kvminithart()应该不会有问题。因为kvminithart()是直接操作全局内核页表,没有涉及到其他操作。但是这里却会报下面的错误,真是令人费解。

被这个错误折磨了好久,结果问题出在调用kvminithart()。

难搞的是内核的进程处理不会立即报错,而是会运行到一个莫名其妙的地方在报错,“异步性报错”真是害苦了我啊。而且我之前赖以生存的神器printf大法也因此失效了,呜呼~

接下来详细分析下proc_freekernelpagetable()部分。这里就用到了hint3的分析“但是!我们并没有把其他进程的内核栈映射存到当前进程的内核页表中。这说明当前进程的内核页表有一部分是空的,没有被映射”我最初的思路是直接调用uvmfree(),因为在我看来释放用户的内核页表和普通页表操作大同小异,即va直接填0。还是参考了别人的博客突然发现va是个至关重要的破局点。

 如果va直接填0,就会出现下列的问题。幸亏官方给的代码报错处理写的十分详细,静下心来仔细分析一番也是可以发现问题所在的。发现va的问题之后也就相当于解决uvmfree_kernel()部分了。

 

根据这里可以看出是kernel_pagetable的映射有问题。uvmfree_kernel()还有一个注意点就是用户进程的trampoline和trapframe不可以取消映射,这我倒是没有理解。我悟了,对比普通页表可以发现我们并没有给进程内核页表分配trampoline和trapframe,所以才会出现“uvmunmap: not mapped”的问题。

完成这个lab最后一个关键点在kvmpa()。得注意walk函数传入的应该是当前进程的内核页表而不是全局内核页表,要不是有报错信息还真是隐蔽至极。

解决以上几个问题后差不多就完成这个lab了,现在看起来条理清晰,当时写的时候真是让我道心一次又一次受损,几欲破罐子破摔。个中艰辛,更与谁人说

Simplify copyin/copyinstr (hard)

下面还是先对官方给的hints进行分析:

1、Replace copyin() with a call to copyin_new first, and make it work, before moving on to copyinstr.

这个hint要求我们把copyin()函数体的内容替换为copyin_new(),不需要进行别的修改,copyin_str()亦是如此。照做就可。

2、At each point where the kernel changes a process's user mappings, change the process's kernel page table in the same way. Such points include fork(), exec(), and sbrk().

由于这部分要求我们把进程的用户页表映射到了进程的内核页表里,所以几个涉及页表的函数调用也应该进行相应的修改。把用户页表映射到内核页表看起来很抽象,但细究也就是进行pte复制——把用户页表内存在的页表项都复制到内核页表中就行。然后再fork()、exec()、sbrk()调用这个映射函数即可。

这里映射的地址也大有讲究。在sbrk()真正的实现函数growproc中,如果空间是增长的,那需要进行的映射是“把用户页表新增长的空间映射到内核页表中”,而不是从头开始映射一遍,不然就会出现下列“panic: remap”的问题。因为在内核页表中0~oldsize的部分已经是用户页表的内容了,但是oldsize~newsize的部分确实空的,所以需要映射的只有这一小块。

fork()要注意的点在进行进程的内核页表拷贝时,不可以像用户页表一样调用uvmcopy()进行拷贝,而是要调用我们自己写的内核拷贝函数。用户页表之所以可以调用uvmcopy(),是因为用户页表被分配后内部并没有其他初始化映射。uvmcopy()而在内核页表内部,我们初始化了内核栈(kstack)映射。

exec()也需要注意映射的地址问题。千万不能习惯性地把最后一个参数写成PGSIZE,否则依旧会报错。

3、Don't forget that to include the first process's user page table in its kernel page table in userinit.

在第一个进程中添加user2kernel_mappages()。这里最后一个参数是p->sz或者PGSIZE都没问题。

4、What permissions do the PTEs for user addresses need in a process's kernel page table? (A page with PTE_U set cannot be accessed in kernel mode.)

在user2kernel_mappages()中,把用户页表的pte复制到内核页表的pte时,要注意PTE_U的修改。如果内核页表中某pte的PTE_U为1,则该pte是无法被内核访问的。

5、Don't forget about the above-mentioned PLIC limit.

只有用户页表空间出现变化时其大小才可能超过PLIC,用户页表空间初始是位于PLIC之下的。所以在sbrk()真正的实现部分添加限制语句即可。

这个部分折磨了我一周,真的是道心破碎了。那个错误真是让我束手无策,一度怀疑是part2有隐患才导致这部分也跟着出错,为此看了一遍又一遍part2的代码。结果还是一无所获。在网上看到最多的问题是remap问题,但是我是kerneltrap,问题还出在memmove这个根本不该出错的函数里。

最后实在没啥办法了,只能先copy一份正确的代码,再把自己的函数一个个替换过去。以此来排查错误到底出在哪儿。改到最后发现错误是“remap”了,呜呼!天可怜见,看到这个错误那距离成功只有一步之遥了。解决方法就是注释掉进程内核中“CLIENT”部分的映射。

关于PGSIZE和p->sz的辨析

最初页表的大小都是PGSIZE,故初始化阶段涉及到地址或者页表大小时传入PGSIZE是没问题的。但是后序涉及页表空间的参数就不一定是PGSIZE了,因为进程可能调用了sbrk()使用户页表的大小发生变化。同时,涉及到某页表内容(trapframe、kstack)时,给它们给配的空间默认也是一个页表的初始大小,故涉及空间大小时参数也是PGSIZE。

 user2kernrl_mappages()内部的映射也要注意。因为这里是进行顶层pte的复制,而一个顶层pte会索引到次级页表,所以这里传入的大小应该是PGSIZE。

在copyin()中,我还给自己挖了个坑。并不是说copyin_new返回值小于0就一定是有问题的。Xv6应该是内置了错误处理,是允许copyin_in返回-1的,我在这里打印输出反而会误导自己。

      话说回来这还是得怪那个“kerneltrap”,就是因为这个报错我才开始疑神疑鬼,可恶

首先注意下vm.c中头文件的引入顺序

#include "param.h"
#include "types.h"
#include "memlayout.h"
#include "elf.h"
#include "riscv.h"
#include "defs.h"
#include "fs.h"
#include "spinlock.h" //头文件引入的顺序也有问题
#include "proc.h"

最后还是改写了kvminit哈哈哈

void kvminit(){
  kernel_pagetable=kvminit_modify();
}

pagetable_t
kvminit_modify()
{
  // 为进程创建内核页表
  pagetable_t kernel_pagetable=(pagetable_t) kalloc();
  memset(kernel_pagetable, 0, PGSIZE);
  
  // 将 uart 寄存器映射到内核页表
  kvmmap(kernel_pagetable, UART0, UART0, PGSIZE, PTE_R | PTE_W);

  // 将 virtio mmio 磁盘接口映射到内核页表
  kvmmap( kernel_pagetable, VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);

  // 将 CLINT 映射到内核页表
  // kvmmap( kernel_pagetable, CLINT, CLINT, 0x10000, PTE_R | PTE_W);

  // 将 PLIC 映射到内核页表
  kvmmap( kernel_pagetable, PLIC, PLIC, 0x400000, PTE_R | PTE_W);

  // 将内核文本映射到内核页表
  kvmmap( kernel_pagetable, KERNBASE, KERNBASE, (uint64)etext-KERNBASE, PTE_R | PTE_X);

  // 将内核数据和物理 RAM 映射到内核页表
  kvmmap( kernel_pagetable, (uint64)etext, (uint64)etext, PHYSTOP-(uint64)etext, PTE_R | PTE_W);

  // 将 trampoline 映射到内核页表
  kvmmap( kernel_pagetable, TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);
  return kernel_pagetable;
}

自定义函数freewalk_kernel()

void freewalk_kernel(pagetable_t kernel_pagetable){
  for(int i=0; i<512; i++){
    pte_t pte=kernel_pagetable[i];
    if((pte&PTE_V)&&(pte&(PTE_R|PTE_W|PTE_X))==0){
      //说明这还不是底层pte
      uint64 child=PTE2PA(pte);
      freewalk_kernel((pagetable_t)child);
      kernel_pagetable[i]=0;
    }
    else if(pte&PTE_V){
      //要不要加上一句
      kernel_pagetable[i]=0;
      // printf("Something wrong when freewalk_kernel\n");
      // kfree((void*)(pagetable_t)(PTE2PA(pte)));
    }
  }
  kfree((void*)kernel_pagetable);
}
//释放进程的内核页表
void uvmfree_kernel(pagetable_t kernel_pagetable, uint64 sz, uint64 va){
  if(sz>0)
    uvmunmap(kernel_pagetable, va, PGROUNDUP(sz)/PGSIZE, 1);
  freewalk_kernel(kernel_pagetable);
}

 自定义函数user2kernel_mappages()

//把进程的用户页表复制到进程的内核页表中
//return 0表示正常退出,return -1表示异常
int user2kernel_mappages(pagetable_t kernel_pagetable, pagetable_t pagetable, uint64 va_start, uint64 va_end){
  pte_t *pte_userpg;
  va_start=PGROUNDUP(va_start); //虚拟地址的end其实就是普通页表的大小(p->size),涉及大小进行向上舍入
  uint64 i, pa;
  int perm;
  for(i=va_start; i<va_end; i+=PGSIZE){
    if((pte_userpg=walk(pagetable, i, 0))==0){
      printf("error: the pte is 0\n");
      return -1;      
    }

    //如果普通页表的这个pte不为空
    if(*pte_userpg & PTE_V){
      pa=PTE2PA(*pte_userpg);   //先得到这个pte的物理地址pa,再把物理地址pa和内核页表的pte进行映射
      perm=(*pte_userpg) & 0x3FF;  //不是pxmask而是0x3FF
      perm=perm & (~PTE_U);  //把PTE_U置为0
      if(mappages(kernel_pagetable, i, PGSIZE, pa, perm)<0){
        printf("Something wrong when call user2lkernel_mappages\n");
        return -1;
      }
    }

  }
  return 0;
}

allocproc()

  //准备一张内核页表

  // p->kernel_pagetable=kvminit();
  p->kernel_pagetable=kvminit_modify();
  char *pa = kalloc();
  if(pa == 0)
    panic("kalloc");
  // uint64 va = KSTACK((int) (p - proc));
  uint64 va = KSTACK((int) 0);
  kvmmap(p->kernel_pagetable,va, (uint64)pa, PGSIZE, PTE_R | PTE_W);
  p->kstack = va;
  // kvminithart();
  // printf("成功给进程分配了一张内核页表\n");

 proc_freekernelpagetable()

//释放进程的内核页表
//传入的va是kstack的地址
void proc_freekernelpagetable(pagetable_t kernel_pagetable,uint64 sz, uint64 va){
  // uvmunmap(kernel_pagetable, TRAMPOLINE, 1, 0);
  // uvmunmap(kernel_pagetable, TRAPFRAME, 1, 0);
  // printf("准备清除进程的内核页表\n");
  uvmfree_kernel(kernel_pagetable, sz, va);
}

 修改的userinit()

  //把第一个进程的用户页表映射到内核页表中
  printf("给第一个进程添加内核映射\n");
  user2kernel_mappages(p->kernel_pagetable, p->pagetable, 0, PGSIZE);
  printf("userinit() 未出错\n");

  // prepare for the very first "return" from kernel to user.
  p->trapframe->epc = 0;      // user program counter
  p->trapframe->sp = PGSIZE;  // user stack pointer

修改的fork()

  np->sz = p->sz;
  //复制父进程的内核页表给子进程 
  //这里不能uvmcopy(p->kernel_pagetable,np->kernel_pagetable,0,np->sz)
  if(user2kernel_mappages(np->kernel_pagetable,np->pagetable,0,np->sz)<0){
    freeproc(np);
    release(&np->lock);
    return -1;
  }

 主要修改的部分如上

最后贴一个运行通过的图吧

  • 35
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值