现在执行的是进程0的代码。从这里开始,进程0准备切换到进程1去执行。
在linux 0.11的进程调度机制中,通常有以下两种情况可以产生进程切换。
1)允许进程运行的时间结束。
进程在创建时,都被赋予了有限的时间片,以保证所有进程每次都只执行有限的时间。一旦进程的时间片被削减为0,就说明这个进程此次执行的时间用完了,立即切换到其他进程去执行,实现多进程轮流执行。
2)进程的运行停止。
当一个进程需要等待外设提供的数据,或等待其他程序的运行结果……或进程已经执行完毕时,在这些情况下,虽然还有剩余的时间片,但是进程不再具备进一步执行的“逻辑条件”了。如果还等着时钟中断产生后再切换到别的进程去执行,就是在浪费时间,应立即切换到其他进程去执行。
这两种情况中任何一种情况出现,都会导致进程切换。
进程0角色特殊。现在进程0切换到进程1既有第二种情况的意思,又有怠速进程的意思。我们会在3.3.1节中讲解怠速进程。
进程0执行for(;;) pause( ),最终执行到schedule()函数切换到进程1,如图3-13所示。
pause函数的执行代码如下:
pause()函数的调用与fork()函数的调用一样,会执行到unistd.h中的syscall0,通过int 0x80中断,在system_call.s中的call _sys_call_table(,%eax,4)映射到sys_pause( )的系统调用函数去执行,具体步骤与3.1.1节中调用fork()函数步骤类似。略有差别的是,fork()函数是用汇编写的,而sys_pause()函数是用c语言写的。
进入sys_pause()函数后,将进程0设置为可中断等待状态,如图3-13中第一步所示,然后调用schedule()函数进行进程切换,执行代码如下:
在schedule()函数中,先分析当前有没有必要进行进程切换,如果有必要,再进行具体的切换操作。
首先依据task[64]这个结构,第一次遍历所有进程,只要地址指针不为空,就要针对它们的“报警定时值alarm”以及“信号位图signal”进行处理(我们会在后续章节详细讲解信号,这里先不深究)。在当前的情况下,这些处理还不会产生具体的效果,尤其是进程0此时并没有收到任何信号,它的状态是“可中断等待状态”,不可能转变为“就绪态”。
第二次遍历所有进程,比较进程的状态和时间片,找出处在就绪态且counter最大的进程。现在只有进程0和进程1,且进程0是可中断等待状态,不是就绪态,只有进程1处于就绪态,所以,执行switch_to(next),切换到进程1去执行,如图3-14中的第一步所示。
执行代码如下:
程序将一直执行到"ljmp %0nt" 这一行。ljmp通过cpu的任务门机制并未实际使用任务门,将cpu的各个寄存器值保存在进程0的tss中,将进程1的tss数据以及ldt的代码段、数据段描述符数据恢复给cpu的各个寄存器,实现从0特权级的内核代码切换到3特权级的进程1代码执行,如图3-14中的第二步所示。
接下来,轮到进程1执行,它将进一步构建环境,使进程能够以文件的形式与外设交互。
需要提醒的是,pause()函数的调用是通过int 0x80中断从3特权级的进程0代码翻转到0特权级的内核代码执行的,在_system_call中的call _sys_call_table (,%eax,4) 中调用sys_pause()函数,并在sys_pause( )中的schedule( )中调用switch( ),在switch( )中ljmp进程1的代码执行。现在,switch( )中ljmp后面的代码还没有执行,call _sys_call_table (,%eax,4) 后续的代码也还没有执行,int 0x80的中断没有返回。