lab_3
1 achieve isolation
把类似shell、cat、 …程序 boxes around these
让它们不会互相影响, so that they cant really affect each other
we want to be completely independent of the kernel, the operating system
如果一个应用程序出现意外,或者发生bad thing,不会影响其他操作系统
so that if an application does something either accidentally bad or maliciously bad, that it doesn‘t really affect the operating system
如果没用内存隔离,那么假设在同一物理内存空间,有着shell 、 cat、 … 程序,那么如果cat突然出现操作了shell的内存空间,就会导致shell出错;
虚拟地址->物理地址映射
如图,虚拟地址分为了2部分:Index索引, Offest偏移量
12位偏移量对应着物理地址偏移量,一页的物理地址为4096个字节,即2的12次方
when the MMU does the translation, it takes the index, indexes into the map
当MMU转换时,它使用索引在映射中
把偏移量加到基础页面上,得到真正的物理地址
64位的RSIC-V虚拟内存只用了39位,还有25位没使用
但是物理内存index为44,总共使用了56位(这个具体位数是由硬件设计师决定的),比单个 虚拟地址空间大
每个进程都有自己的映射;如果每个进程都有完整的页表,那么会很快填满物理内存,所以它不是一个2的27次方索引,而是一个多级结构:
如上图,Index的27位,分3个页目录(这3个页是物理页),每个目录9位;
上述的3个页目录;每个页目录存放一页,所以总共是4096个字节,同时每个条目是64位,8个字节,也就是说每页存放着4096/8 = 512个条目(entries)
三级页表虚拟地址只用后面39位,前面27位为3级映射,每个9位,最后12位为对应的实际物理地址的offest;1.首先,从 SATP(Supervisor Address Translation and Protection)寄存器中获取顶级页表的物理地址这个页表包含了 512 个条目每个条目是一个页表条目(PTE)(page table entry)虚拟地址的高 9 位(VPN[2])用于在这个顶级页表中索引,以找到对应的 PTE如果这个 PTE 的有效位(V)是 1,那么它的 bits [53:10] 包含了下一级页表(第二级页表)的物理页号(PPN)2.第二级页表:根据第一级页表中的 PPN,找到第二级页表的物理地址第二级页表同样包含了 512 个条目虚拟地址的次高 9 位(VPN[1])用于在第二级页表中索引,以找到对应的 PTE如果这个 PTE 的有效位(V)是 1,那么它的 bits [53:10] 又包含了下一级页表(第三级页表)的物理页号(PPN)3.第三级页表:根据第二级页表中的 PPN,找到第三级页表的物理地址第三级页表也包含了 512 个条目虚拟地址的最低 9 位(VPN[0])用于在第三级页表中索引,以找到对应的 PTE如果这个 PTE 的有效位(V)是 1,那么它的 bits [53:10] 包含了最终物理页的物理页号(PPN)
q:为什么页面目录保存的是物理页面编号,而不是虚拟地址?
a:因为我们要查找内存,在内存中查找下一个directory
q:satp存放虚拟地址还是物理地址
a:物理地址,satp指向第一个page directory,页目录就是物理地址
it looks like that you know any memory reference to virtual address basically requires three memory reach and so that seenms expensiveso, what happens in pratice where almost all every procesor does is it has a cache sitting on the side, that contains recently used translationsthat is called translation loo-aside buffer (TLB)// 对虚拟地址的使用,每次都需要3次访问内存,这是比较大的开销, 实际的处理器中,会在边上放一个缓存,包含着最近使用的转换
这个被称作为 TLB
它只是保存了页表条目或PTE条目的缓存当cpu通过虚拟地址访问物理内存,通过3级映射;
TLB会保存[VA(虚拟地址), PA(物理地址)]的映射,下次访问时可以直接从TLB获取物理地址,不需要3级映射了
MMU是硬件的一部分,3级映射都是由硬件实现的,不是由操作系统
虚拟空间和物理空间映射:
右边的Physical Address是由硬件师设计的
可以看到,设计师从0x80000000开始,指向DRAM芯片,0x8000000向下可能是其他一些IO设备的地址
// add a mapping to the kernel page table.
// only used when booting.
// does not flush TLB or enable paging.
void
kvmmap(uint64 va, uint64 pa, uint64 sz, int perm)
{if(mappages(kernel_pagetable, va, sz, pa, perm) != 0)panic("kvmmap");
}
继续查看walk函数,首先:
#define PXMASK 0x1FF // 9 bits
#define PXSHIFT(level) (PGSHIFT+(9*(level)))
#define PX(level, va) ((((uint64) (va)) >> PXSHIFT(level)) & PXMASK)
上面说的虚拟地址3个9位索引,12位的页偏移量
PXMASK宏定义为0x1 1111 1111 9位全为1的掩码,为了下面提取9位索引的
PXSHIFT(level) (PGSHIFT+(9*(level))) PGSHIFT定义12,是末尾的12位的偏移量,level是想提取第几级的9位索引,如果2就是top级的9位;
PX(level, va) ((((uint64) (va)) >> PXSHIFT(level)) & PXMASK) 就是将VA虚拟地址,如果先左移多少位,然后提取9位索引;
然后 walk函数:
pte_t *
walk(pagetable_t pagetable, uint64 va, int alloc)
{if(va >= MAXVA)panic("walk");for(int level = 2; level > 0; level--) {pte_t *pte = &pagetable[PX(level, va)];if(*pte & PTE_V) {pagetable = (pagetable_t)PTE2PA(*pte);} else {if(!alloc || (pagetable = (pde_t*)kalloc()) == 0)return 0;memset(pagetable, 0, PGSIZE);*pte = PA2PTE(pagetable) | PTE_V;}}return &pagetable[PX(0, va)];
}
1、
pte_t *pte = &pagetable[PX(level, va)];pagetable是一个指向页表开始的指针, 页表是一个数组, 存放着9位个上述Page Directory即512个;pte_t* 指向一个Page Directory中的 一个entry(PTE);即44位PPN,10位标志位
2、
#define PTE2PA(pte) (((pte) >> 10) << 12)
先从top级的9位找到页表项,然后通过PTE2PA,右移10位去除标志位,左移12位,补上偏移量
如果PTE_V无效,就重新分配3层遍历后,返回最后一层的PTE的指针
/kernel/vm.c
虚拟内存初始化:
void
kvminit()
{kernel_pagetable = (pagetable_t) kalloc();memset(kernel_pagetable, 0, PGSIZE);// uart registerskvmmap(UART0, UART0, PGSIZE, PTE_R | PTE_W);// virtio mmio disk interfacekvmmap(VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);...
}
主要是kvmmap函数,该函数是将虚拟地址和物理地址进行映射;
// add a mapping to the kernel page table.
// only used when booting.
// does not flush TLB or enable paging.
void
kvmmap(uint64 va, uint64 pa, uint64 sz, int perm)
{if(mappages(kernel_pagetable, va, sz, pa, perm) != 0)panic("kvmmap");
}
定义虚拟地址起始位置,物理地址起始位置,
以及sz为要映射多少个字节
// Create PTEs for virtual addresses starting at va that refer to
// physical addresses starting at pa. va and size might not
// be page-aligned. Returns 0 on success, -1 if walk() couldn't
// allocate a needed page-table page.
int
mappages(pagetable_t pagetable, uint64 va, uint64 size, uint64 pa, int perm)
{uint64 a, last;pte_t *pte;a = PGROUNDDOWN(va);last = PGROUNDDOWN(va + size - 1);for(;;){if((pte = walk(pagetable, a, 1)) == 0)return -1;if(*pte & PTE_V)panic("remap");*pte = PA2PTE(pa) | perm | PTE_V;if(a == last)break;a += PGSIZE;pa += PGSIZE;}return 0;
}
通过mappages看出,a是虚拟地址对齐位置,last是要对映射到最后的地址位置;
walk函数上面分析了,是遍历3级找到最后的pte,如果某一级无效(PTE_V为0),还会为其分配一页(同时只将PTE_V设置为1),
同时从上面的代码中,*pte = PA2PTE(pa) | perm | PTE_V; 看出来,只有walk后的pte,也就是最后一级的pte才被设置了其他标志位
综上2条,可以看出,不是最后一级的pte,其他的标志位都没有被立起来
将物理地址通过PA2PTE函数转换为虚拟地址最后一级的pte,同时添加给定的标志位
这样就完成了映射,同时可以看出,该设计是线性连续映射的
a kernel page table per process
为每一个新进程单独分配一个属于它的内核页
1、/kernel/proc.h 给进程结构体添加一个成员变量:pagetable_t kpagetable; 每个进程专属内核页
2、/kernel/vm.c 中,模仿kvminit(), kvmmap()函数,生成ukvminit()函数,即用户专属内核页初始化
3、/kernel/proc.c中,在分配进程函数中,添加对上述的专属内核页初始化的 调用
4、把进程中的kernel stack在专属内核页上,完成其和物理地址的映射
1.从上面的虚拟空间和物理空间的映射图看出,里面有多个内核栈,如图中的Kstack 0; Kstack 1 ... 每个进程的结构体中都有一个kstack的成员变量
2.从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();
}
看出uint64 va = KSTACK((int) (p - proc));其中(p - proc)就是进程在进程结构体数组中的下标,通过宏定义,参考虚拟空间到物理空间图,计算出每个进程内核栈的虚拟内存,通过这里已经对其进程物理地址映射了
3.所以,我们现在在新进程分配中,清空对应的物理地址,然后,将虚拟地址和物理地址,在我们设置的专属内核页中进行映射uint64 va = KSTACK((int) (p - proc));pte_t pa = kvmpa(va);memset((void *)pa, 0, PGSIZE); // 刷新清空kernel stackukvmmap(p->kpagetable, va, (uint64)pa, PGSIZE, PTE_R | PTE_W);p->kstack = va
5、在scheduler切换进程的时候,切换新进程的内核页表,刷新页表映射base,最后切换全局的kernel page
void
scheduler(void)
{struct proc *p;struct cpu *c = mycpu();c->proc = 0;for(;;){// Avoid deadlock by ensuring that devices can interrupt.intr_on();int found = 0;for(p = proc; p < &proc[NPROC]; p++) {acquire(&p->lock);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->kpagetable));sfence_vma();swtch(&c->context, &p->context);// Process is done running for now.// It should have changed its p->state before coming back.c->proc = 0;kvminithart();found = 1;}release(&p->lock);}
#if !defined (LAB_FS)if(found == 0) {intr_on();asm volatile("wfi");}
#else;
#endif}
}
6、销毁进程时,回收内核页表;只需要回收 因为我们在创建每个进程的内核虚拟地址和物理空间映射,所产生的三级映射所产生的物理页,不需要回去其他物理页,因为其他物理页,如设备io是全局共享的
void
ukvmunmap(pagetable_t pagetable, uint64 va, uint64 npages)
{uint64 a;pte_t *pte;if((va % PGSIZE) != 0)panic("ukvmunmap: not aligned");for(a = va; a < va + npages*PGSIZE; a += PGSIZE){if((pte = walk(pagetable, a, 0)) == 0)goto clean;if((*pte & PTE_V) == 0)goto clean;if(PTE_FLAGS(*pte) == PTE_V)panic("ukvmunmap: not a leaf");clean:*pte = 0;}
}这个用来删除第三级即叶子节点的物理页
// 递归删除前2级的物理页
void
ufreewalk(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);ufreewalk((pagetable_t)child);pagetable[i] = 0;}pagetable[i] = 0;}kfree((void*)pagetable);
}void freeprockvm(struct proc* p) {pagetable_t kpagetable = p->kpagetable;// reverse order of allocation// 按分配顺序的逆序来销毁映射, 但不回收物理地址ukvmunmap(kpagetable, p->kstack, PGSIZE/PGSIZE);ukvmunmap(kpagetable, TRAMPOLINE, PGSIZE/PGSIZE);ukvmunmap(kpagetable, (uint64)etext, (PHYSTOP-(uint64)etext)/PGSIZE);ukvmunmap(kpagetable, KERNBASE, ((uint64)etext-KERNBASE)/PGSIZE);ukvmunmap(kpagetable, PLIC, 0x400000/PGSIZE);ukvmunmap(kpagetable, CLINT, 0x10000/PGSIZE);ukvmunmap(kpagetable, VIRTIO0, PGSIZE/PGSIZE);ukvmunmap(kpagetable, UART0, PGSIZE/PGSIZE);ufreewalk(kpagetable);
}在freeproc中添加充值内核栈和内核页
copyin_new
要求:原来的copyin函数是通过用户的虚拟地址加上字节长度,找到相应的物理地址中的数据,然后传送到内核和缓冲中;
在用户空间的地址 没有 被映射到内核页表时,内核尝试访问用户虚拟地址会导致硬件的异常,因为MMU会检测到非法的地址访问并触发错误
现在该lab要求就是将用户地址部分,映射到内核中,这样内核可以直接访问用户空间
1、添加复制函数
void
u2kvmcopy(pagetable_t pagetable, pagetable_t kernelpt, uint64 oldsz, uint64 newsz){pte_t *pte_from, *pte_to;oldsz = PGROUNDUP(oldsz);for (uint64 i = oldsz; i < newsz; i += PGSIZE){if((pte_from = walk(pagetable, i, 0)) == 0)panic("u2kvmcopy: src pte does not exist");if((pte_to = walk(kernelpt, i, 1)) == 0)panic("u2kvmcopy: pte walk failed");uint64 pa = PTE2PA(*pte_from);uint flags = (PTE_FLAGS(*pte_from)) & (~PTE_U);*pte_to = PA2PTE(pa) | flags;}
}
2、在内核更改进程的用户映射的每一处 (fork(), exec(), 和sbrk()),都复制一份到进程的内核页表
int
exec(char *path, char **argv){...sp = sz;stackbase = sp - PGSIZE;// 添加复制逻辑u2kvmcopy(pagetable, p->kernelpt, 0, sz);// Push argument strings, prepare rest of stack in ustack.for(argc = 0; argv[argc]; argc++) {...
}int
fork(void){...// Copy user memory from parent to child.if(uvmcopy(p->pagetable, np->pagetable, p->sz) < 0){freeproc(np);release(&np->lock);return -1;}np->sz = p->sz;...// 复制到新进程的内核页表u2kvmcopy(np->pagetable, np->kernelpt, 0, np->sz);...
}int
growproc(int n)
{uint sz;struct proc *p = myproc();sz = p->sz;if(n > 0){// 加上PLIC限制if (PGROUNDUP(sz + n) >= PLIC){return -1;}if((sz = uvmalloc(p->pagetable, sz, sz + n)) == 0) {return -1;}// 复制一份到内核页表u2kvmcopy(p->pagetable, p->kernelpt, sz - n, sz);} else if(n < 0){sz = uvmdealloc(p->pagetable, sz, sz + n);}p->sz = sz;return 0;
}