进程调度

来源:互联网 发布:unity3d角色动画教程 编辑:程序博客网 时间:2024/05/18 01:46

进程调度算法概述

进程调度算法就进程来分,分为三类:批处理、交互式、实时。下面将分别进行描述。

批处理系统

先到先服务

这种调度算法属于非抢占式,只有当前进程主动放弃处理器别的进程才会有机会运行。这个算法只有一个运行队列,一个进程进入就绪状态时就自动转移到运行队列的队尾等待调用。该算法简单,容易理解,但在处理器使用率上达不到要求。

最短作业优先

该算法可用在运行时间可以预知的系统中。当系统中同时有若干同等重要的作业需要运行的时候,最短作业优先可以有效降低短作业的等待时间。但该算法的运行环境必须是可以预知作业长短的,而且任务必须是同时就绪的。

最短剩余时间优先算法

这个算法是最短作业优先的抢占式版本。使用这样调度算法,调度程序会挑选剩余时间最短的作业运行。当然,这要求作业的运行时间可以预知。该算法可以是新来的短作业得到较好的服务,解决了最短优先算法中对作业起始时间同时开始的要求。

交互式系统

时间片轮转调度

在这种调度算法中,每个进程被分配一个时间段,称之为时间片,也就是进程允许运行的时间。如果时间片结束时进程仍然在运行,则CPU将被抢占并分配给另一个进程。如果在时间片结束前阻塞或结束,则CPU马上进行切换。时间片轮转是目前最古老,最简单也是使用最广的一种调度策略。

优先级调度

时间片调度是一种很公平的调度策略,但是这种公平并不一定是好的。它默认了所有的作业都处在同一种优先级下。但实际情况往往并不如此。在优先级调度中,优先级较高的作业更容易获得CPU,但为了不让这种作业无休止的运行下去,

实时系统

实时系统是一些时间因素非常重要的系统,往往要就系统对突发性的事件进行快速响应。实时系统通常分为硬实时和软实时。在硬实时系统中,进程必须满足时间要求,但在软实时系统中进程可以被允许偶尔的“犯规”。实时系统中程序将分为几个进程,每个进程的行为都是可以预知的。每个进程的生存时间很短。

实时调度算法可以分为静态的和动态的。静态算法要求系统启动前就完成调度决策,后者则像其它系统一样在运行时进行调度。

Linux操作系统进程调度算法

这里介绍的Linux进程调度算法是基于在2.6.x的内核。它的主要实现在kernel/sched.c中。该进程调度算法的主要目标如下:

l         实现低时间复杂度的调度。

l         实现对SMP的兼容。

l         对交互式程序有所倾斜,保证系统在高负载下的响应。

l         保证公平。在设计目标范围内,消除明显的不公平。

运行队列

在Linux系统中存在一种数据结构叫做运行队列。它的定义在kernel/sched.c中。运行队列(runqueue)和处理器一一对应,是一个关于可以执行的进程的链表。任何一个处于运行状态的进程都属于一个可执行队列。运行队列数据结构中包含了保护该runqueue的自旋锁、可运行任务数、队列最后被换出的时间、当前运行任务、等待I/O操作的任务数目等。运行队列中的自旋锁保证了该数据结构被调用时首先应该被锁住。在SMP,处理器锁住其它处理器运行队列是被允许的但很少出现。在进行运行队列的所操作时,必须保证以执行队列地址从低到高的顺序,不然会出现死锁的情况。

优先级数组

每个运行队列都有两个优先级数组,一个活跃的和一个过期的。优先级数据是Linux能够在低时间复杂度情况下进行调度的基础。优先级数组包含一系列的优先级队列,同时还有一个优先级位图。前者使处理器拥有各个优先级上的进程链表,后者则可以查找当前拥有最高优先级的进程。

宏MAX_PRIO定义了系统拥有的优先级的个数,一般是140。针对这140个优先级,有140个struct list_head结构体与之对应。优先级数组包含一个优先级位图数组。当调度开始时,每个优先级位图位都被置为0。只有当拥有某一个优先级的进程进行运行时,该优先级对应的位图位被置为1。

优先级数组还包含一个叫做struct list_head的队列,每个struct list_head元素都对应一个优先级,包含该处理器相应优先级的全部可运行进程。

时间片的重新计算

在系统中所有时间片都用完时,调度程序会重新计算每个进程的时间片。决定每个进程时间片时间是个比较耗时的过程,在2.6.x内核中Linux调度程序没有使用循环分配的方式进行。在这个过程中使用了处理器所维护的两个优先级数组即活动数组和过时数组。当一个进程的时间片耗尽时将被从活动优先级数组移动到过时优先级数组,在此之前,它的时间片已经被计算好了。重新计算时间片的活动就变成了在两个优先级数组之间进行切换的活动。由于数组是通过指针进行访问的,所以重新计算时间片的耗时就是在优先级数组之间交换指针的耗时。这种设计是新的Linux调度程序保持在低时间复杂度的关键。

schedule()

schedule()完成了选择并进入下一个进程的操作。schedule()函数独立于单个的处理器,如果有进程将被抢占,schedule()函数将被调用。

schedule()在选择时需要在在指定处理器的活动优先级数组中找个到优先级最高的任务,也就是在该优先级位图中第一个被设置的位对应的任务。

计算优先级和时间片

在计算任务的优先级时,由于要保证系统的快速响应所以对交互型任务的计算将有所不同。

每个Linux任务都会有一个称之为nice值的优先级,范围为-20到+19,默认值为0。nice值越大代表优先级越低。这个值在任务的进程空间里面放在struct task_struct的static_prio域中。nice优先级称之为静态优先级,由用户指定后不能随意改变。而调度程序要指定的叫做动态优先级,它存放在同一个结构中的prio域中。动态优先级由静态优先级和进程的交互性来决定。

调度程序会根据进程休眠的时间长短来判断该进程是I/O消耗型进程或者处理器消耗型进程。从而获得对该进程交互性的判断。

完成了优先级的确定后,时间片的计算就很简单了,以静态优先级为基础。在新进程被创建时,新建的进程和父进程均分剩余的时间片,防止用户通过不断的创建进程获取时间片。时间片耗尽后,task_timeslice()函数负责根据进程的静态优先级计算新的时间片。时间片按优先级比例缩放,符合时间片的数值范围要求。nice为-20的进程能获得8000ms的时间片长度,最低优先级进程可以获得5ms时间片。默认优先级则可以或的100ms的时间片。

为了进一步提高系统响应能力,调度程序对交互性较强的进程会特殊照顾。当它时间片用完后,调度程序不会将其放入过期数组中,而是依然放在活动数组。当然,虽然被放入了活动数组,该进程不会立即执行,而是和优先级相同的进程轮流被调度和执行。

睡眠和唤醒

一些程序由于各种原因不愿意被执行,这些进程被称为处在阻塞状态下。不愿意执行的原因有很多,一般都是为了等待某一事件,比如I/O或某个资源的使用权。要阻塞的进程首先把自己标记为休眠状态,把自己从可执行队列移出,放入等待队列,然后调用schedule()函数交出处理器。如果一个休眠的进程等待的事件已经发生则将自己标记为可以运行状态,然后移动到可执行队列。在Linux进程调度中,休眠分为两种,一种是可打断休眠,一种是不可打断休眠。等待队列是由一些处在休眠状态的进程组成的简单链表。

SMP中的负载均衡

在SMP架构中,每个处理器都有一个自己的进程链表、可执行队列和锁。处理器只对属于自己的进程进行调度操作。从调度程序的角度看,所有的处理器是一个整体,需要完成整体性能的提高,不能出现处理器负载失衡的情况。负载均衡程序来完成,这部分工作也可以算作调度程序的一部分。

负载均衡程序有kernel/sched.c中的函数load_balance()函数实现。举个简单的例子,在schedule()执行的时候只要当前的可执行队列为空,load_balance()就会被调用,它的主要操作是找到一些进程并且插入到那个为空的队列。

load_balance()函数一般会首先找到最繁忙的队列,然后从该队列中选在一个优先级数组,最好是过期数组。然后通过优先级位图找到含有进程并且优先级最高的链表。分析找到的链表,选择不是在执行,并且没有和处理器绑定,没有在高速缓存中的进程。抽取满足上述要求的进程到当前队列中。如果此时依然不均衡则重复。

总结

进程调度是内核重要的组成部分,运行着的进程首先在使用计算机。要设计一个可以满足各种需求的调度算法是不现实的。不过Linux内核的新调度程序尽量满足各个方面,并以较完善的可伸缩性位所有情况提供了比较好的解决方案。

http://blog.csdn.net/beff2047/article/details/3639703