BUAA_OS_2020_Lab4_Code_Review

Lab4的内容主要是系统调用与fork。lab4可以说是最坑爹的一个lab了,因为这个lab依赖于前几个lab的代码,而如果之前有遗留bug,就会对lab4造成致命性的影响,笔者就深受其害,分别在lab4查出了lab2与lab3的数个bug,调试体验极差。而也正是因为在这个lab中受了不少苦,现在回顾来看,这个lab也成为了我理解最为透彻的一个lab。

Lab4的文件树如下:

.
├── boot
│   ├── Makefile
│   └── start.S
├── drivers
│   ├── gxconsole
│   │   ├── console.c
│   │   ├── dev_cons.h
│   │   └── Makefile
│   └── Makefile
├── fs
│   └── fsformat
├── gxemul
│   ├── elfinfo
│   ├── fsformat
│   ├── r3000
│   ├── r3000_test
│   ├── test
│   └── view
├── include
│   ├── args.h
│   ├── asm
│   │   ├── asm.h
│   │   ├── cp0regdef.h
│   │   └── regdef.h
│   ├── asm-mips3k
│   │   ├── asm.h
│   │   ├── cp0regdef.h
│   │   └── regdef.h
│   ├── env.h
│   ├── error.h
│   ├── fs.h
│   ├── kclock.h
│   ├── kerelf.h
│   ├── mmu.h
│   ├── pmap.h
│   ├── printf.h
│   ├── print.h
│   ├── queue.h
│   ├── sched.h
│   ├── stackframe.h
│   ├── trap.h
│   ├── types.h
│   └── unistd.h
├── include.mk
├── init
│   ├── code_a.c
│   ├── code_b.c
│   ├── code.c
│   ├── init.c
│   ├── main.c
│   └── Makefile
├── lib
│   ├── env_asm.S
│   ├── env.c
│   ├── genex.S
│   ├── getc.S
│   ├── kclock_asm.S
│   ├── kclock.c
│   ├── kernel_elfloader.c
│   ├── Makefile
│   ├── printBackUp
│   ├── print.c
│   ├── printf.c
│   ├── sched.c
│   ├── syscall_all.c
│   ├── syscall.S
│   └── traps.c
├── Makefile
├── mm
│   ├── Makefile
│   ├── pmap.all
│   ├── pmap.c
│   └── tlb_asm.S
├── readelf
│   ├── kerelf.h
│   ├── main.c
│   ├── Makefile
│   ├── readelf.c
│   ├── testELF
│   └── types.h
├── tools
│   ├── scse0_3.lds
│   └── scse0_3.lds~ *
└── user *
    ├── bintoc *
    ├── console.c *
    ├── entry.S *
    ├── fd.c *
    ├── fd.h *
    ├── file.c *
    ├── fktest.c *
    ├── fork.c *
    ├── fprintf.c *
    ├── fsipc.c *
    ├── idle.c *
    ├── ipc.c *
    ├── lib.h *
    ├── libos.c *
    ├── Makefile *
    ├── pageref.c *
    ├── pgfault.c *
    ├── pingpong.c *
    ├── pipe.c *
    ├── print.c *
    ├── printf.c *
    ├── string.c *
    ├── syscall_lib.c *
    ├── syscall_wrap.S *
    └── user.lds *
Lab4文件树(已折叠)

新增的代码基本上都是用户态代码,前几个lab的进程管理、内存管理等都是在内核态运行的。而从这个lab开始,用户态正式出现了,并与内核态代码通过系统调用联系在一起。

系统调用相关

系统调用已经不必过多介绍,在用户态中,系统调用都被封装在syscall_lib.c中以供使用。在调用其中的函数时,调用关系为:user/syscall_lib.c: void syscall_*() -> user/syscall_wrap.S: msyscall -> syscall -> lib/syscall.S: handle_sys() -> lib/syscall_all.c: void sys_*()。以syscall为用户态与内核态的界限,通过这个过程层层陷入到内核态中。

以syscall_lib.c中的syscall_putchar(char ch)为例,

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

其将msyscall进行了封装,msyscall是定义在syscall_wrap.S中的汇编函数。汇编函数是由汇编代码中的标签定义的,C语言函数在编译时也被编译为标签,函数的调用通过压栈再跳转到标签实现。后边的几个0是为了统一格式,便于系统调用时对参数进行处理。在这个函数中,直接syscall陷入内核态。从内核态返回后直接返回到上层函数中。

1 #include <asm/regdef.h>
2 #include <asm/cp0regdef.h>
3 #include <asm/asm.h>
4 
5 LEAF(msyscall)
6 syscall
7 jr ra
8 nop
9 END(msyscall)

随后由syscall.S中定义的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 NESTED(handle_sys,TF_SIZE, sp)
 8     SAVE_ALL                            // Macro used to save trapframe
 9     CLI                                 // Clean Interrupt Mask
10     nop
11     .set at                             // Resume use of $at
12 
13     lw t0, TF_EPC(sp)
14     addiu t0, t0, 4
15     sw t0, TF_EPC(sp)
16 
17     lw a0, TF_REG4(sp)
18     
19     addiu   a0, a0, -__SYSCALL_BASE     // a0 <- relative syscall number
20     sll     t0, a0, 2                   // t0 <- relative syscall number times 4
21     la      t1, sys_call_table          // t1 <- syscall table base
22     addu    t1, t1, t0                  // t1 <- table entry of specific syscall
23     lw      t2, 0(t1)                   // t2 <- function entry of specific syscall
24 
25     lw      t0, TF_REG29(sp)            // t0 <- user's stack pointer
26     lw      t3, 16(t0)                  // t3 <- the 5th argument of msyscall
27     lw      t4, 20(t0)                  // t4 <- the 6th argument of msyscall
28 
29     lw a0, TF_REG4(sp)
30     lw a1, TF_REG5(sp)
31     lw a2, TF_REG6(sp)
32     lw a3, TF_REG7(sp)
33     addiu sp, sp, -24
34     sw t3, 16(sp)
35     sw t4, 20(sp)
36     
37     jalr    t2                          // Invoke sys_* function
38     nop
39     
40     addiu sp, sp, 24
41     
42     sw      v0, TF_REG2(sp)             // Store return value of function sys_* (in $v0) into trapframe
43 
44     j       ret_from_exception          // Return from exeception
45     nop
46 END(handle_sys)
47 
48 sys_call_table:                         // Syscall Table
49 .align 2
50     .word sys_putchar
51     .word sys_getenvid
52     .word sys_yield
53     .word sys_env_destroy
54     .word sys_set_pgfault_handler
55     .word sys_mem_alloc
56     .word sys_mem_map
57     .word sys_mem_unmap
58     .word sys_env_alloc
59     .word sys_set_env_status
60     .word sys_set_trapframe
61     .word sys_panic
62     .word sys_ipc_can_send
63     .word sys_ipc_recv
64     .word sys_cgetc
./lib/syscall.S(已折叠)

 这个函数的工作为:

  • 将运行栈中的EPC改为EPC+4,使函数返回时能够返回到syscall的下一条指令
  • 从栈中将系统调用号取出,并计算出相应内核态的系统调用函数入口地址(可以由sys_call_table计算出)
  • 传递参数,将前四个参数保存至a0~a3,其余压入栈中
  • 跳转至相应函数进行处理

剩余的工作由lib/syscall_all.c完成。依此流程即可实现系统调用功能。依靠系统调用,可以实现比较丰富的功能,也可以直接将内核态函数封装起来供用户态程序使用。添加系统调用可以通过一套固定的流程实现:

  1. 在./include/unistd.h中添加系统调用号
  2. 在./user/syscall_lib.c中添加用户态系统调用接口
  3. 在./lib/syscall_all.c中添加内核态系统调用函数
 1 #ifndef UNISTD_H
 2 #define UNISTD_H
 3 
 4 #define __SYSCALL_BASE 9527
 5 #define __NR_SYSCALLS 20
 6 
 7 #define SYS_putchar         ((__SYSCALL_BASE ) + (0 ) )
 8 #define SYS_getenvid         ((__SYSCALL_BASE ) + (1 ) )
 9 #define SYS_yield            ((__SYSCALL_BASE ) + (2 ) )
10 #define SYS_env_destroy        ((__SYSCALL_BASE ) + (3 ) )
11 #define SYS_set_pgfault_handler    ((__SYSCALL_BASE ) + (4 ) )
12 #define SYS_mem_alloc        ((__SYSCALL_BASE ) + (5 ) )
13 #define SYS_mem_map            ((__SYSCALL_BASE ) + (6 ) )
14 #define SYS_mem_unmap        ((__SYSCALL_BASE ) + (7 ) )
15 #define SYS_env_alloc        ((__SYSCALL_BASE ) + (8 ) )
16 #define SYS_set_env_status    ((__SYSCALL_BASE ) + (9 ) )
17 #define SYS_set_trapframe        ((__SYSCALL_BASE ) + (10 ) )
18 #define SYS_panic            ((__SYSCALL_BASE ) + (11 ) )
19 #define SYS_ipc_can_send        ((__SYSCALL_BASE ) + (12 ) )
20 #define SYS_ipc_recv        ((__SYSCALL_BASE ) + (13 ) )
21 #define SYS_cgetc            ((__SYSCALL_BASE ) + (14 ) )
22 #endif
./include/unistd.h(已折叠)

可以看出许多用户态函数都是与内核态函数一一对应的,除了直接通过系统调用相对应的外,还有一系列用户态库函数。在user文件夹中,主要存在两类程序,第一类是如上所述的库函数,第二类是用来创建用户态进程的程序。在OS课程中,这两者我们都要编写,在OS课程以外,我们通常直接调用库函数,主要编写用来创建进程的程序。(创建用户态进程的方法见于lab3的code view)

Fork相关

 fork是从父进程创建子进程的函数,这样能以较小的开销对子进程进行管理。流程为:

  • 创建子进程的进程控制块(此时子进程尚不可调度)
  • 为子进程复制页表,并设置写时复制位进行保护(duppage())
  • 为子进程分配异常处理栈并设置缺页处理函数(set_pgfault_handler())
  • 唤醒子进程,父子进程各自继续执行,当对COW页写入时触发缺页中断,由缺页中断函数对页面进行复制

在复制页表时,指导书上给出了4种情况:

  • 只读页面:按照相同的权限(只读)映射给子进程
  • 共享页面:即具有PTE_LIBRARY标记的页面,需保持共享可写的状态
  • 写时复制页面:即具有PTE_COW标记的页面,是上一次fork的结果
  • 可写页面:需要给父子进程加入COW位

综合来说,只有可写页面的权限需要加入COW位,其他情况均保持原状进行映射即可。

其他步骤均按照指导书与代码中提示即可完成,代码略。

可以说,本lab虽然调试比较麻烦,但是是比较清晰的一个lab。


2020.6.25更新:👴申优测试过了,后边两个lab不写了,欧液✌

 

posted @ 2020-06-09 18:37  LittleNyima  阅读(326)  评论(0编辑  收藏  举报