• BUAA_OS lab3 难点梳理


    BUAA_OS lab3 难点梳理

    实验难点

    进程创建

    对于初始化部分,首先需要在pmap.c中修改mips_vm_init()函数,为envs开空间,并map到UENVS空间。

    其次,模仿page_init()的做法,将空闲进程控制块串成env_free_list。

    至此没有什么理解上的难度。

     

    进程部分的难点,主要在于进程创建流程的理解。进程创建的流程为:

    1. 从env_free_list中获取一个空的PCB

    2. 初始化进程控制块

    3. 为进程分配资源

    4. 从空闲链表中移出,开始执行

     

    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_alloc

    首先,从env_free_list中取出一个空PCB。

    然后调用env_setup_vm()函数,该函数的主要作用是初始化新进程的空间。具体实现如下:

     1 static int
     2  env_setup_vm(struct Env *e)
     3  {
     4  //printf("start_env_setup_vm
    ");
     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
    ");
    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<PDX(UTOP);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
    ");
    40      return 0;
    41  }
    env_setup_vm

    首先我们要明确,每个进程都有自己的页表

    在这个函数中,首先调用page_alloc()为该进程分配一个页目录页。获取该页的虚拟地址为pgdir的虚拟地址(至于为什么是虚拟地址,lab2中已有说明)。

    接下来,需要将内核部分的页表进行拷贝。这是因为每个进程都有自己单独的页表,这个页表会映射完整的4G空间。但由于实验中采用的是2G+2G的模式,对于所有进程而言,用户态是不同的,但内核态部分是相同的(共享)。所以,所有进程的页表的内核2G部分都是完全相同的

    完成页表拷贝之后,需要对PCB中相应值进行设置。然后回到env_alloc()。

    接下来需要设置PCB中的某些值,其中尤其要注意的是e->env_tf.cp0_status。该设置使得能正常相应中断。然后将该进程从空闲列表中移出。

    至此,创建进程的前两步完成。

    STEP3&4

    创建进程第三步,本质上也就是加载二进制镜像,在lab3中涉及三个函数,主要步骤如下:

    1. load_icode()函数,初始化一个栈,然后调用load_elf()函数。

    1. load_elf()负责解析ELF文件的字段,并调用load_icode_mapper()函数。

    2. load_icode_mapper()则根据传入的参数将ELF文件内容加载进内存。

    3. 返回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
    ");
     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
    ");
    18      curenv=e;
    19      curenv->env_status=ENV_RUNNABLE;
    20      /*Step 3: Use lcontext() to switch to its address space. */
    21  //  printf("start lcontext
    ");
    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 <see mips run linux>, page 135-144)
    29       */
    30  //  printf("start pop tf
    ");
    31      env_pop_tf(&curenv->env_tf, GET_ENV_ASID(curenv->env_id));
    32      printf("end run
    ");
    33  }
    env_run

    首先,我们取出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 ... 
    ");
             sched_yield();
         }
     }

    为什么要在destroy进程的时候,将KERNEL_SP的tf拷贝到TIMESTACK中?百思不得其解。

    个人想法是,在调用sched_yield()获取下一个要执行的进程之前,要将环境恢复到调用当前进程之前的环境。

    也可能和kill到最后一个进程的时候要恢复到最初状态有关。

     

    中断异常

    中断一场部分代码量较小,主要需要理解的是遇到中断异常后函数的调用关系。

    1. 跳转到.text.exc_vec3代码段

    2. 根据时钟中断,分发handle_ int函数来处理时钟中断

    3. timer_ irq 里跳转到sched_ yield,选择下一个进程执行。

    调度函数的实现根据注释来也没有大问题。但是在后期lab4-extra的时候可能会由于调度错误导致无法通过,所以需要尽量保证情况周全。

     

    (代码仓库位于右上角Github)

     

  • 相关阅读:
    js正则 转载
    asp.net中打开新窗口的多种方法(转载)
    ajax有两种提交数据的方式,分别为get和post(转)
    DropDownList 绑定(转载)
    CentOS网络配置
    Java内存区域-- 运行时数据区域
    Spring Ioc--Bean装配
    Spring--Spring容器
    java正则表达式 --简单认识
    Leetcode 402:移除K个数字
  • 原文地址:https://www.cnblogs.com/CindyZhou/p/12852837.html
Copyright © 2020-2023  润新知