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)
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 = main
p->trapframe->sp = sp; // initial stack pointer
proc_freepagetable(oldpagetable, oldsz);
if(p->pid==1) vmprint(p->pagetable); //只要加这一行
return argc; // this ends up in a0, the first argument to main(argc, argv)
至此完成
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 registers
uvmmap(kpt, UART0, UART0, PGSIZE, PTE_R | PTE_W);
// virtio mmio disk interface
uvmmap(kpt, VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);
// CLINT
uvmmap(kpt, CLINT, CLINT, 0x10000, PTE_R | PTE_W);
// PLIC
uvmmap(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都没看
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的页表,得到pte
kernel_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对应的物理页表赋给pagetable
pagetable = (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= 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 registers
uvmunmap(pagetable, UART0, 1, 0);
// virtio mmio disk interface
uvmunmap(pagetable, VIRTIO0, 1, 0);
// CLINT
//uvmunmap(pagetable, CLINT, 0x10000/PGSIZE, 0);
// PLIC
uvmunmap(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的页表,得到pte
kernel_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报错原因,回想了下自己写的博客,就猜测是第一个进程的页表没有正确复制到内核页表,所以解决问题要多想想不同的办法