6.1 进程概述

6.1.1 进程介绍

什么是进程?书中说:系统中的若干进程就像一个人在一天内要做的若干样工作:

总体上:每样工作相对独立,又受控于人,可以产生某种结果;
细节上:每样工作都有自己的方法、工具和需要的资源;
时间上:每一个时刻只能有一项工作正在处理,各项工作可以轮换来做,对最终结果没有影响。

进程也是一样:

宏观上:每个进程相对独立,有自己的目标/功能,又受控于进程调度模块;
微观上:它可以利用系统的资源,有自己的代码(做事的方法),有自己的数据和堆栈(做事需要的资源和工具);
时间上:进程需要被调度轮换,不影响最终结果。

6.1.2 未雨绸缪——形成进程的必要考虑

为了实现进程的调度,我们必须有一个数据结构,记录一个进程的状态。在进程将被挂起的时候,进程信息写入其中;进程重新启动时,这些信息被读取出来。

更复杂的是,很多情况下进程和进程调度运行在不同的层级上。简化一下,让所有任务运行在 ring1 ,进程切换运行在 ring0 。

另外,引发进程切换的原因有多种,比如说发生了时钟中断,此时中断处理程序会将控制权交给进程调度模块。这时,如果系统认为应该进行进程切换(也有不应该进行进程切换的时候),进程调度就发生了——当前进程的状态被保存起来,队列中下一个进程被恢复执行。

6.2 最简单的进程

进程切换时的情形:

  1. 一个进程 A 正在运行;

  2. 这时候时钟中断发生,特权级从 ring1 跳到 ring0 ,开始执行时钟中断处理程序;

  3. 中断处理程序调用进程调度模块,指定下一个应该运行的进程 B ;

  4. 当中断处理程序结束时,下一个进程 B 准备就绪并开始运行,特权级又从 ring0 跳回 ring1,如图所示。

  5. 进程 B 运行中。

要想实现这些功能,必须完成以下几项:

  • 时钟中断处理程序

  • 进程调度模块

  • 两个进程

先来分析一下,进程 A 切换到进程 B 的过程中,有哪些关键技术需要解决。然后用代码分别实现这几个部分。

6.2.1 简单进程的关键技术预测

在实现简单的进程之前,能够想到的关键技术大致包括下面的内容。

  1. 进程的哪些状态需要被保存
    只有可能被改变的才有保存的必要。进程要运行需要CPU和内存相互协作,而不同进程的内存互不干涉。但是CPU只有一个,不同进程共用一个CPU的一套寄存器。所以,我们要把寄存器的值统统保存起来,在进程被恢复执行时使用。

  2. 进程的状态何时以及怎样被保存
    为了保证进程状态完整、不被破坏,要在进程刚刚被挂起时保存所有寄存器的值。
    用 push 或者 pushad (一条指令可以保存许多寄存器值)。这些代码应该写在时钟中断例程的最顶端,以便中断发生时马上被执行。

  3. 如何恢复进程B的状态
    保存用的是 push ,恢复则用 pop 。等所有寄存器的值都已经被恢复,执行指令 iretd ,就回到了进程 B。

  4. 进程表的引入
    进程的状态关系到每一次进程挂起和恢复,对于这样重要的数据结构,我们称之为"进程表"或者进程控制块 PCB 。通过进程表,我们可以非常方便地进行进程管理。

    这里,中断处理的部分内容必须使用汇编,其他大部分进程管理的内容都可以用C编写——将进程表定义成一个结构体;我们有很多个进程,所以会有很多个进程表,形成一个进程表数组。

    进程表是用来描述进程的,所以它必须独立于进程之外。 当我们把寄存器值压到进程表内的时候,已经处在进程管理模块之中。

  5. 进程栈和内核栈
    当寄存器的值已经被保存到进程表内,进程调度模块就开始执行。但这时 esp 指向何处?我们在进程调度模块中会用到堆栈,而寄存器被压到进程表之后,esp 是指向进程表某处的。接下来进行任何的堆栈操作,都会破坏掉进程表的值,从而在下一次进程恢复时产生严重的错误。

    为解决这个问题,必须将 esp 指向专门的内核栈区域。这样,在短短的进程切换过程中,esp的位置出现在3个不同的区域(下图是整个过程的示意)。

    其中:

    进程栈──进程运行时自身的堆栈。
    进程表──存储进程状态信息的数据结构。
    内核栈──进程调度模块运行时使用的堆栈。
    在具体编写代码的过程中,一定要清楚当前使用的是哪个堆栈,以免破坏掉数据。

  6. 特权级变换:ring1→→ring0
    在我们以前的代码中,还没有使用过除 ring0 之外的其他特权级。对于有特权级变换的转移:如果由外层向内层转移时,需要从当前 TSS 中取出内层 ss 和 esp 作为目标代码的 ss 和 esp 。所以,我们必须事先准备好 TSS 。
    由于每个进程相对独立,我们把涉及到的描述符放在局部描述符表 LDT 中,所以,我们还需要为每个进程准备 LDT 。

  7. 特权级变换:ring0→→ring1

    刚才的分析过程中,我们假设初始状态是“进程 A 运行中”。可是到目前为止我们的代码完全运行在 ring0 。所以,当我们准备开始第一个进程时,我们面临一个从 ring0 到 ring1 的转移,并启动进程 A 。

    这跟我们从进程 B 恢复的情形很相似,完全可以在准备就绪之后跳转到中断处理程序的后半部分,假装发生了一次时钟中断来启动进程 A ,利用 iretd 来实现 ring0 到 ring1 的转移。

6.3.2 第一步——ring0→→ring1

在 /kernel 中多了一个 main.c ,里面有个函数 kernel_main( ) ,从中有这样一行: restart( ); 。它调用的便是代码6.1这一段,它是进程调度的一部分,同时也是我们的操作系统启动第一个进程时的入口。

p_proc_ready 是一个指向进程表的指针,存放的便是下一个要启动进程的进程表的地址。而且,其中的内容必然是以图6.7所示的顺序进行存放。这样,才会使 pop 和 popad 指令执行后各寄存器的内容更新一遍。

在头文件 global.h 中可以找到 p_proc_ready ,其类型是一个结构类型指针 struct s_proc* 。再打开 proc.h ,可以看到 s_proc 这个结构体的第一个成员也是一个结构,叫做 s_stackframe 。我们找到 s_stackframe 这个结构体的声明,它的内容安排与我们的推断完全一致。

现在我们知道了,进程的状态都被存放在 s_proc 这个结构体中,而且位于前部的是所有相关寄存器的值, s_proc 这个结构应该是我们提到过的“进程表”。当要恢复一个进程时,便将 esp 指向这个结构体的开始处,然后运行一系列的 pop 命令将寄存器值弹出。