Linux中断与进程切换,Linux进程切换与进程调度时机 | Chaos

13 03 2013

Linux进程切换与进程调度时机

0引言

本文档仅讨论Linux环境下的进程切换与进程调度时机的相关主题

文档内所涉及到的Linux内核源码版本为2.4.0

1概述

在多进程的操作系统中,进程调度是一个全局性的、关键性的问题,它对系统的总体设计、系统的实现、功能的设置以及各方面的性能都有着决定性的影响。那么,需要考虑的具体问题主要有:

(1) 调度的时机:在什么情况下、什么时候进程调度。

(2) 如何实现进程间的切换

下面将具体说明。

2调度时机

进程调度分为自愿和非自愿两种。

2.1自愿调度

自愿的调度随时都可以进行。一个进程可以通过schedule( )启动一次调度,也可以在调用schedule( )之前,将本进程的状态置为TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE,暂时放弃运行而进入睡眠。

从应用的角度看,只有用户空间自愿放弃运行时可见的;而在内核中自愿放弃运行则是不可见的,它隐藏在其他可能受阻的系统调用中。几乎所有涉及到外设的系统调用,如open( )、read( )、write( )和select( )等,都是可能受阻的。

2.2非自愿调度

非自愿调度,即强制地发生在每次从系统调用返回的前夕,以及每次从中断或异常处理返回到用户空间的前夕。需要注意的是,只有在用户空间(当CPU在用户空间运行时)发生的中断或异常才会引起调度。

“从系统空间返回到用户空间”只是发生调度的必要条件,而不是充分条件。具体是否发生调度还要看有无此种要求。只有在当进程的task_struct结构中的need_resched字段为非0时才会转到reschedule处调用schedule( )。need_resched这个字段由内核设置。

下面,将选择一个主动调度的例子来分析进程的调度和切换过程。

3进程切换

主动调度,也就是由当前进程自愿调用schedule( )暂时放弃运行。我们选择的例子是进程调用exit( )时的情况。一个正在结束的进程在do_exit( )中的最后一件事情就是调用schedule( ),见do_exit( )代码。

34903554_1.png

schedule( )代码在/kernel/sched.c中,图下图所示。

34903554_2.png

schedule( )只能由进程在内核中主动调用,或者是当前进程从系统空间返回用户空间的前夕被动地发生,而不能再一个中断服务程序内部发生。即使一个中断服务程序有调度的要求,也只能通过吧当前进程的need_resched字段设为1来表达这种要求,而不能直接调用schedule( )。

schedule( )里主要调用的宏或函数依次为:

schedule() –> context_switch() –> switch_to –> __switch_to()

schedule是主调度函数,涉及到具体的调度算法。当schedule()需要暂停A进程的执行而继续B进程的执行时,就发生了进程之间的切换。

进程切换主要有两部分:

(1) 切换全局页表项,这个切换工作由context_switch()完成。

(2) 切换内核堆栈和硬件上下文,其中switch_to和__switch_to()主要完成第二部分。 更详细的,__switch_to()主要完成硬件上下文切换,switch_to主要完成内核堆栈 切换。

所谓进程的切换主要是堆栈的切换,这是由宏操作switch_to( )完成的,定义于include/asm_i386/system.h中

34903554_3.png

switch_to的参数prev,next,last不是值拷贝,而是它的调用者context_switch()的局部变量。局部变量是通过%ebp寄存器来索引的,也就是通过n(%ebp),n是编译时决定的,在不同的进程的同一段代码中,同一局部变量的n是相同的。有关局部变量如何索引的问题,可以参考这里和这里。 在switch_to中,发生了堆栈的切换,即ebp发生了改变,所以要格外留意在任一时刻的局部变量属于哪一个进程。关于__switch_to()这 个函数的调用,并不是通过普通的call来实现,而是直接jmp,函数参数也并不是通过堆栈来传递,而是通过寄存器来传递。

进程切换发生在内核态模式,由于进程从用户态陷入内核态时已经将用户 进程用到通用寄存器的值,用户态的一些特殊寄存器cs,eip,ss,esp等保存在进程的内核堆栈之中,因此在内核态进行进程切换的主要工作是完成内核 堆栈的切换和相关硬件上下文的切换。

设pre和next分别是切换前后的两个进程,那么这个切换主要需要完成的工作有:

(1) 首先需要将CPU的esp寄存器的值保存到pre进程中,然后再将CPU的esp设置为 next进程的esp(因为内核通过esp识别当前运行的进程,内核堆栈 栈顶指针的切 换通常意味着进程的切换)。

(2) 要更新Task State Segment (TSS),更新的变量是esp0(内核堆栈指针)和IO许可权 限位图。对于Linux系统来说同一个CPU上所有的进程共用一个TSS,进程切换了, 因此TSS需要随之改变。Linux系统中主要从两个方面用到了TSS: 一是任何进程 从用户态陷入内核态都必须从TSS获得内核堆栈指针,二是用户态读写IO需要访 问TSS的权限位图。

(3) 对于pre进程要将 ebp和eflags压入内核堆栈,对于next进程要将ebp和eflags弹出内核堆栈。

下面结合代码说明切换的过程,一共分四步,图下图所示:

第一步:即movl %%esp,%0也就是将寄存器esp中的值保存在进程A的thread.esp中。

第二步:即movl %3,%%esp也就是将进程B的thread.esp的值赋给寄存器esp。(实际上 这个值就是上一次从B中切换走的时候执行的第一步的结果。为了要返回,必须为以后考虑周全。)

第三步:即movl $1f,%1其中1f就是说程序后面标号为1的地方,将标号为1的地方的 代码的地址保存到A的thread.eip中。

第四步:即pushl %4,将进程B的thread.eip的值压栈,此时的esp指向已是进程B的 堆栈。(实际上此时的thread.eip就是上一次从B中切换走的时候第三步执行的 结果,即标号一得位置。所以任何进程恢复运行,首先肯定是执行的标号1的 代码。)

34903554_4.png

这里要说明的是,pushl %4后面的一句代码是调转jmp __switch_to 而__switch_to是个函数,他执行完成以后会有一个ret的操作,即将栈中的第一个地址作为函数返回的地址,所以就会跳到标号1的地方去执行代码了。

由于__switch_to的代码在schedule()中,而shedule()函数又在其他系统调用函数中,比如sys_exit()中,所以先返回到调用B进程上次切换走时的schedule()中,然后返回到调用schedule()的系统调用函数中,最后系统调用又是在用户空间调用的,所有返回到系统调用的那个地方,接着执行用户空间的代码。这样就彻底的回到了B进程。注意由于此时的返回路径是根据B堆栈中保存的返回地址来返回的,所以肯定会返回到B进程中。

4参考资料

[1] 毛德操等著.LINUX内核源代码情景分析(上册).杭州:浙江大学出版社, 2001.7

欢迎转载,但必须以超链接形式标明文章原始出处和作者信息及版权声明

作者:Chao Yang

Comments are currently closed.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值