zoukankan      html  css  js  c++  java
  • BUAA_OS lab4 难点梳理

    BUAA_OS lab4 难点梳理

    lab4体会到了OS难度的飞升。实验需要掌握的重点有以下:

    1. 系统调用流程

    2. 进程通信机制

    3. fork

    本lab理解难度较高,接下来将以以上三部分分别梳理。

    系统调用

    概念

    一般情况下,进程不能存取内核数据。但有的场景必须通过内核执行,因此操作系统设计了陷入异常后调用特定内核函数的过程。

    系统调用流程

    系统调用的具体层次结构为:

    系统调用流程图

    按照这个流程,首先来看syscall_lib.c中的函数们。

    1 void syscall_putchar(char ch)
    2 {
    3     msyscall(SYS_putchar, (int)ch, 0, 0, 0, 0);
    4 }

    以此函数为例,在调用用户空间的syscall_*()函数后,该函数会将传入的参数,连带系统调用号SYS_*()一起,传入msyscall函数。msyscall函数比较简单,将调用syscall。

    1  LEAF(msyscall)
    2      syscall
    3      jr ra
    4      nop
    5  END(msyscall)

    调用syscall指令后,陷入内核态,pc将被指向一个内核异常入口,即handle_sys()函数。该函数作用为将传入的参数安置到合适的位置,然后调用对应的内核态系统调用函数。这就出现了系统调用部分最难理解的区块:传入参数的位置。

     1 #include <asm/regdef.h>
     2  #include <asm/cp0regdef.h>
     3  #include <asm/asm.h>
     4  #include <stackframe.h>
     5  #include <unistd.h>
     6  7  /*** exercise 4.2 ***/
     8  NESTED(handle_sys,TF_SIZE, sp)
     9      SAVE_ALL                            // Macro used to save trapframe
    10      CLI                                 // Clean Interrupt Mask
    11      nop
    12      .set at                             // Resume use of $at
    13 14      // TODO: Fetch EPC from Trapframe, calculate a proper value and store it back to trapframe.
    15      lw      t0, TF_EPC(sp)
    16      addiu   t0, t0, 4
    17      sw      t0, TF_EPC(sp)
    18      // TODO: Copy the syscall number into $a0.
    19      lw      a0, TF_REG4(sp) 
    20      addiu   a0, a0, -__SYSCALL_BASE     // a0 <- relative syscall number
    21      sll     t0, a0, 2                   // t0 <- relative syscall number times 4
    22      la      t1, sys_call_table          // t1 <- syscall table base
    23      addu    t1, t1, t0                  // t1 <- table entry of specific syscall
    24      lw      t2, 0(t1)                   // t2 <- function entry of specific syscall
    25 26      lw      t0, TF_REG29(sp)            // t0 <- user's stack pointer
    27      lw      t3, 16(t0)                  // t3 <- the 5th argument of msyscall
    28      lw      t4, 20(t0)                  // t4 <- the 6th argument of msyscall
    29 30      // TODO: Allocate a space of six arguments on current kernel stack and copy the six arguments to proper location
    31      lw      a0, TF_REG4(sp)
    32      lw      a1, TF_REG5(sp)
    33      lw      a2, TF_REG6(sp)
    34      lw      a3, TF_REG7(sp)
    35      addiu   sp, sp, -24
    36      sw      t3, 16(sp)
    37      sw      t4, 20(sp)
    38      
    39      
    40      jalr    t2                          // Invoke sys_* function
    41      nop
    42      
    43      // TODO: Resume current kernel stack
    44      addiu   sp, sp, 24 
    45      sw      v0, TF_REG2(sp)             // Store return value of function sys_* (in $v0) into trapframe
    46 47      j       ret_from_exception          // Return from exeception
    48      nop
    49  END(handle_sys)
    50 51  sys_call_table:                         // Syscall Table
    52  .align 2
    53      .word sys_putchar
    54      .word sys_getenvid
    55      .word sys_yield
    56      .word sys_env_destroy
    57      .word sys_set_pgfault_handler
    58      .word sys_mem_alloc
    59      .word sys_mem_map
    60      .word sys_mem_unmap
    61      .word sys_env_alloc
    62      .word sys_set_env_status
    63      .word sys_set_trapframe
    64      .word sys_panic
    65      .word sys_ipc_can_send
    66      .word sys_ipc_recv
    67      .word sys_cgetc
    handle_sys

    在进入handle_sys函数时,原先的寄存器都是被以trapframe的形式传入的,因此参数也都保存在trapframe中。msyscall函数的前四个参数(即系统调用号+前三个参数)分别被存储在trapframe的a0-a3寄存器,即需要用TF_REG4-7(sp)进行获取。而我们的目标为,将这四个参数装入a0-a3寄存器。第5、6个参数,分别被安置在16(TF_REG29(sp))和20(TF_REG29(sp)),我们的目标为,sp自减24,后将他们转移到16(sp)和20(sp)。可以理解为,为了装入这六个参数,栈指针下降了24字节来保存他们,而他们根据顺序由地址小到地址大存放。但由于前四个函数在a0-a3中已经存储,所以0(TF_REG29(sp))到12(TF_REG29(sp))空余即可,而5、6个参数依旧需要被存放在16(TF_REG29(sp))和20(TF_REG29(sp))。

    理解了这些,handle_sys函数的操作就比较明显了。首先,需要将TF_EPC(sp)+4,让系统调用后进程能返回下一条指令继续执行;从TF_REG4(sp)取出a0,用以跳转到对应的sys_*函数;在按照以上分析的,将参数从tf中取出,安置到对应的位置。

    系统调用函数

    系统调用函数的实现,即不全syscall_all.c中的各函数,没有什么理解难度,在此就不赘述了。

     

    进程通信

    进程间通信机制IPC,需要通过系统调用来实现进程之间的数据交流。由于进程的地址空间都是独立的,要想把数据从一个地址空间转移到另一个空间,需要利用各个进程都共享的空间——内核的2G空间(具体原因lab3中已阐述)。

    因此,选择使用内核中的进程控制块来实现进程通信,即修改PCB的某些属性。至此,也没有什么理解难度了。

     

    fork

    首先需要直到,从顶层来看,fork函数执行后的效果,就是产生了一个和原本进程几乎一模一样的子进程,但他们相互独立。

    fork 在不同的进程中返回值不一样,在父进程中返回值不为0(返回子进程的id),在子进程中返回值为0。

    调用fork之后的具体流程如下图,也是一个理解fork的保命图:

    fork流程图

    父进程正常执行之上的部分主要展示了fork()函数的流程,而之下有关缺页中断的部分主要涉及写时复制机制,也是这部分的理解难点。

    写时复制

    在fork时,父进程会为子进程分配新的虚拟地址空间,但是父子进程实际上共用物理空间。在父进程或子进程需要修改内存时,需要调用写时复制机制,为发生修改的页单独分配新的物理空间,父进程指向新的空间,而子进程依旧指向原来的空间。

    对于每一页,都会用PTE_COW标志位保护起来,即表示当它被修改时,需要进行写时复制。

    与写时复制相关的函数主要有以下。

     1 void
     2  page_fault_handler(struct Trapframe *tf)
     3  {
     4      struct Trapframe PgTrapFrame;
     5      extern struct Env *curenv;
     6  //  printf("start page fault handler
    ");
     7  8      bcopy(tf, &PgTrapFrame, sizeof(struct Trapframe));
     9 10      if (tf->regs[29] >= (curenv->env_xstacktop - BY2PG) &&
    11          tf->regs[29] <= (curenv->env_xstacktop - 1)) {
    12              tf->regs[29] = tf->regs[29] - sizeof(struct  Trapframe);
    13              bcopy(&PgTrapFrame, (void *)tf->regs[29], sizeof(struct Trapframe));
    14          } else {
    15              tf->regs[29] = curenv->env_xstacktop - sizeof(struct  Trapframe);
    16              bcopy(&PgTrapFrame,(void *)curenv->env_xstacktop - sizeof(struct  Trapframe),sizeof(struct Trapframe));
    17          }
    18      // TODO: Set EPC to a proper value in the trapframe
    19      tf->cp0_epc=curenv->env_pgfault_handler;
    20  //  printf("end page fault handler
    ");
    21      return;
    22  }

    该函数主要进行写时复制前的一些处理,返回前需要将cp0_epv指向env_pgfault_handler函数入口。而env_pgfault_handler指向的函数,就是pgfault(),即真正处理缺页异常的函数。(写时复制依赖于缺页异常实现)。

     1 static void
     2  pgfault(u_int va)
     3  {
     4      u_int *tmp;
     5      u_int ret;
     6      u_int perm=(*vpt)[VPN(va)]&0xfff;
     7      if((perm&PTE_COW)==0){
     8          user_panic("not a copy-on-write page
    ");
     9          return;
    10      }
    11      tmp=USTACKTOP;
    12      u_int round_va=ROUNDDOWN(va,BY2PG);
    13      ret=syscall_mem_alloc(0,tmp,PTE_V|PTE_R);
    14      if(ret<0){
    15          user_panic("alloc error
    ");
    16      }
    17      //map the new page at a temporary place
    18      user_bcopy((void*)round_va,(void*)tmp, BY2PG);
    19      //map the page on the appropriate place
    20      ret=syscall_mem_map(0,tmp,0,round_va,PTE_V|PTE_R);
    21      if(ret<0){
    22          user_panic("map error
    ");
    23      }
    24      //unmap the temporary place
    25      ret=syscall_mem_unmap(0,tmp);
    26      if(ret<0){
    27          user_panic("unmap error
    ");
    28      }
    29  }

    该函数首先判断是否为写时复制页,如果是,则先分配新的内存页到临时位置,将要复制的内容拷贝到刚刚分配的页中,再将临时位置上的内容映射到发生缺页中断的虚拟地址上,注意设定好对应的页面权限,然后解除临时位置对内存的映射。至此,完成缺页异常的处理。

    fork函数

    解决完缺页异常和写时复制问题,我们再来看一下fork函数的具体流程。

     1 extern void __asm_pgfault_handler(void);
     2  int
     3  fork(void)
     4  {
     5      // Your code here.
     6      u_int newenvid;
     7      extern struct Env *envs;
     8      extern struct Env *env;
     9      u_int i,j;
    10      u_int ret;
    11 12      //The parent installs pgfault using set_pgfault_handler
    13      //alloc a new alloc
    14      set_pgfault_handler(pgfault);
    15 16      newenvid=syscall_env_alloc();
    17      if(newenvid == 0){
    18      //  writef("start son
    ");
    19          env = &envs[ENVX(syscall_getenvid())];
    20      //  writef("son fork end
    ");
    21          return 0;
    22      }
    23          
    24      for(i=0;i<USTACKTOP;i+=BY2PG){
    25      //  writef("0x%x
    ",i);
    26          if((Pde*)(*vpd)[i>>PDSHIFT]){
    27          //  writef("start duppage
    ");
    28              if((Pte*)(*vpt)[i>>PGSHIFT]){
    29                  duppage(newenvid,VPN(i));
    30              }
    31          }
    32      }
    33      
    34      //  writef("duppage end
    ");
    35  //  writef("start alloc in fork
    ");
    36      ret=syscall_mem_alloc(newenvid,UXSTACKTOP-BY2PG,PTE_V|PTE_R);
    37  //  writef("end alloc in fork
    ");
    38      if(ret<0) {
    39          return ret;
    40      }
    41      ret=syscall_set_pgfault_handler(newenvid,__asm_pgfault_handler,UXSTACKTOP);
    42  //  writef("end pgdault
    ");
    43      if(ret<0) {
    44          return ret;
    45      }
    46      ret=syscall_set_env_status(newenvid,ENV_RUNNABLE);
    47  //  writef("end status
    ");
    48      if(ret<0) {
    49          return ret;
    50      }
    51 52      return newenvid;
    53  }
    1. 设置缺页异常处理函数pgfault。

    2. 使用syscall_env_alloc()创建新进程

    3. 如果是子进程,将env设为该进程,直接返回

    4. 如果是父进程,将地址空间使用duppage复制一份给子进程

    5. 为子进程alloc出一块异常处理栈,位置为UXSTACKTOP-BY2PG

    6. 为子进程设置异常处理函数

    7. 设置子进程状态为可执行

    以上fork流程在流程图中已有展现,需要特别强调的是duppage函数。

    duppage函数对于操作的具体要求如下:

    对于可写页面,给父进程和子进程都加PTE_COW的时候要注意顺序。必须要先给子进程加,再给父进程加。至于原因,下图展现了如果先给父进程加可能会造成的问题。

    如果先给父进程加PTE_COW,然后修改了该页,该页将进行写时复制,父进程指向新的页,而新页没有被加上PTE_COW。此时再map子进程,子进程该页加上PTE_COW位而父进程没有。在随后程序运行中,若父进程进行修改,由于缺失PTE_COW,导致无法进行写时复制,因此子进程的运行出现错误(子进程该页本来不该被改,但却由于父进程被改而一起改了)。

     

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

  • 相关阅读:
    2. Add Two Numbers
    1. Two Sum
    leetcode 213. 打家劫舍 II JAVA
    leetcode 48. 旋转图像 java
    leetcode 45. 跳跃游戏 II JAVA
    leetcode 42. 接雨水 JAVA
    40. 组合总和 II leetcode JAVA
    24. 两两交换链表中的节点 leetcode
    1002. 查找常用字符 leecode
    leetcode 23. 合并K个排序链表 JAVA
  • 原文地址:https://www.cnblogs.com/CindyZhou/p/12858468.html
Copyright © 2011-2022 走看看