Lab中对system call的使用很简单,看起来和普通函数调用并没有什么区别,但实际上的调用流程是较为复杂的。我们很容易产生一些疑问:系统调用的整个生命周期具体是什么样的?用户进程和内核进程之间是如何切换上下文的?系统调用的函数名、参数和返回值是如何在用户进程和内核进程之间传递的?
1.用户态调用
在用户空间,所有system call的函数声明写在user.h中,调用后会进入usys.S执行汇编指令:将对应的系统调用号(system call number)置于寄存器a7中,并执行ecall指令进行系统调用,其中函数参数存在a0~a5这6个寄存器中。ecall指令将触发软中断,cpu会暂停对用户程序的执行,转而执行内核的中断处理逻辑,陷入(trap)内核态。
2.上下文切换
中断处理在kernel/trampoline.S中,首先进行上下文的切换,将user进程在寄存器中的数据save到内存中(保护现场),并restore(恢复)kernel的寄存器数据。内核中会维护一个进程数组(最多容纳64个进程),存储每个进程的状态信息,proc结构体定义在proc.h,这也是xv6对PCB(Process Control Block)的实现。用户程序的寄存器数据将被暂时保存到proc->trapframe结构中。
3.内核态执行
完成进程切换后,调用trap.c/usertrap(),接着进入syscall.c/syscall(),在该方法中根据system call number拿到数组中的函数指针,执行系统调用函数。函数参数从用户进程的trapframe结构中获取(a0~a5),函数执行的结果则存储于trapframe的a0字段中。完成调用后同样需要进程切换,先save内核寄存器到trapframe->kernel_*,再将trapframe中暂存的user进程数据restore到寄存器,重新回到用户空间,cpu从中断处继续执行,从寄存器a0中拿到函数返回值。
至此,系统调用完成,共经历了两次进程上下文切换:用户进程 -> 内核进程 -> 用户进程,同时伴随着两次CPU工作状态的切换:用户态 -> 内核态 -> 用户态。
实验代码:https://github.com/zhayujie/xv6-riscv-fall19
原文链接:https://zhayujie.com/mit6828-lab-util.html