我们以三个用户进程(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); }
6、接着他们都会继续不断地压栈,这三个用户进程在线性地址空间内压入他们各自的栈中的数据都是连续的,但是在物理空间内压栈的数据却是完全”交错“分布的。
三个程序执行一段时间后,压入他们各自的栈中的数据在主内存中的分布如下图:
图 5