Loading

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\n");
 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\n");
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\n");
 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\n");
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\n");
23      }
24      //unmap the temporary place
25      ret=syscall_mem_unmap(0,tmp);
26      if(ret<0){
27          user_panic("unmap error\n");
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\n");
19          env = &envs[ENVX(syscall_getenvid())];
20      //  writef("son fork end\n");
21          return 0;
22      }
23          
24      for(i=0;i<USTACKTOP;i+=BY2PG){
25      //  writef("0x%x\n",i);
26          if((Pde*)(*vpd)[i>>PDSHIFT]){
27          //  writef("start duppage\n");
28              if((Pte*)(*vpt)[i>>PGSHIFT]){
29                  duppage(newenvid,VPN(i));
30              }
31          }
32      }
33      
34      //  writef("duppage end\n");
35  //  writef("start alloc in fork\n");
36      ret=syscall_mem_alloc(newenvid,UXSTACKTOP-BY2PG,PTE_V|PTE_R);
37  //  writef("end alloc in fork\n");
38      if(ret<0) {
39          return ret;
40      }
41      ret=syscall_set_pgfault_handler(newenvid,__asm_pgfault_handler,UXSTACKTOP);
42  //  writef("end pgdault\n");
43      if(ret<0) {
44          return ret;
45      }
46      ret=syscall_set_env_status(newenvid,ENV_RUNNABLE);
47  //  writef("end status\n");
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)

posted @ 2020-05-09 16:23  圆*  阅读(1006)  评论(0编辑  收藏  举报