我们以三个用户进程(str1、str2、str3)为例,来看看多个进程是如何运行的,他们又是如何切换的。
进程的源代码如下,str1、str2、str3三者代码一样。
#include <stdio.h>
int foo(int n)
{
char text[2048];
if(n==0)
return 0;
else
{
int i = 0;
for(i; i<2048; i++)
text[i] = '\0';
sleep(5);
foo(n - 1);
}
}
int main(int argc, char **argc)
{
foo(6);
return 0;
}
1、假设现在创建(fork)了三个进程,并执行(execve)对应的程序,他们的进程号是5、6、7,它们的线性地址空间的位置应该依次是4*64 ~ 5*64MB,5*64 ~ 6*64MB,6*64 ~ 7*64MB。假设三个进程此时都处于就绪态,也就是如下图:
图 1
2、假设现在轮到str1进程执行。str1开始执行foo函数调用,就需要压栈(这是从汇编的角度看的,局部变量需要压栈),于是产生了缺页中断。在缺页中断中,内核为str1进程申请了空闲物理页面,并将其映射到str1进程的线性地址空间。之后进程再对text数组进行设置,内容就被写在了刚分配的物理页面上了。执行效果如下图:
图 2
3、str1在执行过程中,无轮在0特权级还是3特权级,每10毫秒就会产生一次时钟中断,会调用do_timer,这样就会消减它的时间片,直到消减到0,如果此时进程处于3特权级,就执行schedule。
do_timer如下:
void do_timer(long cpl)
{
......
if ((--current->counter)>0) return;//判断时间片是否削减为0
current->counter=0;
if (!cpl) return;//只有在3特权级下才能切换,0特权级下不能切换
schedule();
}
我们假设切换到str2进程,进程str2也执行同样逻辑的程序,值得注意的是,当设置text数组时,屏幕打印的逻辑地址与当时进程str1的地址相同。但他们的线程地址不同,物理内存中进程str2也并没有于str1重叠。如下图:图 3
4、str2无轮在0特权级还是3特权级,每10毫秒就会产生一次时钟中断,会调用do_timer,这样就会消减它的时间片,直到消减到0,如果此时进程处于3特权级,就执行schedule,切换到str3执行,它也要压栈,str3开始执行,执行的代码于进程str2相同,也是压栈,并设置text。str3程序执行压栈的效果如下:
图 4
5、str3执行一段时间后,时间片也用完了。这样三个用户进程虽然还需要继续执行,但时间片都用完了。当再发生时钟中断时,do_timer()函数调用schedule()函数进行进程切换,这时,内核会为他们重新分配时间片。
内核从task[]的末端开始重新给当前系统的所有进程(包括处于睡眠的进程,但进程0除外)分配时间片,时间片的大小为couter/2 + priority。priority是进程的优先级,所以进程的优先级越高,分配到的时间片就越多。然后根据此时时间片的情况重新选择进程运行,如此反复。
执行的代码如下:
void schedule(void)
{
.....
/* this is the scheduler proper: */
while (1) {
c = -1;
next = 0;
i = NR_TASKS;
p = &task[NR_TASKS];
while (--i) {
if (!*--p)
continue;
if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
c = (*p)->counter, next = i;
}
if (c) break;
for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
if (*p)
(*p)->counter = ((*p)->counter >> 1) +
(*p)->priority;
}
switch_to(next);
}
三个程序执行一段时间后,压入他们各自的栈中的数据在主内存中的分布如下图:
图 5