标签:
这次操作系统实验感觉还是比较难的,除了因为助教老师笔误引发的2个错误外,还有一些关键性的理解的地方感觉还没有很到位,这些天一直在不断地消化、理解Lab3里的内容,到现在感觉比Lab2里面所蕴含的内容丰富很多,也算是有所收获,和大家分享一下我个人的一些看法与思路,如果有错误的话请指正。
首先第一部分我觉得比较关键的是对于一些非常关键的函数的理解与把握,这些函数是我们本次实验的精华所在,虽然好几个实验都不需要我们自己实现,但是这些函数真的是非常厉害!有多厉害,呆会就知道了。
首先是从第一个我们要填的函数说起吧:
1 void 2 env_init(void) 3 { 4 int i; 5 6 /*precondition: envs pointer has been initialized at mips_vm_init, called by mips_init*/ 7 /*1. initial env_free_list*/ 8 LIST_INIT(&env_free_list); 9 //step 1; 10 /*2. travel the elements in ‘envs‘, initial every element(mainly initial its status, mark it as free) and inserts them into 11 the env_free_list. attention :Insert in reverse order */ 12 for(i=NENV-1;i>=0;i--){ 13 envs[i].env_status = ENV_FREE; 14 LIST_INSERT_HEAD(&env_free_list,envs+i,env_link); 15 } 16 17 }
以上是env_init的实现。其实这个函数没什么太多好说的,就是初始化env_free_list,然后按逆序插入envs[i]。
这里唯一值得并需要引起警惕的是逆序,因为我们使用的是LIST_INSERT_HEAD这个宏,任何一个对齐有所了解的人应该都知道,这个宏每次都会将一个结点插入,变成链表的第一个可用结点,而我们在取用的时候是使用LIST_FIRST宏来取的,所以如果这里写错了的话,可能在调度算法里就要有所更改。
可能会有同学问为什么NENV是envs的长度,这个实际上在pmap.c里面的mips_vm_init里可以找到我们的证据,证明envs数组确实给它分配了NENV个结构体的空间,所以它也就有NENV个元素了。
1 static int 2 env_setup_vm(struct Env *e) 3 { 4 // Hint: 5 6 int i, r; 7 struct Page *p = NULL; 8 9 Pde *pgdir; 10 if ((r = page_alloc(&p)) < 0) 11 { 12 panic("env_setup_vm - page_alloc error\n"); 13 return r; 14 } 15 p->pp_ref++; 16 e->env_pgdir = (void *)page2kva(p); 17 e->env_cr3 = page2pa(p); 18 19 static_assert(UTOP % PDMAP == 0); 20 for (i = PDX(UTOP); i <= PDX(~0); i++) 21 e->env_pgdir[i] = boot_pgdir[i]; 22 e->env_pgdir[PDX(VPT)] = e->env_cr3 ; 23 e->env_pgdir[PDX(UVPT)] = e->env_cr3 ; 24 25 return 0; 26 }
其实这个函数并不需要我们实现,但是我还是想讲一讲这个函数的一些有意思的地方。
我们知道,每一个进程都有4G的逻辑地址可以访问,我们所熟知的系统不管是Linux还是Windows系统,都可以支持3G/1G模式或者2G/2G模式。3G/1G模式即满32位的进程地址空间中,用户态占3G,内核态占1G。这些情况在进入内核态的时候叫做陷入内核,因为即使进入了内核态,还处在同一个地址空间中,并不切换CR3寄存器。但是!还有一种模式是4G/4G模式,内核单独占有一个4G的地址空间,所有的用户进程独享自己的4G地址空间,这种模式下,在进入内核态的时候,叫做切换到内核,因为需要切换CR3寄存器,所以进入了不同的地址空间!
而我们这次实验,根据./include/mmu.h里面的布局来说,我们其实就是2G/2G模式,用户态占用2G,内核态占用2G。所以记住,我们在用户进程开启后,访问内核地址不需要切换CR3寄存器!其实这个布局模式也很好地解释了为什么我们需要把boot_pgdir里的内容拷到我们的e->env_pgdir中,在我们的实验中,对于不同的进程而言,其虚拟地址ULIM以上的地方,映射关系都是一样的!这是因为这2G虚拟地址与物理地址的对应,不是由进程管理的,是由内核管理的。
另外一点有意思的地方不知大家注意到没有,UTOP~ULIM明明是属于User的区域,却还是把内核这部分映射到了User区,而且我们看mmu.h的布局,觉得会非常有意思!
o ULIM -----> +----------------------------+-----------0x8000 0000
o | User VPT | PDMAP
o UVPT -----> +----------------------------+-----------0x7fc0 0000
o | PAGES | PDMAP
o UPAGES -----> +----------------------------+-----------0x7f80 0000
o | ENVS | PDMAP
o UTOP,UENVS -----> +----------------------------+-----------0x7f40 0000
o UXSTACKTOP -/ | user exception stack | BY2PG
o +----------------------------+------------0x7f3f f000
o | Invalid memory | BY2PG
o USTACKTOP ----> +----------------------------+------------0x7f3f e000
o | normal user stack | BY2PG
o +----------------------------+------------0x7f3f d000
a | |
盗用mmu.h里面这张图,我们仔细地来分析一下:
可以看到UTOP是0x7f40 0000,既然有映射,一定就有分配映射的过程,我们使用grep指令搜索一下 UENVS,发现它在这里有pmap.c里的mips_vm_init有所迹象:
envs = (struct Env*)alloc(NENV*sizeof(struct Env),BY2PG,1);
boot_map_segment(pgdir,UENVS,NENV*sizeof(struct Env),PADDR(envs),PTE_R);
可以发现什么呢?其实我们发现,UENVS和envs实际上都映射到了envs对应的物理地址!
其实足以看出来,内核在映射的时候已经为用户留下了一条路径!一条获取其他进程信息的路途!而且我们其实可以知道,这一部分对于进程而言应当是只能读不可以写的。开启中断后我们在进程中再访问内核就会产生异常来陷入内核了,所以应该是为了方便读一些进程信息,内核专门开辟了这4M的用户进程虚拟区。用户读这4M空间的内容是不需要产生异常的。
e->env_pgdir[PDX(VPT)] = e->env_cr3 ;
e->env_pgdir[PDX(UVPT)] = e->env_cr3 ;
这一部分是设置UVPT和VPT映射到4M的页表的起始地址,不过这里还没想太清楚。这里设置UVPT充其量只是能读到e->env_pgdir的那些东西,只有4K的页目录而已,那为什么要用4M的虚拟地址来映射呢?奇怪。。。
1 int env_alloc(struct Env **new, u_int parent_id) 2 { 3 int r; 4 /*precondtion: env_init has been called before this function*/ 5 /*1. get a new Env from env_free_list*/ 6 struct Env *currentE; 7 currentE = LIST_FIRST(&env_free_list); 8 /*2. call some function(has been implemented) to intial kernel memory layout for this new Env. 9 *hint:please read this c file carefully, this function mainly map the kernel address to this new Env address*/ 10 if((r=env_setup_vm(currentE))<0) 11 return r; 12 /*3. initial every field of new Env to appropriate value*/ 13 currentE->env_id = mkenvid(currentE); 14 currentE->env_parent_id = parent_id; 15 currentE->env_status = ENV_NOT_RUNNABLE; 16 /*4. focus on initializing env_tf structure, located at this new Env. especially the sp register, 17 * CPU status and PC register(the value of PC can refer the comment of load_icode function)*/ 18 //currentE->env_tf.pc = 0x20+UTEXT; 19 currentE->env_tf.regs[29] = USTACKTOP; 20 currentE->env_tf.pc = UTEXT + 0xb0; 21 currentE->env_tf.cp0_status = 0x10001004; 22 /*5. remove the new Env from Env free list*/ 23 LIST_REMOVE(currentE,env_link); 24 *new = currentE; 25 return 0; 26 }
1 static void 2 load_icode(struct Env *e, u_char *binary, u_int size) 3 { 4 int r; 5 u_long currentpg,endpg; 6 7 currentpg = UTEXT; 8 // printf("\ncurrentpg:%x\n",currentpg); 9 endpg = currentpg + ROUND(size,BY2PG); 10 //currentpg is since 0x0040 0000;so it is already rounded; 11 /*precondition: we have a valid Env object pointer e, a valid binary pointer pointing to some valid 12 machine code(you can find them at $WORKSPACE/init/ directory, such as code_a_c, code_b_c,etc), which can 13 *be executed at MIPS, and its valid size */ 14 while(currentpg < endpg){ 15 struct Page *page; 16 if((r= page_alloc(&page))<0) 17 return; 18 if((r= page_insert(e->env_pgdir,page,currentpg,PTE_V|PTE_R))<0) 19 return; 20 //printf("*binary:%8x\n",binary); 21 //printf("*page2kva:%8x\n",page2kva(page)); 22 //bcopy((void *)binary,page2kva(page),BY2PG); 23 //bcopy((void *)binary,page2pa(page),BY2PG); 24 bzero(page2kva(page),BY2PG); 25 bcopy((void *)binary,page2kva(page),BY2PG); 26 //printf("copy succeed!\n"); 27 binary += BY2PG; 28 currentpg +=BY2PG; 29 } 30 //currentpg = UTEXT; 31 //bzero(currentpg,ROUND(size,BY2PG)); 32 //bcopy((void *)binary,(void *)currentpg,size); 33 /*1. copy the binary code(machine code) to Env address space(start from UTEXT to high address), it may call some auxiliare function 34 (eg,page_insert or bcopy.etc)*/ 35 struct Page *stack; 36 page_alloc(&stack); 37 page_insert(e->env_pgdir,stack,(USTACKTOP-BY2PG),PTE_V|PTE_R); 38 //printf("Stack Set success\n"); 39 /*2. make sure PC(env_tf.pc) point to UTEXT + 0x20, this is very import, or your code is not executed correctly when your 40 * process(namely Env) is dispatched by CPU*/ 41 assert(e->env_tf.pc == UTEXT+0xb0); 42 e->env_status = ENV_RUNNABLE; 43 //printf("env_tf.pc:%x\n",e->env_tf.pc); 44 }
这个堪称是本次实验中为数不多的坑函数之一,无数仁人志士在bcopy这里落马,所以我也就重点讲一下几个要点好了。
首先要解释的就是这个page_insert函数,这个函数看起来平淡无奇,但是如果层层深入,就能发现里面的一些奥妙之处。
我们首先来看page_insert:
1 int 2 page_insert(Pde *pgdir, struct Page *pp, u_long va, u_int perm) 3 { // Fill this function in 4 u_int PERM; 5 Pte *pgtable_entry; 6 PERM = perm | PTE_V; 7 8 pgdir_walk(pgdir, va, 0, &pgtable_entry); 9 10 if(pgtable_entry!=0 &&(*pgtable_entry & PTE_V)!=0) 11 if(pa2page(*pgtable_entry)!=pp) page_remove(pgdir,va); 12 else 13 { 14 tlb_invalidate(pgdir, va); 15 *pgtable_entry = (page2pa(pp)|PERM); 16 return 0; 17 } 18 tlb_invalidate(pgdir, va); 19 if(pgdir_walk(pgdir, va, 1, &pgtable_entry)!=0){ 20 return -E_NO_MEM; 21 } 22 *pgtable_entry = (page2pa(pp)|PERM); 23 // printf("page_insert get the pa:*pgtable_entry %x\n",*pgtable_entry); 24 pp->pp_ref++; 25 return 0; 26 }
实际上这个函数是这样一个流程:
先判断va是否有对应的页表项,如果页表项有效。或者叫va是否已经有了映射的物理地址。如果有的话,则去判断这个物理地址是不是我们要插入的那个物理地址,如果不是,那么就把该物理地址移除掉;如果是的话,则修改权限,放到tlb里去!
关于page_inert以下两点一定要注意:
既然提到了tlb_invalidate函数,那么我们来仔细分析一下这个函数,这个函数代码如下:
1 void 2 tlb_invalidate(Pde *pgdir, u_long va) 3 { 4 if (curenv) 5 tlb_out(PTE_ADDR(va)|GET_ENV_ASID(curenv->env_id)); 6 else 7 tlb_out(PTE_ADDR(va)); 8 9 }
关于为什么要使用GET_ENV_ASID宏,助教老师给的指导书里其实没有讲太清楚,tlb的ASID区域只有20位,而我们mkenvid函数调用后得到的id值是可以超出20位的,大家可以在env_init初始化的时候打印env_id的值,然后在init.c里面create 1024个进程即可看到实际上envid最大可达1ffbfe,而使用GET宏之后最大可达ffc0,而且都可以为tlb用于区分进程,所以肯定是位数越少越好啦。而且还有一个比较有意思的地方,GET宏里实际上是让env_id先 >>11 然后 <<6 达到最后效果的,这样和>>5有什么区别呢?区别就在于 如果先>>11再 <<6,后6位一定是0!(2进制位),所以我猜后六位一定是有其独特用处的,否则在这里也不会强调清零,不过我们这次实验里还没有看到特殊用处。
1 LEAF(tlb_out) 2 //1: j 1b 3 nop 4 mfc0 k1,CP0_ENTRYHI 5 mtc0 a0,CP0_ENTRYHI 6 nop 7 tlbp 8 nop 9 nop 10 nop 11 nop 12 mfc0 k0,CP0_INDEX 13 bltz k0,NOFOUND 14 nop 15 mtc0 zero,CP0_ENTRYHI 16 mtc0 zero,CP0_ENTRYLO0 17 nop 18 tlbwi 19 //add k0, 40 20 //sb k0, 0x90000000 21 //li k0, ‘>‘ 22 //sb k0, 0x90000000 23 NOFOUND: 24 25 mtc0 k1,CP0_ENTRYHI 26 27 j ra 28 nop 29 END(tlb_out)
这段汇编是tlb_invalidate函数的精华所在,CP0_ENTRYHI实际上就是用来给tlb倒腾数据的,不用太在意其本身的作用。
前两句是指把之前的CP0_ENTRYHI存在k1里面暂存一下。然后我们就有一条很关键的汇编指令 tlbp ,很关键!
通过查mips手册可以知道tlbp的功能如下:
之后的几个nop应该是为tlb指令设置的流水缓冲,因为tlbp执行的周期要比一般指令长。其实这条汇编的目的就是:
To find a matching entry in the TLB.所以说实际上是把va及其对应的物理地址存在tlb里了,而且tlbp应该是依托于CP0_INDEX和CP0_EnrtyHI寄存器的。那么后面的那些读CP0_INDEX实际上是对tlbp执行是否成功的一个判断而已。注意,这里的tlbp就是在内核态下进行的,所以不会产生异常!如果在用户态下修改CP0的寄存器,或者使用tlbp汇编等,那就说明是tlb缺失或page_fault了!
那么再返回我们的page_insert来看看下一句,下一句是建立一个va与pa之间的桥梁,一个页表的建立,pgdir_walk(pgdir, va, 1, &pgtable_entry),所以说我们其实在最开始load_icode的时候,实际上是建立了不止size大小的页,还需要建立一个能够映射到该页的页表!那么在最后,为页表项的内容设置权限位PTE_R。恩,那么page_insert函数就此结束了。
page_insert函数结束了,不代表我们这个load_icode结束。下一步则是bcopy。
bcopy这个函数本身不坑,坑的是用法。首先对比原文中的这句我们来粗浅地看一下bcopy:
bzero(page2kva(page),BY2PG); bcopy((void *)binary,page2kva(page),BY2PG);
我个人以为这里bzero清零比较好,因为不能保证lab2哪里有问题还会影响到这里来。我倾向于分配就一页一页地copy,当然实验证明这样写也是没有任何问题的。那么下面来解释一下为什么这里用的是page2kva(page),而不是用与UTEXT有关的数值?
首先我们解释过了,UTEXT+0xb0是程序的入口,何谓入口?比如我们现在启动了一个进程,我们如何能从哪里开始,该怎样跑呢?这取决于我们run一个进程前的准备工作,当然这个工作在进程切换时也需要做,其中很重要的一点就是保存pc。这一点很重要,极其重要。如果是第一次run一个进程的时候,我们的pc是务必要被设置为UTEXT+0xb0的,这也是在env_alloc里面所做的工作。之后有一些我们没有关注过的汇编程序就会默默地根据我们设置的pc去找我们的程序入口,默默地执行,遇到中断默默地保存,切换。于是就这样完成了进程的运行与切换大计。
那么我们这里bcopy不能用UTEXT来copy是因为,我们这里是还没开始一个进程,没有其页目录来作为及目录
标签:
原文地址:http://www.cnblogs.com/SivilTaram/p/oslab3.html