# 进程切换

​		进程切换是进程管理的核心功能，我们参考了uCore的实现方式，使用轻量级的RegContext来实现内核进程切换，在异常处理时使用`sscratch `寄存器保留用户态下的内核栈帧，内核态下时为0，从而实现区分内核栈和用户栈。（一部分系统也会使用该寄存器存储进程结构体之类的内容，不过考虑到这样汇编代码和结构体布局关联就大了，不易于维护，因此我们没有采用）



#### 切换发生的时机

​		要处理进程切换，就要先理解什么时候会发生进程切换，在我们的系统中，进程切换有如下几种情况：

1. 被动：时间片用完，发生时钟中断，中断处理程序调用调度器进行调度
2. 主动：进程结束，包括正常终止和因错误终止等原因，进程无法继续执行。其实现方法一致地为首先调用当前进程的`Exit`函数，然后紧接着调用`Schedule`函数让调度器进行切换。对于内核线程和用户进程的区别只有它们的汇编出入代码略有差异，下文介绍。
3. 主动：进程请求主动让出CPU，Syscall中即yeild调用，实现也非常简单，即调用当前进程的`Rest`函数即可。而Rest函数内部会调用调度器让其他进程得到运行。
4. 主动：进入阻塞状态，目前仅由信号量提供阻塞机制，信号量会将进程切换到阻塞状态，并让调度器调度其他进程。具体设计见0x33同步原语文档的信号量部分。



#### 调度器实现

​		调度器是进程调度的管理者，我们采用的简单实用的时间片轮转调度，目前不区分优先级。实现时即轮番扫描进程列表，选择当前进程后面（环形）遇到的第一个就绪进程，调用该进程的`Run`成员函数即可。此外调度器还目前还额外承担了定时器的功能，即扫描时会判断某个超时等待的进程是否可以因为超时而就绪了，不过目前精度不高，后续计划利用定时器来设计。



#### 进程的`Run`方法

​		在我们的设计中，将进程切换是为当前进程让某一进程`Run`的过程，这样抽象后既可以方便地实现调度器，也可以实现进程指定剩余时间片的继承者。

​		具体实现为，将当前进程设为`S_Ready`，将目标进程设为`S_Running`并更新当前进程指针（此时的执行环境还是在原先进程的栈中），进入到目标进程的虚拟内存空间`VMS`，调用`ProcessSwitchContext`函数，该函数会将所有被调用者保存的寄存器（即前面提到的`RegContext`）保存到`Process`对象中，然后载入目标进程的`RegContext`并从函数调用返回，由于栈指针发生了变化，程序的其他寄存器的值就随着栈的更换而实现了切换。

​		`Run`是我们系统中进程切换的唯一途径，可以认为是调用了Run后会从其他进程的`Run`调用离开，自己停在`Run`调用里等待调度。



#### 切换示意图



#### 内核线程切换

​		内核线程的切换就很简单了，通过调度器、休息等途径最终使用`Run`来切换执行。

#### 用户进程切换

​		首先，用户态无法自己完成进程切换，因此用户进程切换的流程总是用户态因为某些原因陷入到内核态，此时就可以视作是要做内核线程的切换了，使用上述的同样操作即可。

#### 用户入口点

​		首先构造RegContext寄存器，将返回地址和栈指针设置为初始化所需的状态，保证第一次调度会进入到用户入口点`UserThreadEntry`。然后通过构造TrapFrame中断帧，提供从内核态进入用户态的跳转。

​		此外，我们还在`UserThreadEntry`的开头加入了判断`S0`寄存器是否为空，如果不为空则是做函数指针进行调用，参数为`S1`寄存器的值，这样设计的好处是我们可以在进程启动前插入一段内核代码，为进程启动做准备，例如ELF的载入，我们将加载ELF文件的步骤放在创建出来的进程里做，即进程自己负责自己的加载，这样不会让调用者等待太久。



----------------

By：qianpinyi

2022.06.05