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 *
新增的代码基本上都是用户态代码,前几个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_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
可以看出许多用户态函数都是与内核态函数一一对应的,除了直接通过系统调用相对应的外,还有一系列用户态库函数。在user文件夹中,主要存在两类程序,第一类是如上所述的库函数,第二类是用来创建用户态进程的程序。在OS课程中,这两者我们都要编写,在OS课程以外,我们通常直接调用库函数,主要编写用来创建进程的程序。(创建用户态进程的方法见于lab3的code view)
fork是从父进程创建子进程的函数,这样能以较小的开销对子进程进行管理。流程为:
在复制页表时,指导书上给出了4种情况:
- 只读页面:按照相同的权限(只读)映射给子进程
- 共享页面:即具有PTE_LIBRARY标记的页面,需保持共享可写的状态
- 写时复制页面:即具有PTE_COW标记的页面,是上一次fork的结果
- 可写页面:需要给父子进程加入COW位
综合来说,只有可写页面的权限需要加入COW位,其他情况均保持原状进行映射即可。
其他步骤均按照指导书与代码中提示即可完成,代码略。
可以说,本lab虽然调试比较麻烦,但是是比较清晰的一个lab。
原文:https://www.cnblogs.com/littlenyima/p/13073947.html