进程调度与进程切换的过程小析

作者:xujianguo

 原创作品转载请注明出处,《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000

——————————————————————————————————————————————————————-————

实验目的:

  •  使用gdb跟踪分析一个schedule()函数 ,验证您对Linux系统进程调度与进程切换过程的理解;
  •  理解进程上下文的切换机制,以及与中断上下文切换的关系;

实验环境:

  

        实验楼:www.shiyanlou.com。

实验步骤

   1.配置环境,登录实验楼网站。

按照以前的实验方式配置好环境,具体过程见图示:

测试安装是否成功:

成功。

2.3.配置调试系统,和gdb调试配置。

3设置断点,测试进程调度。

——schedule:测试安排。

switch_to:进入点同上。

实验分析:

       

进程调度的时机:

1、进程状态转换的时刻:进程终止、进程睡眠;

    进程要调用sleep()或exit()等函数进行状态转换,这些函数会主动调用调度程序进行进程调度;

2、当前进程的时间片用完时(current->counter=0);

    由于进程的时间片是由时钟中断来更新的,因此,这种情况和时机4是一样的。

3、设备驱动程序

    当设备驱动程序执行长而重复的任务时,直接调用调度程序。在每次反复循环中,驱动程序都检查need_resched的值,如果必要,则调用调度程序schedule()主动放弃CPU。

4、进程从中断、异常及系统调用返回到用户态时;

     不管是从中断、异常还是系统调用返回,最终都调用ret_from_sys_call(),由这个函数进行调度标志的检测,如果必要,则调用调用调度程序。那么,为什么从系统调用返回时要调用调度程序呢?这当然是从效率考虑。从系统调用返回意味着要离开内核态而返回到用户态,而状态的转换要花费一定的时间,因此,在返回到用户态前,系统把在内核态该处理的事全部做完。

用老师的话总结如下:

  • 中断处理过程(包括时钟中断、I/O中断、系统调用和异常)中,直接调用schedule(),或者返回用户态时根据need_resched标记调用schedule();

  • 内核线程可以直接调用schedule()进行进程切换,也可以在中断处理过程中进行调度,也就是说内核线程作为一类的特殊的进程可以主动调度,也可以被动调度;

  • 用户态进程无法实现主动调度,仅能通过陷入内核态后的某个时机点进行调度,即在中断处理过程中进行调度。

每个时钟中断(timer interrupt)发生时,由三个函数协同工作,共同完成进程的选择和切换,它们是:schedule()、do_timer()及ret_form_sys_call()。我们先来解释一下这三个函数:

schedule():进程调度函数,由它来完成进程的选择(调度);

do_timer():暂且称之为时钟函数,该函数在时钟中断服务程序中被调用,是时钟中断服务程序的主要组成部分,该函数被调用的频率就是时钟中断的频率即每秒钟100次(简称100赫兹或100Hz);

ret_from_sys_call():系统调用返回函数。当一个系统调用或中断完成时,该函数被调用,用于处理一些收尾工作,例如信号处理、核心任务等等。

进程的切换

  • 为了控制进程的执行,内核必须有能力挂起正在CPU上执行的进程,并恢复以前挂起的某个进程的执行,这叫做进程切换、任务切换、上下文切换;

  • 挂起正在CPU上执行的进程,与中断时保存现场是不同的,中断前后是在同一个进程上下文中,只是由用户态转向内核态执行;

  • 进程上下文包含了进程执行需要的所有信息

    • 用户地址空间: 包括程序代码,数据,用户堆栈等

    • 控制信息 :进程描述符,内核堆栈等

    • 硬件上下文

进程上下文切换由以下4个步骤组成:
1)决定是否作上下文切换以及是否允许作上下文切换。包括对进程调度原因的检查分析,以及当前执行进程的资格和CPU执行方式的检查等。在操作系统中,上下文切换程序并不是每时每刻都在检查和分析是否可作上下文切换,它们设置有适当的时机。
(2)保存当前执行进程的上下文。这里所说的当前执行进程,实际上是指调用上下文切换程序之前的执行进程。如果上下文切换不是被那个当前执行进程所调用,且不属于该进程,则所保存的上下文应是先前执行进程的上下文,或称为“老”进程上下文。显然,上下文切换程序不能破坏“老”进程的上下文结构。
(3)使用进程调度算法,选择一处于就绪状态的进程。

(4)恢复或装配所选进程的上下文,将CPU控制权交到所选进程手中。

其中关系最重要的就是schedule函数:

Linux系统的一般执行过程

最一般的情况:正在运行的用户态进程X切换到运行用户态进程Y的过程

 

  1. 正在运行的用户态进程X

  2. 发生中断——save cs:eip/esp/eflags(current) to kernel stack,then load cs:eip(entry of a specific ISR) and ss:esp(point to kernel stack).

  3. SAVE_ALL //保存现场

  4. 中断处理过程中或中断返回前调用了schedule(),其中的switch_to做了关键的进程上下文切换

  5. 标号1之后开始运行用户态进程Y(这里Y曾经通过以上步骤被切换出去过因此可以从标号1继续执行)

  6. restore_all //恢复现场
  7. iret - pop cs:eip/ss:esp/eflags from kernel stack

  8. 继续运行用户态进程Y

特殊情况:

 

  • 通过中断处理过程中的调度时机,用户态进程与内核线程之间互相切换和内核线程之间互相切换,与最一般的情况非常类似,只是内核线程运行过程中发生中断没有进程用户态和内核态的转换;

  • 内核线程主动调用schedule(),只有进程上下文的切换,没有发生中断上下文的切换,与最一般的情况略简略;

  • 创建子进程的系统调用在子进程中的执行起点及返回用户态,如fork;

  • 加载一个新的可执行程序后返回到用户态的情况,如execve;

 代码分析:

*next = pick_next_task(rq, prev);//进程调度算法都封装这个函数内部
*context_switch(rq, prev, next);//进程上下文切换
*switch_to利用了prev和next参数(prev指向当前进程,next指向被调度的进程)

asm volatile("pushfl "
"pushl %p " \将当前进程的堆栈基址压栈
"movl %%esp,%[prev_sp] " \把当前的栈顶保存起来,保存到thread.sp "movl %[next_sp],%%esp " \把下个进程的栈顶放到esp寄存器里面
"movl $1f,%[prev_ip] " \保存当前进程的eip
"pushl %[next_ip] " \将下一个进程的起点压到堆栈中来 next进程的 栈顶就是它的起点
__switch_canary
"jmp __switch_to "
"1: " \开始执行next进程的第一条指令
"popl %p " \pop的原因是因为next进程作为prev进程是曾经push过
"popfl "

总结:

    Linux系统一般执行过程主要是由进程的调度时机和进程的切换机制组成,进程的调度时机主要分上述四种情况,其中主要由schedule函数来完成进程的调度,switch_to完成关键上下文的切换。

    进程调度的时机有以下四种:中断处理过程(包括时钟中断、I/O中断、系统调用和异常)中,直接调用schedule(),或者返回用户态时根据need_resched标记调用schedule();内核线程可以直接调用schedule()进行进程切换,也可以在中断处理过程中进行调度,也就是说内核线程作为一类的特殊的进程可以主动调度,也可以被动调度;用户态进程无法实现主动调度,仅能通过陷入内核态后的某个时机点进行调度,即在中断处理过程中进行调度以及在设备驱动程序的调度,具体情况参见实验分析。

    系统进程切换机制和中断处理过程不一样,是在同一进程中进行切换,两者是需要区分开的。具体分析见实验分析。

           谢谢老师详细的指导!

    

参考资料:

··   1.http://os.51cto.com/art/201003/187407.htm

..        2.http://oss.org.cn/kernel-book/ch05/5.3.2.htm

原文地址:https://www.cnblogs.com/emochuanshuo/p/4458660.html