实验难点
进程创建
对于初始化部分,首先需要在pmap.c中修改mips_vm_init()函数,为envs开空间,并map到UENVS空间。
其次,模仿page_init()的做法,将空闲进程控制块串成env_free_list。
至此没有什么理解上的难度。
进程部分的难点,主要在于进程创建流程的理解。进程创建的流程为:
-
从env_free_list中获取一个空的PCB
-
初始化进程控制块
-
为进程分配资源
-
从空闲链表中移出,开始执行
STEP1&2
env_alloc()函数清晰地展现了这一进程的前两步,我们以此展开分析。
1 int 2 env_alloc(struct Env **new, u_int parent_id) 3 { 4 int r; 5 struct Env *e; 6 7 /*Step 1: Get a new Env from env_free_list*/ 8 if(LIST_EMPTY(&env_free_list)){ 9 *new=NULL; 10 return -E_NO_FREE_ENV; 11 } 12 e = LIST_FIRST(&env_free_list); 13 14 /*Step 2: Call certain function(has been completed just now) to init kernel memory layout for this new Env. 15 *The function mainly maps the kernel address to this new Env address. */ 16 17 r = env_setup_vm(e); 18 if(r<0){ 19 return r; 20 } 21 22 /*Step 3: Initialize every field of new Env with appropriate values.*/ 23 e->env_id=mkenvid(e); 24 e->env_parent_id=parent_id; 25 e->env_status=ENV_RUNNABLE; 26 27 /*Step 4: Focus on initializing the sp register and cp0_status of env_tf field, located at this new Env. */ 28 e->env_tf.cp0_status = 0x10001004; 29 e->env_tf.regs[29]=USTACKTOP; 30 31 32 /*Step 5: Remove the new Env from env_free_list. */ 33 LIST_REMOVE(e,env_link); 34 *new=e; 35 return 0; 36 37 }
首先,从env_free_list中取出一个空PCB。
然后调用env_setup_vm()函数,该函数的主要作用是初始化新进程的空间。具体实现如下:
1 static int 2 env_setup_vm(struct Env *e) 3 { 4 //printf("start_env_setup_vm\n"); 5 int i, r; 6 struct Page *p = NULL; 7 Pde *pgdir; 8 9 /* Step 1: Allocate a page for the page directory 10 * using a function you completed in the lab2 and add its pp_ref. 11 * pgdir is the page directory of Env e, assign value for it. */ 12 r = page_alloc(&p); 13 if (r < 0) { 14 panic("env_setup_vm - page alloc error\n"); 15 return r; 16 } 17 p->pp_ref++; 18 pgdir = (Pde *)page2kva(p); 19 /*Step 2: Zero pgdir's field before UTOP. */ 20 for(i=0;i){ 21 pgdir[i]=0; 22 } 23 24 /*Step 3: Copy kernel's boot_pgdir to pgdir. */ 25 26 /* Hint: 27 * The VA space of all envs is identical above UTOP 28 * (except at UVPT, which we've set below). 29 * See ./include/mmu.h for layout. 30 * Can you use boot_pgdir as a template? 31 */ 32 for(i=PDX(UTOP);i<=PDX(~0);i++){ 33 pgdir[i]=boot_pgdir[i]; 34 } 35 e->env_pgdir = pgdir; 36 e->env_cr3 = PADDR(pgdir); 37 // UVPT maps the env's own page table, with read-only permission. 38 e->env_pgdir[PDX(UVPT)] = e->env_cr3 | PTE_V|PTE_R; 39 // printf("end_setup_vm\n"); 40 return 0; 41 }
首先我们要明确,每个进程都有自己的页表
在这个函数中,首先调用page_alloc()为该进程分配一个页目录页。获取该页的虚拟地址为pgdir的虚拟地址(至于为什么是虚拟地址,lab2中已有说明)。
接下来,需要将内核部分的页表进行拷贝。这是因为每个进程都有自己单独的页表,这个页表会映射完整的4G空间。但由于实验中采用的是2G+2G的模式,对于所有进程而言,用户态是不同的,但内核态部分是相同的(共享)。所以,所有进程的页表的内核2G部分都是完全相同的
完成页表拷贝之后,需要对PCB中相应值进行设置。然后回到env_alloc()。
接下来需要设置PCB中的某些值,其中尤其要注意的是e->env_tf.cp0_status。该设置使得能正常相应中断。然后将该进程从空闲列表中移出。
至此,创建进程的前两步完成。
STEP3&4
创建进程第三步,本质上也就是加载二进制镜像,在lab3中涉及三个函数,主要步骤如下:
-
load_icode()函数,初始化一个栈,然后调用load_elf()函数。
-
load_elf()负责解析ELF文件的字段,并调用load_icode_mapper()函数。
-
load_icode_mapper()则根据传入的参数将ELF文件内容加载进内存。
-
返回load_icode()函数后,设置pc寄存器值,使得能正常进入执行
这一部分也没有很复杂的逻辑,但是难在load_icode_mapper()函数的实现。
首先来看一个指导书中的图,可以说是活命必需品:
难点就在于,需要处理情况种类较多,需要重合考虑va是否对齐;bin_size结尾处是否对齐;sgsize结尾处是否对齐。
进程运行和切换
这一部分涉及函数为env_run()。其作用为保存当前进程上下文+恢复启动进程上下文
1 void 2 env_run(struct Env *e) 3 { 4 /*Step 1: save register state of curenv. */ 5 /* Hint: if there is an environment running, you should do 6 * switch the context and save the registers. You can imitate env_destroy() 's behaviors.*/ 7 // printf("start run\n"); 8 struct Trapframe *old; 9 old = (struct Trapframe *)(TIMESTACK - sizeof(struct Trapframe)); 10 11 if(curenv!=NULL){ 12 curenv->env_tf=*old; 13 curenv->env_tf.pc=curenv->env_tf.cp0_epc; 14 } 15 16 /*Step 2: Set 'curenv' to the new environment. */ 17 //printf("start curenv=e\n"); 18 curenv=e; 19 curenv->env_status=ENV_RUNNABLE; 20 /*Step 3: Use lcontext() to switch to its address space. */ 21 // printf("start lcontext\n"); 22 lcontext((int)e->env_pgdir); 23 24 /*Step 4: Use env_pop_tf() to restore the environment's 25 * environment registers and return to user mode. 26 * 27 * Hint: You should use GET_ENV_ASID there. Think why? 28 * (read, page 135-144) 29 */ 30 // printf("start pop tf\n"); 31 env_pop_tf(&curenv->env_tf, GET_ENV_ASID(curenv->env_id)); 32 printf("end run\n"); 33 }
首先,我们取出old,及当前环境上下文(寄存器的值等)。
然后将当前环境保存进当前进程的env_tf中,并当前进程的pc设置为cp0_epc,让其陷入中断。
到这里,保存现场的任务完成,可以将curenv设置为下一进程e。
最后,调用env_pop_tf()恢复现场。
在进程切换过程中,最难理解的就是TIMESTACK的含义。我认为TIMESTACK是时钟栈,存储时钟中断的时候存的trapframe。进入时钟中断后,把TIMESTACK的值赋值给寄存器们,再执行中断处理。而KERNEL_SP应当是系统调用后的存储区。
有关TIMESTACK,还有个很难理解的地方,在以下函数中:
void env_destroy(struct Env *e) { /* Hint: free e. */ env_free(e); /* Hint: schedule to run a new environment. */ if (curenv == e) { curenv = NULL; /* Hint:Why this? */ bcopy((void *)KERNEL_SP - sizeof(struct Trapframe), (void *)TIMESTACK - sizeof(struct Trapframe), sizeof(struct Trapframe)); printf("i am killed ... \n"); sched_yield(); } }
为什么要在destroy进程的时候,将KERNEL_SP的tf拷贝到TIMESTACK中?百思不得其解。
个人想法是,在调用sched_yield()获取下一个要执行的进程之前,要将环境恢复到调用当前进程之前的环境。
也可能和kill到最后一个进程的时候要恢复到最初状态有关。
中断异常
中断一场部分代码量较小,主要需要理解的是遇到中断异常后函数的调用关系。
-
跳转到.text.exc_vec3代码段
-
-
timer_ irq 里跳转到sched_ yield,选择下一个进程执行。
调度函数的实现根据注释来也没有大问题。但是在后期lab4-extra的时候可能会由于调度错误导致无法通过,所以需要尽量保证情况周全。
(代码仓库位于右上角Github)