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 #include2 #include 3 #include 4 5 LEAF(msyscall) 6 syscall 7 jr ra 8 nop 9 END(msyscall)
随后由syscall.S中定义的handle_sys()进行处理:
1 #include2 #include 3 #include 4 #include 5 #include 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
这个函数的工作为:
- 将运行栈中的EPC改为EPC+4,使函数返回时能够返回到syscall的下一条指令
- 从栈中将系统调用号取出,并计算出相应内核态的系统调用函数入口地址(可以由sys_call_table计算出)
- 传递参数,将前四个参数保存至a0~a3,其余压入栈中
- 跳转至相应函数进行处理
剩余的工作由lib/syscall_all.c完成。依此流程即可实现系统调用功能。依靠系统调用,可以实现比较丰富的功能,也可以直接将内核态函数封装起来供用户态程序使用。添加系统调用可以通过一套固定的流程实现:
- 在./include/unistd.h中添加系统调用号
- 在./user/syscall_lib.c中添加用户态系统调用接口
- 在./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相关
fork是从父进程创建子进程的函数,这样能以较小的开销对子进程进行管理。流程为:
- 创建子进程的进程控制块(此时子进程尚不可调度)
- 为子进程复制页表,并设置写时复制位进行保护(duppage())
- 为子进程分配异常处理栈并设置缺页处理函数(set_pgfault_handler())
- 唤醒子进程,父子进程各自继续执行,当对COW页写入时触发缺页中断,由缺页中断函数对页面进行复制
在复制页表时,指导书上给出了4种情况:
- 只读页面:按照相同的权限(只读)映射给子进程
- 共享页面:即具有PTE_LIBRARY标记的页面,需保持共享可写的状态
- 写时复制页面:即具有PTE_COW标记的页面,是上一次fork的结果
- 可写页面:需要给父子进程加入COW位
综合来说,只有可写页面的权限需要加入COW位,其他情况均保持原状进行映射即可。
其他步骤均按照指导书与代码中提示即可完成,代码略。
可以说,本lab虽然调试比较麻烦,但是是比较清晰的一个lab。