From d64b3d41a1f9288bda878dd52ea7e408dbffa654 Mon Sep 17 00:00:00 2001 From: Shine wOng <1551885@tongji.edu.cn> Date: Sun, 15 Sep 2019 10:03:32 +0800 Subject: [PATCH] add syscall fork to lab5_report. --- thu_os/lab4_report.md | 2 +- thu_os/lab5_report.md | 72 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 71 insertions(+), 3 deletions(-) diff --git a/thu_os/lab4_report.md b/thu_os/lab4_report.md index 1e444d6..69a2d43 100644 --- a/thu_os/lab4_report.md +++ b/thu_os/lab4_report.md @@ -444,4 +444,4 @@ forkrets: > 线程切换总结 -线程的切换,主要有依赖`proc_struct.context`和`proc_struct.tf`,即上下文信息和中断帧信息。首先通过上下文信息,跳转到了中断返回函数,在其中通过模拟中断返回机制,使程序的控制权最终转交到中断帧中所指示的位置。值得注意的是,这是线程第一个投入运行时的情况,在经过调度后重新恢复运行时,只需要上下文信息`context`就足够了,因为在此前的被切换的过程中,`context`已经保存了恢复的地址和状态信息了。 +线程的切换,主要有依赖`proc_struct.context`和`proc_struct.tf`,即上下文信息和中断帧信息。首先通过上下文信息,跳转到了中断返回函数,在其中通过模拟中断返回机制,使程序的控制权最终转交到中断帧中所指示的位置。值得注意的是,这是线程第一次投入运行时的情况,在经过调度后重新恢复运行时,只需要上下文信息`context`就足够了,因为在此前的被切换的过程中,`context`已经保存了恢复的地址和状态信息了。 diff --git a/thu_os/lab5_report.md b/thu_os/lab5_report.md index 441ecdf..88ec38e 100644 --- a/thu_os/lab5_report.md +++ b/thu_os/lab5_report.md @@ -363,7 +363,7 @@ syscall(void) { 它将实际上跳转到`sys_exit`函数中,随后调用`kern/process/proc.c`中实现的`do_exit`函数来完成`exit`系统调用的处理。这里我们再看一下`do_exit`是如何真正地实现进程的退出的吧。 -## `exit`的实现 +### `exit`的实现 在原理课中讲过,当一个进程执行完毕需要退出时,将执行`exit`系统调用,从而在其中将它所占用的资源几乎全部归还给操作系统。为什么是几乎呢?因为归还资源的操作是它自己进行的,只要这个进程还存在,就至少还有进程控制块`PCB`的内存资源没有归还给操作系统。 @@ -430,7 +430,7 @@ do_exit(int error_code) { } ``` -## `wait`的实现 +### `wait`的实现 在原理课中讲到,当父进程需要等待某一个子进程退出,该子进程退出之后,父进程才能继续执行。在这种情况下,就应该使用`wait`系统调用。`wait`系统调用的实现和`exit`一样,都是一步步通过中断机制,分发到系统调用总控函数`syscall`,然后再调用实质性的处理函数`do_wait`,它也在`kern/process/proc.s`中实现。 @@ -520,6 +520,74 @@ UNINIT --------> RUNNABLE <----------> RUNNING ZOMBIE ``` +### `fork`系统调用的实现 + +在`lab4_report`中已经阐述了`do_fork`函数的实现,但是这里还有一个问题,在原理课上老师讲到,通过`fork`系统调用创建的子进程和父进程几乎完全相同,在被调度后子进程会从父进程被中断的地方开始执行,这是怎么实现的呢?此外,为了区分开父进程和子进程,可以通过`fork`系统调用的返回值,即父进程的返回值为子进程的`pid`,子进程的返回值为0,如下面的代码: + +```c +main(){ + ....... + int pid = fork(); + if(pid == 0){//child process goes here + exec_status = exec("calc", arg0, arg1, ...); + ...... + }else{//parent process goes here + ...... + } +} +``` + +又是如何实现父进程的子进程的返回值不一样的呢?接下来我们将一步一步分析`fork`系统调用实现的步骤。 + +如前面所说的那样,用户进程调用`fork()`时,会转到用户态系统调用库`user/libs/syscall.c(h)`,在其中会由`syscall`函数调用`INT 80`来请求操作系统的`fork`服务,之后CPU的控制权就转交给了中断服务程序,这里涉及到当前进程(父进程)中断帧的保存,最终实际的`fork`操作由`kern/process/proc.c::do_fork`来完成,`kern/syscall/syscall.c::sys_fork`的实现如下: + +```c +static int +sys_fork(uint32_t arg[]) { + struct trapframe *tf = current->tf; + uintptr_t stack = tf->tf_esp; + return do_fork(0, stack, tf); +} +``` + +可以看到,调用`do_fork`函数的参数分别是当前进程的用户堆栈`tf->tf_esp`以及中断帧。在`lab4`中已经实现了这个`do_fork`函数,在其中会进行一系列的资源拷贝工作。在`copy_thread`函数中,会把父进程的`tf`拷贝到子进程的`tf`当中。但是,子进程的上下文信息`context`仍然是设置到`forkret`。 + +到目前为止,我们应该可以看出子进程是如何在父进程中断的地方继续执行的了。子进程被调度后,通过`forkret`,转到中断返回函数,在其中通过`iret`将保存了父进程中断帧的`tf`加载到寄存器当中,这样就相当于恢复了父进程的中断现场。 + +但是还有一点,它们的返回值是如何做到不同的呢?事实上,注意到在`user/libs/syscall`中,有 + +```asm +asm volatile ( + "int %1;" + : "=a" (ret) + : "i" (T_SYSCALL), + "a" (num), + "d" (a[0]), + "c" (a[1]), + "b" (a[2]), + "D" (a[3]), + "S" (a[4]) + : "cc", "memory"); +``` + +可以看到,`INT 80`的返回值是保存在`eax`寄存器当中的,对于父进程,这个返回值就是`do_fork`的返回值,即子进程的`pid`。而对于子进程而言,注意到在`copy_thread`当中,是将中断帧的`tf_regs.reg_eax`设置为零的: + +```c +static void +copy_thread(struct proc_struct *proc, uintptr_t esp, struct trapframe *tf) { + proc->tf = (struct trapframe *)(proc->kstack + KSTACKSIZE) - 1; + *(proc->tf) = *tf; + proc->tf->tf_regs.reg_eax = 0; + proc->tf->tf_esp = esp; + proc->tf->tf_eflags |= FL_IF; + + proc->context.eip = (uintptr_t)forkret; + proc->context.esp = (uintptr_t)(proc->tf); +} +``` + +这样在中断返回时,这个零就会被加载到`eax`寄存器中,从而子进程的`fork`返回值就是零了。 + ## 进程的切换与退出 之前我一直很迷惑,这里ucore的进程怎么实现切换的,最后又是怎么退出的,一开始感觉非常神奇,后来发现原来是最朴素的策略。原来这里根本就没有什么所谓的时间片轮转算法,没有抢占机制,一切的切换都是进程自己主动通过系统调用执行的。