且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

《Linux内核设计的艺术:图解Linux操作系统架构设计与实现原理》——3.2 内核第一次做进程调度

更新时间:2022-09-20 22:12:09

3.2 内核第一次做进程调度

现在执行的是进程0的代码。从这里开始,进程0准备切换到进程1去执行。
在Linux 0.11的进程调度机制中,通常有以下两种情况可以产生进程切换。
1)允许进程运行的时间结束。
进程在创建时,都被赋予了有限的时间片,以保证所有进程每次都只执行有限的时间。一旦进程的时间片被削减为0,就说明这个进程此次执行的时间用完了,立即切换到其他进程去执行,实现多进程轮流执行。
2)进程的运行停止。
当一个进程需要等待外设提供的数据,或等待其他程序的运行结果……或进程已经执行完毕时,在这些情况下,虽然还有剩余的时间片,但是进程不再具备进一步执行的“逻辑条件”了。如果还等着时钟中断产生后再切换到别的进程去执行,就是在浪费时间,应立即切换到其他进程去执行。
这两种情况中任何一种情况出现,都会导致进程切换。
进程0角色特殊。现在进程0切换到进程1既有第二种情况的意思,又有怠速进程的意思。我们会在3.3.1节中讲解怠速进程。
进程0执行for(;;) pause( ),最终执行到schedule()函数切换到进程1,如图3-13所示。

《Linux内核设计的艺术:图解Linux操作系统架构设计与实现原理》——3.2 内核第一次做进程调度

pause函数的执行代码如下:

//代码路径:init/main.c:
    …
static inline _syscall0(int,fork)
static inline _syscall0(int,pause)
    …
void main(void)
{ 
    …
    move_to_user_mode();
    if (!fork()) {        /* we count on this going ok */
         init();
    }
    for(;;) 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()函数进行进程切换,执行代码如下:

//代码路径:kernel/sched.c:
int sys_pause(void)
{
//将进程0设置为可中断等待状态,如果产生某种中断,或其他进程给这个进程发送特定信号…才有可能将
//这个进程的状态改为就绪态
       current->state= TASK_INTERRUPTIBLE;
       schedule();
       return 0;
}

在schedule()函数中,先分析当前有没有必要进行进程切换,如果有必要,再进行具体的切换操作。
首先依据task[64]这个结构,第一次遍历所有进程,只要地址指针不为空,就要针对它们的“报警定时值alarm”以及“信号位图signal”进行处理(我们会在后续章节详细讲解信号,这里先不深究)。在当前的情况下,这些处理还不会产生具体的效果,尤其是进程0此时并没有收到任何信号,它的状态是“可中断等待状态”,不可能转变为“就绪态”。
第二次遍历所有进程,比较进程的状态和时间片,找出处在就绪态且counter最大的进程。现在只有进程0和进程1,且进程0是可中断等待状态,不是就绪态,只有进程1处于就绪态,所以,执行switch_to(next),切换到进程1去执行,如图3-14中的第一步所示。
执行代码如下:

//代码路径:kernel/sched.c:
void schedule(void)
{
    int i,next,c;
    struct task_struct ** p;

/* check alarm, wake up any interruptible tasks that have got a signal */
    for(p= &LAST_TASK;p > &FIRST_TASK;--p)
         if (*p){
               if((*p)->alarm&&(*p)->alarm<jiffies){       //如果设置了定时或定时已过
                     (*p)->signal |= (1<<(SIGALRM-1));    //设置SIGALRM
                     (*p)->alarm= 0;            //alarm清零    
                     }
               if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) &&
               (*p)->state==TASK_INTERRUPTIBLE)        //现在还不是这种情况
                     (*p)->state=TASK_RUNNING; 
         }

/* 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)//找出就绪态中
                               //counter最大的进程
                     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;//即counter= counter /2 + priority
    }
    switch_to(next);
}
//代码路径:include/sched.h:
    …
// FIRST_TSS_ENTRY<<3是100000,((unsigned long) n)<<4,对进程1是10000
// _TSS(1)就是110000,最后2位特权级,左第3位GDT,110是6即GDT中tss0的下标
#define _TSS(n) ((((unsigned long) n)<<4) + (FIRST_TSS_ENTRY<<3))
    …
#define switch_to(n) {\            //参看2.9.1节
struct {long a,b;} __tmp; \        //为ljmp的CS、EIP准备的数据结构
__asm__("cmpl %%ecx,_current\n\t" \
         "je 1f\n\t" \            //如果进程n是当前进程,没必要切换,退出
         "movw %%dx,%1\n\t" \        //EDX的低字赋给*&__tmp.b,即把CS赋给.b
         "xchgl %%ecx,_current\n\t" \    //task[n]与task[current]交换
         "ljmp %0\n\t" \  // ljmp到__tmp,__tmp中有偏移、段选择符, 但任务门忽略偏移
         "cmpl %%ecx,_last_task_used_math\n\t" \//比较上次是否使用过协处理器    
         "jne 1f\n\t" \
         "clts\n" \                 //清除CR0中的切换任务标志
      "1:" \
         ::"m" (*&__tmp.a),"m" (*&__tmp.b), \    //.a对应EIP(忽略),.b对应CS 
         "d" (_TSS(n)),"c" ((long) task[n]));\//EDX是TSS n的索引号,ECX即task[n]
}

程序将一直执行到"ljmp %0nt" 这一行。ljmp通过CPU的任务门机制并未实际使用任务门,将CPU的各个寄存器值保存在进程0的TSS中,将进程1的TSS数据以及LDT的代码段、数据段描述符数据恢复给CPU的各个寄存器,实现从0特权级的内核代码切换到3特权级的进程1代码执行,如图3-14中的第二步所示。

《Linux内核设计的艺术:图解Linux操作系统架构设计与实现原理》——3.2 内核第一次做进程调度

接下来,轮到进程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的中断没有返回。