Linux进程调度原理:
调度程序是内核的组成部分,它负责选择下一个要运行的进程。可以看作是在可运行态进程之间分配有限的处理器时间资源的内核子系统。只有通过调度程序的合理调度,系统资源才能最大程度的发挥作用,多进程才会有并发执行的效果。
多任务操作系统是能同时并发地交互执行多个进程的操作系统。可以划分两类:非抢占式多任务和抢占式多任务。Linux属于后者,即由调度程序来决定什么时候停止一个进程的运行以便其他进程能够得到执行的机会。进程在被抢占之前能够运行的时间是预先设置好的,称为时间片,即每个可运行进程可用的处理器时间。Linux采用动态方法计算时间片。
进程可以被分为I/O消耗型和处理器消耗型。前者要求进程响应时间短,因此要经常处于可运行状态。后者则相反,应减少运行频率,增加其运行时间。
调度算法中最基本的就是基于优先级的调度。优先级高的先运行,低的后运行。相同优先级的进程按轮转方式进行调度(一个接一个)。一般优先级高的进程时间片也较长。Linux内核提供了两组独立的优先级范围,第一种是nice值,范围从-20到19,默认值是0,-20优先级最高。第二种是实时优先级,其值是可配置的,默认从0到99。
时间片分配:调度程序提供较长的时间片给I/O消耗型,以此来提高交互性的速度,对于处理器消耗型,进程的运行和阻塞与用户关系不大,则分配较少的时间片,具体如图:
当一个进程的时间片耗尽时,就不在投入运行了,除非等到其他所有的进程的时间片都消耗尽,然后所有的进程时间片重新计算。
Linux系统是抢占式的,当一个进程进入TASK_RUNNING状态,内核会检查它的优先级是否高于当前正在执行的进程。若是这样,调度程序就会被唤醒,抢占当前正在运行的进程并运行新的可执行进程。当一个进程的时间片为0时,同样。
Linux调度算法:
1.充分实现O(1)调度。不管有多少进程,新调度程序采用的每个算法都能在恒定的时间内完成。
2.全面实现SMP的可扩展性。每个处理器拥有自己的锁和自己的可执行队列。
3.强化SMP的亲和力。尽量将相关一组任务分配给一个CPU进行持续的执行。只有在需要平衡任务队列大小的时候才在CPU之间移动进程。
4.加强交互能力。即使在系统处于相当负载的情况下,也能保证系统的响应,并立即调度交互式进程。
5.保持公平。在合理设定的时间范围内,没有进程会处于饥饿状态。同样的,也没有进程能够显失公平的得到大量的时间片。
6.虽然最常见的优化情况是系统中只有1~2个可运行进程,但是优化也完全有能力扩展到具体多处理器且每个处理器上运行多个进程的系统中。
调度程序中最基本的数据结构是运行队列(runqueue)。此为给定处理器上的可执行进程的链表,每个处理器一个。结构体如下:
在对可执行队列操作前,应该先锁住它,在其拥有者读取或写队列成员的时候,可执行队列的锁用来防止对列被其他代码改动。
每个运行队列都有两个优先级数组,一个活跃的一个过期的。其类型为prio_array类型。结构如下:
MAX_PRIO定义了系统拥有的优先级个数。默认值是140.BITMAP_SIZE是优先级位图数组的大小,类型为unsigned long类型,长32位,如果每一位代表一个优先级的话,140个优先级需要5个这种类型的才能表示。所以bitmap含有5个数组项,共160位。根据位图中的哪一位被置1可得出进程的优先级,第七位被置1,则该进程优先级是7.每个优先级数组还包含一个叫做struct list_head的队列,其中每一个元素都是一个 struct list_head类型的队列。每个链表与一个给定的优先级相对应。事实上,每个链表都包含该处理器队列上相应优先级的全部可执行进程。还有一个计数器nr_active。它保存了该优先级数组内可执行进程的数目。
重新计算时间片
一般的操作系统在所有进程的时间片都用完时,都采用一种显示的方法来重新计算每个进程的时间片。典型的实现是循环访问每个进程:
for(系统中的每个任务){
重新计算优先级
重新计算时间片
}
这种方式存在一些弊端:1.耗费时间长。2.重算是必须靠锁的形式来保护任务队列和每个进程描述符,加剧了锁的争用。3.重新计算时间片的时机是不确定的,这会给时间确定性要求很高的实时程序带来麻烦。4.它的实现很粗糙。
新的Linux调度程序减少了对循环的依赖。采用为每个处理器维护两个优先级数据:活动数据和过期数组。当一个进程的时间片耗尽时,它会被移动到过期数组,在此之前,时间片已经重新计算好了。该操作由schedule()完成。这种做法是O(1)调度程序的核心。只需完成一个两个步骤就能实现数组间的切换,解决了传统的弊端。
选定下一个进程并切换到它去执行是由schedule()函数完成的。当内核代码想休眠时,会直接调用该函数,或者哪个进程将被抢占,该函数也会被唤醒执行。schedule()函数独立于每个处理器执行,所以每个CPU都要对下一次该运行哪个进程做出自己的判断。
计算优先级和时间片
进程拥有一个初始的优先级,叫做nice值,范围从-20到19,默认为0。保存在进程task_struct中的static_prio域中,称为静态优先级。而调度程序要用到的动态优先级放在prio域中。动态优先级通过静态优先级和进程的交互性函数关系计算而来。
Linux记录了一个进程用于休眠和用于执行的时间。该值存放在task_struct的sleep_avg域中。它的范围是从0到MAX_SLEEP_AVG。通过优先级奖罚制度,计算出进程的优先级。
重新计算时间片只要以静态优先级为基础就可以了。新建的子进程和父进程平分剩余的进程时间片。
调度程序还提供了另外一种机制来支持交互进程:如果一个进程的交互性非常强,那么当它的时间片用完后,它会被再防止到活动数组而不是过期数组中。当活动数组中没有剩余的进程的时候,此时活动数组会变成过期数组,过期数组替代活动数组。
睡眠和唤醒:
休眠(被阻塞)的进程处于一个特殊的不可执行的状态,进程休眠的原因有很多种,但是肯定是为了等待一些事件。两种状态:TASK_INTERRUPTIBLE和TASK_UNUNTERTUPTIBLE。后者会忽略信号。休眠通过等待队列处理。进程通过执行下面几个步骤将自己加入到一个等待队列中:
1.调用DECLARE_WAITQUEUE()创建一个等待队列的项
2.调用add_wait_queue()把自己加入到队列中,该队列会再进程等待的条件中满足时唤醒它。当然我们必须在其他地方撰写相关代码,在事件发生时,对等待队列执行wake_up()操作。
3.将进程的状态变更为TASK_INTERRUPTIBLE或TASK_UNUNTERTUPTIBLE
4.如果状态被设置为TASK_INTERRUPTIBLE,则信号唤醒进程。这就是所谓的伪唤醒,因此检查并处理信号。
5.检查条件是否为真,如果是的话,就没必要休眠了。如果条件不为真,调用schedule()。
6.当进程被唤醒的时候,它就会再次的检查条件是否为真。如果是,它就退出循环,如果不是,它就再次调用schedule()并一直重复这个操作。
7.当条件满足后,进程将自己设置为TASK_RUNNING并调用remove_wait_queue()把自己移出等待队列。
负载平衡程序:
每个处理器都有各自的可执行队列和锁,即每个处理器拥有一个自己的进程链表。它只对属于自己的这些进程进行相关的调度工作。当不同的处理器上可执行的进程数相差过大时,此时负载平衡程序开始执行,首先查询当前拥有可执行进程数最多的处理器,若是比其他多出25%及以上,则将其中的进程选取优先级高的进程移到空闲的处理器中。
参考自:《Linux Kernel Development》.