lab1、2不是太难,lab 3太变态了,github上记一下代码,源代码地址 :https://github.com/CodePpoi/mit-lab

参考博客 : https://blog.csdn.net/u013577996/article/details/109582932?utm_medium=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7EOPENSEARCH%7Edefault-1.base&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7EOPENSEARCH%7Edefault-1.base

每个进程都有自己的页表,里面有很多页表项(即PTE)

Print a page table

1. 在defs.h里面声明vmprint()

void vmprint(pagetable_t);

2. 在vm.c里面实现vmprint

void
vmprint_level(pagetable_t pagetable, uint64 level)
{// 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){// this PTE points to a lower-level page table.for(int j = 0; j < level; j++) {printf(".. ");}printf("..%d: pte %p pa %p\n", i, pte, child);vmprint_level((pagetable_t)child, level + 1);} else if(pte & PTE_V){for(int j = 0; j < level; j++) {printf(".. ");}printf("..%d: pte %p pa %p\n", i, pte, child);}}
}void
vmprint(pagetable_t pagetable) {printf("page table %p\n", pagetable);vmprint_level(pagetable,(uint64) 0);
}

3.在exec.c里面调用vmprint

p->trapframe->epc = elf.entry;  // initial program counter = mainp->trapframe->sp = sp; // initial stack pointerproc_freepagetable(oldpagetable, oldsz);if(p->pid==1) vmprint(p->pagetable);  //只要加这一行return argc; // this ends up in a0, the first argument to main(argc, argv)

至此完成

A kernel page table per process

1.在proc.h文件的proc结构体中添加pagetable_t:

pagetable_t kernel_pagetable

2. 找到allocproc,使用的命令是:

find . -name "*.c" | xargs grep "allocproc"

那么allocproc到底是干什么的呢,就是先去进程表找到一个unused进程,然后给该进程分配pid,trapframe 页表

3. 新增内核版本的kvminit,kvminit0

pagetable_t
kvminit0()
{pagetable_t kpt;kpt = (pagetable_t) kalloc();memset(kpt, 0, PGSIZE);// uart registersuvmmap(kpt, UART0, UART0, PGSIZE, PTE_R | PTE_W);// virtio mmio disk interfaceuvmmap(kpt, VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);// CLINTuvmmap(kpt, CLINT, CLINT, 0x10000, PTE_R | PTE_W);// PLICuvmmap(kpt, PLIC, PLIC, 0x400000, PTE_R | PTE_W);// map kernel text executable and read-only.uvmmap(kpt, KERNBASE, KERNBASE, (uint64)etext-KERNBASE, PTE_R | PTE_X);// map kernel data and the physical RAM we'll make use of.uvmmap(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.uvmmap(kpt, TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);return kpt;
}

用户页表和内核页表两者之间的区别是什么? 从上图可以看出,内核页表中虚拟地址和物理地址是一样的,不过有一点比较奇怪的就是,既然一样,那为啥不直接用物理地址? 我觉得是因为,用户页表用的是虚拟地址,为了统一,所以内核也用的虚拟地址,那么为啥用户地址不直接用物理地址呢?是为了隔离,让不同的程序之间不会相互干扰,而内核页表为什么要有这些mapping呢?

为什么用户的(物理)地址空间小,而内核的地址空间在比较大的位置, 看下图,右下侧CLINT下面,boot ROM上面的就是用户地址空间,而内核的kernel text(代码),kernel data(运行时需要的数据,比如变量值),就在物理地址0x80000000开始,所以看起来内核地址空间比较大

"在完成了虚拟到物理地址的翻译之后,如果得到的物理地址大于0x80000000会走向DRAM芯片,如果得到的物理地址低于0x80000000会走向不同的I/O设备。"

"当你对主板上电,主板做的第一件事情就是运行存储在boot ROM中的代码,当boot完成之后,会跳转到地址0x80000000,操作系统需要确保那个地址有一些数据能够接着启动操作系统。"

4. 以及uvmmap

void
uvmmap(pagetable_t pgt,uint64 va, uint64 pa, uint64 sz, int perm)
{if(mappages(pgt, va, sz, pa, perm) != 0)panic("uvmmap");
}

6. 然后让kernel/proc.c里的allocproc去调用kvminit0

//试图分配一张页表给p
p->kernel_pagetable = kvminit0();
//如果(比如因为内存不够),分配不成功if(p->kernel_pagetable == 0){//那么销毁这个进程freeproc(p);release(&p->lock);return 0;}

7. 因为所有的stack分配都是在procinit里面的,所以需要把procinit的一部分移动到到allocproc,先注释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();
}
char *pa = kalloc();if(pa == 0)panic("kalloc");uint64 va = KSTACK((int) (p - proc));uvmmap(p->kernel_pagetable, va, (uint64)pa, PGSIZE, PTE_R | PTE_W);p->kstack = va;memset(&p->context, 0, sizeof(p->context));p->context.ra = (uint64)forkret;

8. 修改kernel/proc.c里scheduler的内容,主要是设置kernel page table到satp寄存器

  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;w_satp(MAKE_SATP(p->kernel_pagetable));sfence_vma();swtch(&c->context, &p->context);kvminithart();// 必须加这一行,不然usertest会非常慢
/***其实就是对应下面注释报错的两行,但是kernel_pagetable在当前c文件没有,所以报错了
kvminithart()
{w_satp(MAKE_SATP(kernel_pagetable));sfence_vma();
}**/// Process is done running for now.// It should have changed its p->state before coming back.c->proc = 0;found = 1;}release(&p->lock);}
#if !defined (LAB_FS)if(found == 0) {intr_on();//w_satp(MAKE_SATP(kernel_pagetable));//sfence_vma(); 这两行会报错asm volatile("wfi");}

9.修改kernel/proc.c的freeproc

if(p->pagetable)proc_freepagetable(p->pagetable, p->sz);if(p->kernel_pagetable)proc_kernel_freepagetable(p->kernel_pagetable, p->kstack);p->pagetable = 0;p->kernel_pagetable = 0;

4.x都看完了

5.3 gdb 没看,后面5.x都没看

Simplify copyin/copyinstr

1.在defs.h里面添加copyin_new和copyinstr_new声明

int             copyin_new(pagetable_t, char *, uint64, uint64);
int             copyinstr_new(pagetable_t, char *, uint64, uint64);

2.将vm.c里面的copyin转换成copyin_new

return copyin_new(pagetable, dst, srcva, len);return copyinstr_new(pagetable, dst, srcva, len);

4. 在vm.c的kvminit0里面注释掉uvmmap,


//  uvmmap(kpt, CLINT, CLINT, 0x10000, PTE_R | PTE_W);

4.修改kernel/proc.c的fork函数

然后其实就是把用户空间的页表,复制到内核空间的页表中去,不过有一点我不明白,为什么在用户空间就是虚拟地址,而复制到内核空间,就直接是物理地址了?这搞毛啊, 这个原因就是,因为用户地址空间比较小,而对于地址小于PHYSTOP的虚拟地址,其虚拟地址与物理地址的值是一样的

"这意味着左侧低于PHYSTOP的虚拟地址,与右侧使用的物理地址是一样的。"

还有PLIC,这个到底是用来干嘛的?  终端控制器,不能被用户地址空间覆盖,但是为啥CLINT就能被覆盖??? 嗯 我的理解是,全局内核空间表需要这个,但是各个进程的内核空间表就不需要这个映射了 "因为要保证每个进程的内核页表中低于PLIC的地址,作为用户空间地址使用,而CLINT的地址小于PLIC地址,故每个进程的内核页表不能映射CLINT。"

pte_t *pte, *kernel_pte;// Allocate process.if((np = allocproc()) == 0){return -1;}// Copy user memory from parent to child.if(uvmcopy(p->pagetable, np->pagetable, p->sz) < 0){freeproc(np);release(&np->lock);return -1;}//这一行就是复制进程的pagetable到内核pagetable,并且置位PTU_U//不过我有个疑问,为啥父进程的kernel pagetable就不需要复制? //因为之前已经复制了?那么第一个进程肯定要复制进程的table到kernel pagetable对吧for (int j = 0; j < p->sz; j += PGSIZE) {pte =  walk(np->pagetable, j, 0); // 遍历p的页表,得到ptekernel_pte = walk(np->kernel_pagetable, j, 1); // 遍历kernel页表,如果没有该页,则分配一页*kernel_pte = (*pte) & ~PTE_U; // 内核必须设置~PTE_U, 不然内核无法使用}

先看看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) //walk应该就是遍历的意思
{if(va >= MAXVA)panic("walk");for(int level = 2; level > 0; level--) {pte_t *pte = &pagetable[PX(level, va)];if(*pte & PTE_V) { //如果PTE是valid有效的,那么把pte对应的物理页表赋给pagetablepagetable = (pagetable_t)PTE2PA(*pte);} else { // 如果没有找到有效页表if(!alloc || (pagetable = (pde_t*)kalloc()) == 0) //并且调用我们的方法没有让我们分配,或者分配的时候内存不够,导致kalloc失败,直接返回return 0;memset(pagetable, 0, PGSIZE); // 对新分配的页表全部置0*pte = PA2PTE(pagetable) | PTE_V; //将这个页表添加到PTE,置为valid}}return &pagetable[PX(0, va)]; // 返回物理页表对应的地址,PX到底是个啥,应该是放到第几级页表,va不是虚拟地址,我觉得是偏移量,相对于pagetable的偏移量,因为有可能传va=0进来,所以这个肯定是偏移量
}

再看看walkaddr, 其实就是通过虚拟地址va返回物理地址pa

// Look up a virtual address, return the physical address,
// or 0 if not mapped.
// Can only be used to look up user pages.
uint64
walkaddr(pagetable_t pagetable, uint64 va)
{pte_t *pte;uint64 pa;if(va >= MAXVA)return 0;pte = walk(pagetable, va, 0);if(pte == 0)return 0;if((*pte & PTE_V) == 0)return 0;if((*pte & PTE_U) == 0)return 0;pa = PTE2PA(*pte);return pa;
}

5.修改exec.c的文件,添加对sz不能超过PLIC的判断:

for(i=0, off=elf.phoff; i<elf.phnum; i++, off+=sizeof(ph)){if(readi(ip, 0, (uint64)&ph, off, sizeof(ph)) != sizeof(ph))goto bad;if(ph.type != ELF_PROG_LOAD)continue;if(ph.memsz < ph.filesz)goto bad;if(ph.vaddr + ph.memsz < ph.vaddr)goto bad;uint64 sz1;if((sz1 = uvmalloc(pagetable, sz, ph.vaddr + ph.memsz)) == 0)goto bad;if(sz1 >= PLIC) {goto bad;}

并且释放kenerl pagetable,复制新的user pagetable到kenel pagetable:

uvmunmap(p->kernel_pagetable, 0 , PGROUNDUP(oldsz)/PGSIZE, 0);
//proc_kernel_freepagetable(p->kernel_pagetable, p->kstack, oldsz);for (int j = 0; j < sz; j += PGSIZE) {pte =  walk(pagetable, j, 0);kernel_pte = walk(p->kernel_pagetable, j, 1);*kernel_pte = (*pte) & ~PTE_U;}

6. 修改srbk代码:

uint64
sys_sbrk(void)
{int addr;int n;struct proc * p = myproc();pte_t *pte, *kernel_pte;if(argint(0, &n) < 0)return -1;addr = myproc()->sz;if(addr + n >= PLIC) {return -1;}if(growproc(n) < 0)return -1;if(n > 0) {for (int j = addr; j < addr + n; j += PGSIZE) {pte =  walk(p->pagetable, j, 0);kernel_pte = walk(p->kernel_pagetable, j, 1);*kernel_pte = (*pte) & ~PTE_U;} else {for (j = addr - PGSIZE; j >= addr + n, j -= PGSIZE) {uvmunmap(p->kernel_pagetable, j, 1, 0);}//uvmdealloc(p->kernel_pagetable, addr, addr + n); //这一行可能有错误}}return addr;
}

//测试不过记得改掉uvmdealloc那一行

修改proc_kernel_freepagetable文件

void
proc_kernel_freepagetable(pagetable_t pagetable, uint64 kstack, uint64 sz)
{// uart registersuvmunmap(pagetable, UART0, 1, 0);// virtio mmio disk interfaceuvmunmap(pagetable, VIRTIO0, 1, 0);// CLINT//uvmunmap(pagetable, CLINT, 0x10000/PGSIZE, 0);// PLICuvmunmap(pagetable, PLIC, 0x400000/PGSIZE, 0);// map kernel text executable and read-only.uvmunmap(pagetable, KERNBASE, ((uint64)etext-KERNBASE)/PGSIZE, 0);// map kernel data and the physical RAM we'll make use of.uvmunmap(pagetable, (uint64)etext, (PHYSTOP-(uint64)etext)/PGSIZE, 0);uvmunmap(pagetable, TRAMPOLINE, 1, 0);uvmunmap(pagetable, 0, PGROUNDUP(sz)/PGSIZE, 0);// uvmunmap(pagetable, TRAPFRAME, 1, 0);uvmfree0(pagetable, kstack, 1);
}

在这些都搞完以后,启动的时候报错 kerneltrap, scause是000d,查了8.1对应的trap表,发现是load page fault,页错误

然后我想了下,之前自己写博客的时候也记录了一下"fork"的时候,有注释"第一个进程一定要把pagetable复制到kernel pagetable",而userinit就是第一个进程,所以我们应该在这个进程把pagetable复制到kernel pagetable,而且lab3的问题也有提醒我们

7.修改userinit代码如下:

pte_t *pte, *kernel_pte;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;for (int j = 0; j < p->sz; j += PGSIZE) {pte =  walk(p->pagetable, j, 0); // 遍历p的页表,得到ptekernel_pte = walk(p->kernel_pagetable, j, 1); // 遍历kernel页表,如果没有该页,则分配一页*kernel_pte = (*pte) & ~PTE_U; // 内核必须设置~PTE_U, 不然内核无法使用}

结果如下:

总结:

1. 解决问题的时候,边写博客记录下自己做了什么,是非常有必要的,因为这样会让你觉得做事很有条理,而且记录下来以后,对之前做了什么印象更深,那么排查问题的时候,更容易回想起之前的坑

2. 要解决问题,必须先明白问题是什么,其实我一开始就看了scause是000d,但误以为d代表的是12,Instruction page fault,后面仔细想了想是d =13. 然后才解决了kerneltrap的问题

3. 还有就是学习方法的问题,我是先学了trap那一章节,然后边写的Lab3,如何看scause寄存器的值,也是从trap那一章,才知道怎么看的,所以我觉得,学习的时候,不懂的直接跳过,多往后看,视野开阔了,回头看自然懂了(如果我只盯着pagetable章节,那看到scause就不知道是啥了)

4. 解决一个问题的办法其实会有很多种,不要陷在一个坑里出不来,就是比如上面scause的问题,一开始我本来打算通过gdb调试,去定位问题,这就不得不下载riscv64-elf-gdb并安装,还要配置,这就很麻烦了,但是后面仔细看了看scause报错原因,回想了下自己写的博客,就猜测是第一个进程的页表没有正确复制到内核页表,所以解决问题要多想想不同的办法

MIT6.S081 Lab3 Page tables相关推荐

  1. MIT6.S081 Lab3: page tables

    Print a page table 接收一个pagetable_t并把它指向的页表打印. 在kernel/def.h中增加函数声明void vmprint(void)并在kernel/vm.c中定义 ...

  2. 6.S081 Lab3 page tables

    6.S081 Lab3 page tables 未完成 文章目录 6.S081 Lab3 page tables 未完成 1. Print a page table ([easy](https://p ...

  3. 6.S081 Lab3 page tables 页表

    Speed up system calls 加速系统调用 有些操作系统会通过在用户空间和内核空间之间共享一些内存来加速系统调用.该实验题目要求在内核中保存一个映射到用户内存空间的结构体,该结构体中保存 ...

  4. 操作系统MIT6.S081:P7->Interrupts

    本系列文章为MIT6.S081的学习笔记,包含了参考手册.课程.实验三部分的内容,前面的系列文章链接如下 操作系统MIT6.S081:[xv6参考手册第1章]->操作系统接口 操作系统MIT6. ...

  5. 操作系统MIT6.S081:[xv6参考手册第4章]->Trap与系统调用

    本系列文章为MIT6.S081的学习笔记,包含了参考手册.课程.实验三部分的内容,前面的系列文章链接如下 操作系统MIT6.S081:[xv6参考手册第1章]->操作系统接口 操作系统MIT6. ...

  6. 操作系统MIT6.S081:Lab4->Trap

    本系列文章为MIT6.S081的学习笔记,包含了参考手册.课程.实验三部分的内容,前面的系列文章链接如下 操作系统MIT6.S081:[xv6参考手册第1章]->操作系统接口 操作系统MIT6. ...

  7. MIT6.S081 2021

    MIT6.S081 2021 环境配置 Xv6 and Unix utilities vscode格式化头文件排序问题 以地址空间的视角看待变量 其他 代码参考 system calls trace ...

  8. Mit6.S081学习记录

    Mit6.S081学习记录 前言 一.课程简述 二.课程资源 1,课程主页 2,参考书 3,实验环境 三.学习过程 Mit6.S081-实验环境搭建 Mit6.S081-GDB使用 Mit6.S081 ...

  9. MIT6.S081 Multithreading

    MIT6.S081 Multithreading xv6 book记录 Uthread Using threads Barrier xv6 book记录 阅读xv6 book之前,简要看一下<深 ...

最新文章

  1. LaneATT调试笔记
  2. python是不是特别垃圾-Python里的垃圾回收机制是什么意思,搞不懂?
  3. 同软件多个线程设置不同ip_中学校园广播-中学IP网络广播系统解决方案
  4. “adb”不是内部或外部命令,也不是可运行的程序或批处理文件(Win)与(Mac)——终极解决方案
  5. 《深入理解Android 卷III》第四章 深入理解WindowManagerService
  6. pandas.tseries.offsets
  7. Spring+hibernate+JSP实现Piano的数据库操作---4.配置文件
  8. Audio Hijack 4 for Mac(音频录制工具)
  9. java商品类别如何与价格对应_java编写程序实现某超市商品查价功能。从键盘输入商品号,显示对应的商品价格,以“n”结束查询。...
  10. 关于docker的日常操作(二)
  11. 利用SPA(SQL Performance Analyzer)对比两个SQL Tuning Set
  12. 如何将两张图片合成一张?
  13. 初步使用ligerui
  14. 亚马逊EC2服务器链接方式
  15. 停止精神内耗 每日分享
  16. Latex 参考文献格式
  17. kaggle竞赛:泰坦尼克幸存者预测
  18. 《途客圈创业记:不疯魔,不成活》一一2.7 愿景和使命
  19. FPGA设计开发(基础课题):74LS160计数器芯片设计
  20. 申论考试如何获得高分?

热门文章

  1. PROCESS_MEMORY_COUNTERS
  2. 球坐标系和直角坐标系转换
  3. CW32系列模数转换器(ADC)
  4. HttpClient-v4.5官方文档翻译
  5. C语言:会员管理系统
  6. sga的组成 简述oracle_Oracle SGA多个部分的组成
  7. 电动汽车传动NVH总结
  8. 一,survey:person re-identification:past,present and future
  9. MapStruct Map 转换成对象
  10. 电脑技巧:Win10任务管理器知识介绍,赶快来看一看吧