天天看点

操作系统实验Mit6.S081笔记 Lab3: page tablesPrint a page table(easy)A kernel page table per process (hard)Simplify copyin/copyinstr (hard)

实验时参考了以下两篇博客:

https://blog.csdn.net/u013577996/article/details/109582932?ops_request_misc=&request_id=&biz_id=102&utm_term=xv6%206.S081%20Lab3&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduweb~default-1-.first_rank_v2_pc_rank_v29&spm=1018.2226.3001.4187

https://blog.csdn.net/laplacebh/article/details/117933464?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522163229134416780264072951%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=163229134416780264072951&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduend~default-2-117933464.pc_search_insert_js_new&utm_term=xv6+6.S081+Lab3

Print a page table(easy)

要求:

定义一个名为vmprint()的函数。 它应该接受一个pagetable_t参数,并以下面描述的格式打印该可分页对象。 在exec.c中,在return argc之前插入if(p->pid==1) vmprint(p->pagetable),打印第一个进程的页表。

提示:

把vmprint()放在kernel/vm.c中。

使用kernel/riscv.h文件末尾的宏。

可使用函数freewalk。

在kernel/defs.h中定义vmprint的原型,这样就可以从exec.c中调用它。

在printf调用中使用%p打印完整的64位十六进制pte和地址,如示例所示。

第一行显示vmprint的参数。 在这之后有一行用于每个PTE,包括引用树中更深层次的页表页的PTE。 每个PTE行通过若干“…”进行缩进,表示其在树中的深度。 每个PTE行显示其页表页中的PTE索引、PTE位和从PTE中提取的物理地址。不要打印无效的PTE。 在上面的示例中,顶级页表页具有条目0和255的映射。 条目0的下一层只映射了索引0,而索引0的底层映射了条目0、1和2。

您的代码可能会发出与上面所示不同的物理地址。 表项个数和虚拟地址个数必须相同。

操作系统实验Mit6.S081笔记 Lab3: page tablesPrint a page table(easy)A kernel page table per process (hard)Simplify copyin/copyinstr (hard)

解释:

在vm.c中写一个递归函数vmprint,用于打印页表

pte是页表的入口地址(pagetable entry)

使用…表示页表间的层次关系

第一行显示vmprint的参数,也就是直接打印类型为pagetable_t的参数

在每一行打印索引、PTE和从PTE中提取的物理地址

记得最后要在def.h中添加vmprint的原型,并在exe.c中调用vmprint

代码:

/*
vm.c
*/
//不知道哪里需要用到提示中提到的freework函数
void 
vmprintlevels(pagetable_t pagetable,int level){
    for (int i = 0; i < 512; i++)
    {
        pte_t pte=pagetable[i];//pagetable_t是一个uint64指针 pte_t是uint64
        if((pte&PTE_V)&&(pte&(PTE_R|PTE_W|PTE_X))==0){//PTE_V表示有效 其它几个位全为0说明存的是下一个页表的地址,而不是一个具体要用的空间的地址 各个位在riscv.h第329行
            uint64 child =PTE2PA(pte);//PTE2PA函数在riscv.h第338行
            for(int j=0;j<level;j++){
                printf("..");//根据层数打印..
            }
            printf("..%d: pte %p pa %p\n",i,pte,child);//注意pte前要加空格,不然过不了测试
            vmprintlevels((pagetable_t)child,level+1);
        }else if(pte&PTE_V){
            uint64 child =PTE2PA(pte);
            for(int j=0;j<level;j++){
                printf("..");
            }
            printf("..%d: pte %p pa %p\n",i,pte,child);//注意pte前要加空格,不然过不了测试
        }
    }
}

void
vmprint(pagetable_t pagetable){
    printf("page table %p\n",pagetable);
    vmprintlevels(pagetable,0);
}
           

A kernel page table per process (hard)

要求:

Xv6有一个单独的内核页表,每当它在内核中执行时都会使用它。 内核页表直接映射到物理地址,因此内核虚拟地址x映射到物理地址x。Xv6还为每个进程的用户地址空间有一个单独的页表,只包含该进程的用户内存的映射,从虚拟地址0开始。 因为内核页表不包含这些映射,用户地址在内核中是无效的。 因此,当内核需要使用在系统调用中传递的用户指针时(例如,传递给write()的缓冲区指针),内核必须首先将指针转换为物理地址。 本节和下一节的目标是允许内核直接解除对用户指针的引用。

您的第一个任务是修改内核,以便每个进程在内核中执行时使用自己的内核页表副本。 修改struct proc以维护每个进程的内核页表,并修改调度器以在切换进程时切换内核页表。 对于这一步,每个进程的内核页表应该与现有的全局内核页表相同。 如果用户测试运行正确,您将通过实验室的这一部分。

阅读作业开始时提到的章节和代码; 了解了虚拟内存的工作原理后,正确修改虚拟内存代码将变得更容易。 页表设置中的错误可能由于缺少映射而导致陷阱,可能导致负载和存储影响物理内存的意外页,并可能导致从错误的内存页执行指令。

提示:

在struct proc中添加一个用于进程内核页表的字段。

为新进程生成内核页表的一个合理方法是实现kvminit的修改版本,它生成一个新的页表,而不是修改kernel_pagetable。 你需要从allocproc调用这个函数。

确保每个进程的内核页表都有对应该进程内核堆栈的映射。 在未修改的xv6中,所有内核堆栈都在procinit中设置。 您需要将这些功能的一部分或全部移动到allocproc。

修改scheduler()将进程的内核页表加载到内核的satp寄存器中(参见kvminithart获取灵感)。 不要忘记在调用w_satp()之后调用sfence_vma()。

当没有进程在运行时,Scheduler()应该使用kernel_pagetable。

在freeproc中释放一个进程的内核页表。

您需要一种方法来释放页表,而不同时释放叶物理内存页。

Vmprint在调试页表时可能会派上用场。

可以修改xv6函数或添加新函数; 您可能至少需要在kernel/vm.c和kernel/proc.c中这样做。 (但是,不要修改kernel/vmcopyin.c、kernel/stats.c、user/usertests.c和user/stats.c。)

缺少页表映射可能会导致内核遇到页错误。 它将打印一个错误,其中包含sepc=0x00000000XXXXXXXX。 在“kernel/kernel.asm”中查找“XXXXXXXX”,查找故障发生的位置。

代码:

/*
proc.h

给每个进程加内核页表,在proc.h第100行后加入 
*/
  pagetable_t kpagetable;      //内核页表
           
/*
vm.c

仿照kvminit(),返回一个页表,用mappages()替代kvmmap(),kpagetable作为mappages()的第一个参数,第二个参数va和第四个参数pa均为kvminit()中对应的第一个参数的值。即映射到内核页表。
*/
pagetable_t
kvminit_proc()
{
  pagetable_t kpagetable = (pagetable_t) kalloc();
  memset(kpagetable, 0, PGSIZE);

  // uart registers
  mappages(kpagetable, UART0, PGSIZE, UART0, PTE_R | PTE_W);

  // virtio mmio disk interface
  mappages(kpagetable, VIRTIO0, PGSIZE, VIRTIO0, PTE_R | PTE_W);

  // CLINT
  //kvmmap(CLINT, CLINT, 0x10000, PTE_R | PTE_W);

  // PLIC
  mappages(kpagetable, PLIC, 0x400000, PLIC, PTE_R | PTE_W);

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

  // map kernel data and the physical RAM we'll make use of.
  mappages(kpagetable, (uint64)etext, PHYSTOP-(uint64)etext, (uint64)etext, PTE_R | PTE_W);

  // map the trampoline for trap entry/exit to
  // the highest virtual address in the kernel.
  mappages(kpagetable, TRAMPOLINE, PGSIZE, TRAMPOLINE, PTE_R | PTE_X);
  
  return kpagetable;
}
           

在proc.c第93行allocproc()中,调用上面写的kvminit_proc()。

/*
proc.c

创建一个内核页表,并仿照procinit()分配一个内核栈,注意映射时用mappages()替代kvmmap()
*/
p->kpagetable=kvminit_proc();
char *pa=kalloc();
if(pa==0){
    panic("kalloc");
}
uint64 va=TRAMPOLINE-2*PGSIZE;
mappages(p->kpagetable,va,PGSIZE,(uint64)pa,PTE_R|PTE_W);
p->kstack=va;
           
/*
proc.c

注释掉procinit()中的分配内核栈的部分
*/
void
procinit(void)
{
  struct proc *p;
  
  initlock(&pid_lock, "nextpid");
  for(p = proc; p < &proc[NPROC]; p++) {
      initlock(&p->lock, "proc");

      // 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));
      //kvmmap(va, (uint64)pa, PGSIZE, PTE_R | PTE_W);
      //p->kstack = va;
  }
  kvminithart();
}
           
/*
proc.c

修改scheduler(),在调用swtch()之前,将进程的内核页表加载到内核的satp寄存器中(参见kvminithart())。 
在调用w_satp()之后调用sfence_vma()。
*/
        p->state = RUNNING;
        c->proc = p;
        w_satp(MAKE_SATP(p->kpagetable));
        sfence_vma();
        swtch(&c->context, &p->context);
        kvminithart();
           
/*
vm.c

在kernel/vm.c,kvmpa()方法会在进程执行期间调用,此时需要修改为获取进程内核页表,而不是全局内核页表
*/
  pte = walk(myproc()->kpagetable, va, 0);

  //头部添加引用
  #include "spinlock.h"
  #include "proc.h"
           
/*
proc.c
s
修改freeproc(),在调用proc_freepagetable()前,释放用户进程对应的内核页表。
需要编写一个proc_freekpagetable()于释放内核页表。
*/
if(p->kpagetable){
    procfreekpagetable(p->kpagetable,p->pstack,p->sz);
}
//释放kpagetable
void
proc_freekpagetable(pagetable_t pagetable,uint64 kstack,uint64 sz){
    uvmunmap(pagetable,UART0,1,0);
    uvmunmap(pagetable,VIRTIO0,1,0);
    uvmunmap(pagetable,PLIC,0x400000/PGSIZE,0);
    uvmunmap(pagetable,KERNBASE,((uint64)etext-KERNBASE)/PGSIZE,0);
    uvmunmap(pagetable,(uint64)etext,(PHYSTOP-(uint64)etext)/PGSIZE,0);
    uvmunmap(pagetable,TRAMPOLINE,1,0);
    uvmunmap(pagetable,kstack,1,1);
    uvmfree(pagetable,0);
}
           

Simplify copyin/copyinstr (hard)

要求:

内核的copyin函数读取用户指针指向的内存。 它通过将它们转换为物理地址来实现这一点,内核可以直接解除对物理地址的引用。 它通过在软件中遍历进程页表来执行转换。 在本部分中,您的任务是向每个进程的内核页表(在前一节中创建的)添加用户映射,以便允许copyin(和相关的字符串函数copyinstr)直接解除对用户指针的引用。

在kernel/vm.c中调用copyin_new(在kernel/vmcopyin.c中定义)来替换copyin的主体; 对copyinstr和copyinstr_new执行同样的操作。 向每个进程的内核页表添加用户地址的映射,以便copyin_new和copyinstr_new工作。 如果用户测试运行正确,并且所有make grade测试都通过,您就通过了这项作业。

这种方案依赖于用户的虚拟地址范围不与内核用于其自己的指令和数据的虚拟地址范围重叠。 Xv6对用户地址空间使用从0开始的虚拟地址,幸运的是内核的内存从更高的地址开始。 但是,这个方案限制用户进程的最大大小小于内核的最低虚拟地址。 内核启动后,该地址在xv6中为0xC000000,即PLIC寄存器的地址; 参见kernel/vm.c、kernel/memlayout.h中的kvminit(),文本中如图3-4所示。 您需要修改xv6以防止用户进程增长到大于PLIC地址。

解释为什么在copyin_new()中第三个测试srcva + len < srcva是必要的:为前两个测试失败的srcva和len赋值(即,它们不会导致返回-1),但第三个测试为真(导致返回-1)。

提示:

首先使用copyin_new调用替换copyin(),并使其工作,然后再使用copyinstr。

在内核更改进程的用户映射时,以相同的方式更改进程的内核页表。 这些点包括fork()、exec()和sbrk()。

不要忘记在userinit的内核页表中包含第一个进程的用户页表。

在进程的内核页表中,用户地址的pte需要什么权限? (带有PTE_U设置的页面在内核模式下无法访问。)

不要忘记上述的PLIC限制。

代码:

/*
def.h

在virtio_disk.c的后,添加vmcopyin.c中copyin_new()和copyinstr_new()的声明
*/
//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);
}
           
/*
vm.c

将进程中用户页表复制到内核页表中
首先写一个通用函数,在kernel/vm.c里
其中492行意思是只拷贝所存储的物理地址,即共享同一片物理地址,标志位去掉PTE_W和PTE_X是因为内核只对改地址进行读取,不会修改。
PTE_U是RISC-V系统独有的,有这个标志表示内核不能访问这个页面,因此也需要去掉。但是可以通过设置sstatus寄存器某一位使得内核可以访问带有PTE_U标志的页表。
*/
void
vmcopypage(pagetable_t pagetable,pagetable_t kpagetable,uint64 start,uint64 sz){
    for(uint64 i=start;i<start+sz;i+=PGSIZE){
        pte_t* pte=walk(pagetable,i,0);
        pte_t* kpte=walk(kpagetable,i,1);
        if(!pte||!kpte){
            panic("vmcopypage");
        }
        *kpte=(*pte)&~(PTE_U|PTE_W|PTE_X);
    }
}
//并在def.h中vm.c下添加声明
void            vmcopypage(pagetable_t,pagetable_t,uint64,uint64);
           
/*
proc.c

fork()中,在np->sz = p->sz之后调用
*/
vmcopypage(np->pagetable,np->kpagetable,0,np->sz);
           
/*
exec.c

exec()中,在p->trapframe->a1 = sp之前调用,复制进程页表到进程内核页表
*/
  //释放旧进程内核页表映射
  uvmunmap(p->kpagetable,0,PGROUNDUP(oldsz)/PGSIZE,0);
  //复制进程页表到进程内核页表
  vmcopypage(pagetable,p->kpagetable,0,sz);  
           
/*
sysproc.c

修改sys_sbrk()。因为内存增加时需要复制到内核页表,内存减少需要解除映射。
这里把55行int改为uint64可能会陷入无限循环,出现panic: uvmunmap: walk,某个测试点过不了.
*/
uint64
sys_sbrk(void)
{
  int addr;
  int n;
  struct proc *p=myproc();
  if(argint(0, &n) < 0)
    return -1;
  addr = p->sz;
  if(growproc(n) < 0)
    return -1;
  if(n>0){
    vmcopypage(p->pagetable,p->kpagetable,addr,n);
  }else{
      for(int j=addr-PGSIZE;j>=addr+n;j-=PGSIZE){
          uvmunmap(p->kpagetable,j,1,0);
      }
  }
  return addr;
}
           
/*
proc.c

修改userinit(),在p->sz = PGSIZE之后调用
*/
vmcopypage(p->pagetable,p->kpagetable,0,PGSIZE);
           
/*
exec.c

在iunlockput(ip)前的循环末尾添加。限制内存大小PLIC
*/
    if(sz1>=PLIC)
      goto bad;
           
/*
proc.c

修改proc_free_kernel_pagetable方法,取消进程内核页表地址映射。
*/
void
proc_freekpagetable(pagetable_t pagetable,uint64 kstack,uint64 sz){
    uvmunmap(pagetable,UART0,1,0);
    uvmunmap(pagetable,VIRTIO0,1,0);
    uvmunmap(pagetable,PLIC,0x400000/PGSIZE,0);
    uvmunmap(pagetable,KERNBASE,((uint64)etext-KERNBASE)/PGSIZE,0);
    uvmunmap(pagetable,(uint64)etext,(PHYSTOP-(uint64)etext)/PGSIZE,0);
    uvmunmap(pagetable,TRAMPOLINE,1,0);
    uvmunmap(pagetable,0,PGROUNDUP(sz)/PGSIZE,0);//实验3.3添加 3.2不添加
    uvmunmap(pagetable,kstack,1,1);
    //vmprint(pagetable);
    uvmfree(pagetable,0);
}
           

继续阅读