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)

  • 相关阅读:
    SPRING-BOOT系列之简介
    SPRING-BOOT系列之Spring4快速入门
    Java网络编程
    python中cursor操作数据库(转)
    TextView显示HTML文本时<IMG>标签指定图片的显示处理
    DBCP连接池介绍
    spring中propertyplaceholderconfigurer简介
    NoSQL 数据建模技术(转)
    分布式服务框架 Zookeeper -- 管理分布式环境中的数据(转载)
    Linux下MySQL5.6的修改字符集编码为UTF8
  • 原文地址:https://www.cnblogs.com/CindyZhou/p/12858468.html
Copyright © 2011-2022 走看看