怎么实现进程切换
是什么
进程是一个运行中的程序实体,拥有独立的地址空间和逻辑控制流。
void sayHi()
{
printf("%s\n", "Hello,World");
return 0;
}
sayHi
就是一个函数,它一旦运行起来,就是进程。
独立的逻辑控制流,是说这个进程就像独占一个CPU一样。每个进程使用CPU的时间不是连续的,但它们的指令运行却是前后衔接的,不会受到其他进程的指令对它的指令和数据大的更改。
运行起来
进程,这里是指用户进程(区别于内核),处在特权级3。内核在特权级0。CPU从通电运行起,就处在特权级0。要运行用户进程,需要从特权级0转移到特权级3。
CPU执行哪条指令,受cs:eip
控制。有一个指令,既能够实现特权级从高向低转移,又能更新cs:eip
。这个指令就是iretd
。
执行第一个用户进程的方法是,把cs、eip、ss、esp
等寄存器需要的值入栈,然后用irted
出栈,用户进程就能运行起来了。示意代码如下。注意,下面的代码只是说明大概思路,不是可执行的代码。
push ss
push esp
push cs
push eip
irted
停下来
操作系统要让一个CPU能运行多个进程。一个进程不能总是独占CPU,必须让它停下来,把CPU让给其他进程使用。
时钟中断每隔一段时间就会停止当前进程,转移到执行中断例程。
切换
在中断例程中,我们可以为当前进程A建立一个”快照“,选择运行进程B。
进程的”快照“,是把进程正在使用的数据、下一条要运行的指令等信息存储起来;然后选择另外一个进程,从存储设备中取出这个进程运行所需要的所有信息,最后运行这个进程。
进程正在使用的数据,全部都在寄存器中,把寄存器中的数据存起来,就是为进程建立了快照。
把数据存储到哪里呢?在汇编语言中,动态存储数据,我发现只有堆栈可以使用。进程的切换就是这样一个流程:
- 进程A正在运行,时钟中断发生,执行中断例程。
- 从TSS的
esp0
中获取进程A的堆栈栈顶,把esp
指向这个栈顶。 - 把寄存器中的值都压入进程A的堆栈中。
- 把
esp
指向进程B的堆栈。调度程序就在这个步骤。 - 把TSS的
esp0
指向进程B的堆栈的最高地址处加4个字节。下一次中断发生时,TSS获取的esp0
的值就是在这里获取的。 - B的堆栈出栈。
- 使用
iretd
出栈ss、esp、cs、eip
,开始执行进程B的指令。
堆栈转移
从上面的切换过程,很容易看出,需要转移堆栈。
第1步~~~第3步,堆栈从用户进程中的不知名堆栈转移到存储进程A数据的堆栈。这个堆栈,为进程建立快照使用。每个进程都有一个这样的堆栈。
在第4步前后,都有一次堆栈切换:
- 前面,从A进程的堆栈切换到内核堆栈。进程调度程序很有可能会使用堆栈。如果仍然使用进程A的堆栈,会破坏A的堆栈,重新运行A时会有许多麻烦。
- 进程调度结束后,已经选择了进程B,需要从堆栈中恢复B的数据,于是把
esp
指向B的堆栈。
进入内核后,我们切换过GDT和内核堆栈。当初我觉得没必要切换内核堆栈,现在终于发现了内核堆栈的作用。
那个内核堆栈是这样建立的:先用0填充一段内存空间,比如4KB,然后在下面设置一个标号,这个标号就是内核堆栈。代码如下:
StatckStrace 1024 resp 0
TopStack: