[感受]
这次操作系统实验感觉还是比较难的,除了因为助教老师笔误引发的2个错误外,还有一些关键性的理解的地方感觉还没有很到位,这些天一直在不断地消化、理解Lab3里的内容,到现在感觉比Lab2里面所蕴含的内容丰富很多,也算是有所收获,和大家分享一下我个人的一些看法与思路,如果有错误的话请指正。
[关键函数理解]
首先第一部分我觉得比较关键的是对于一些非常关键的函数的理解与把握,这些函数是我们本次实验的精华所在,虽然好几个实验都不需要我们自己实现,但是这些函数真的是非常厉害!有多厉害,呆会就知道了。
首先是从第一个我们要填的函数说起吧:
images/loading.gif' data-original="http://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif" />
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 into11 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_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 }
env_setup_vm
其实这个函数并不需要我们实现,但是我还是想讲一讲这个函数的一些有意思的地方。
我们知道,每一个进程都有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 }
View Code
这一部分呢,是单个进程控制块要被分配资源的时候做的一些初始化的工作,其中有几个比较有意思的点很值得深究:
currentE->env_tf.pc = UTEXT + 0xb0;
currentE->env_tf.cp0_status = 0x10001004;
第一条可能会有比较大的疑问,为什么进程的二进制码分配到UTEXT对应的地址那里去了,而且也建立好映射关系了,怎么还要加个偏移量作为pc初始值呢?
我们知道pc初始值实际上是进程开始运行的地方,而这里为什么是UTEXT+0xb0,这0xb0是什么东西?我们需要去探究一下code_a.c或者code_b.c文件了,实际上经过一定的了解,这个文件应当是一个elf文件。看其前四个字节就能看出:
0x7f 0x46 0x4c 0x46
这是elf的标准头,而实际上像这样的标准头的长度是有0xb0的长度,这个实际上我们可以把code_a.c里的数组搞出来,然后变成一个elf文件,最后使用readelf来读取出地址,这样就能明白原理了。所以UTEXT+0xb0这个虚拟地址对应物理地址里面放着的,才是真正可执行代码的第一条。
再来就是这个0x10001004这个问题,这个问题很好玩。因为R3000自身的SR寄存器与mips其他版本的SR寄存器略有不同,它的最后六位记载了是一组深度为二的二重栈,不过笔者在这里还残留着一些不大不小的问题。《see mips run》中只是提到了关于这些寄存器的作用,而没有提到中断的时候这些寄存器应当是什么状态。如果有兴趣的同学可以grep一下 "CP0_STATUS" 和"cp0_status" 说不定能发现个中玄机。
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 valid12 machine code(you can find them at $WORKSPACE/init/ directory, such as code_a_c, code_b_c,etc), which can13 *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 function34 (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 your40 * 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 }
load_icode
这个堪称是本次实验中为数不多的坑函数之一,无数仁人志士在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 else13 {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 }
View Code
实际上这个函数是这样一个流程:
先判断va是否有对应的页表项,如果页表项有效。或者叫va是否已经有了映射的物理地址。如果有的话,则去判断这个物理地址是不是我们要插入的那个物理地址,如果不是,那么就把该物理地址移除掉;如果是的话,则修改权限,放到tlb里去!
关于page_inert以下两点一定要注意:
- page_insert处理将同一虚拟地址映射到同一个物理页面上不会将当前已有的物理页面移除掉,但是需要修改掉permission;
- 只要对页表有修改,都必须tlb_invalidate一下,否则后面紧接着对内存的访问很有可能出错。这就是为什么有一些同学直接使用了pgdir_walk而没有page_insert产生错误的原因。
既然提到了tlb_invalidate函数,那么我们来仔细分析一下这个函数,这个函数代码如下:
1 void2 tlb_invalidate(Pde *pgdir, u_long va)3 {4 if (curenv)5 tlb_out(PTE_ADDR(va)|GET_ENV_ASID(curenv->env_id));6 else7 tlb_out(PTE_ADDR(va));8 9 }
tlb_invalidate
关于为什么要使用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 nop10 nop11 nop12 mfc0 k0,CP0_INDEX13 bltz k0,NOFOUND14 nop15 mtc0 zero,CP0_ENTRYHI16 mtc0 zero,CP0_ENTRYLO017 nop18 tlbwi19 //add k0, 4020 //sb k0, 0x9000000021 //li k0, '>'22 //sb k0, 0x9000000023 NOFOUND:24 25 mtc0 k1,CP0_ENTRYHI26 27 j ra28 nop29 END(tlb_out)
tlb_out
这段汇编是tlb_invalidate函数的精华所在,CP0_ENTRYHI实际上就是用来给tlb倒腾数据的,不用太在意其本身的作用。
前两句是指把之前的CP0_ENTRYHI存在k1里面暂存一下。然后我们就有一条很关键的汇编指令 tlbp ,很关键!
通过查mips手册可以知道tlbp的功能如下:
那么我们这里bcopy不能用UTEXT来copy是因为,我们这里是还没开始一个进程,没有其页目录来作为及目录
原标题:[操作系统实验lab3]实验报告
关键词: