Linux进程调度分为主动调度和被动调度两种方式:
自愿的调度随时都可以进行,内核里可以通过schedule()启动一次调度,当然也可以将进程状态设置为TASK_INTERRUPTIBLE、TASK_UNINTERRUPTIBLE,暂时放弃运行而进入睡眠;用户空间可以通过pause()达到同样的目的;如果为这种暂时的睡眠放弃加上时间限制,内核态有schedule_timeout,用户态有nanosleep()用于此目的;注意内核中这种主动放弃是不可见的,隐藏在每一个可能受阻的系统调用中,如open()、read()、select()等。
被动调度发生在系统调用返回的前夕、中断异常处理返回前、用户态处理软中断返回前。
自从Linux 2.6内核后,linux实现了抢占式内核,即处于内核态的进程也可能被调度出去。比如一个进程正在内核态运行,此时一个中断发生使另一个高权值进程就绪,在中断处理程序结束之后,linux2.6内核之前的版本会恢复原进程的运行,直到该进程退出内核态才会引发调度程序;而linux2.6抢占式内核,在处理完中断后,会立即引发调度,切换到高权值进程。为支持内核代码可抢占,在2.6版内核中通过采用禁止抢占的自旋锁来保护临界区。在释放自旋锁时(spin_unlock_mutex),同样会引发调度检查。而对那些长期持锁或禁止抢占的代码片段插入了抢占点,此时检查调度需求,以避免不合理的延迟发生。而在检查过程中,调度进程很可能就会中止当前的进程来让另外一个进程运行,只要新的进程不需要持有该锁。
进程调度在近几个版本中都进行了重要的修改。我们以2.6.9版为例过行分析。在进行具体的代码分析之前。我们先学习一下关于进程调度的原理。
1:进程类型
在linux调度算法中,将进程分为两种类型,即:I/O消耗型和CPU消耗型。例如文本处理程序与正在执行的Make的程序。文本处理程序大部份时间都在等待I/O设备的输入,而make程序大部份时间都在CPU的处理上。因此为了提高响应速度,I/O消耗程序应该有较高的优先级,才能提高它的交互性。相反的,Make程序相比之下就不那么重要了,只要它能处理完就行了。因此,基于这样的原理,linux有一套交互程序的判断机制。
在task_struct结构中新增了一个成员:sleep_avg此值初始值为100。进程在CPU上执行时,此值减少。当进程在等待时,此值增加。最后,在调度的时候。根据sleep_avg的值重新计算优先级。
2:进程优先级
正如我们在上面所说的:交互性强的需要高优先级,交互性弱的需要低优先级。在linux系统中,有两种优先级:普通优先级和实时优先级。我们在这里主要分析的是普通优先级,实时优先级部份可自行了解。
3:运行时间片
进程的时间片是指进程在抢占前可以持续运行的时间。在linux中,时间片长短可根据优先级来调整。进程不一定要一次运行完所有的时间片。可以在运时的中途被切换出去。
4:进程抢占
当一个进程被设为TASK_RUNING状态时,它会判断它的优先级是否高于正在运行的进程,如果是,则设置调度标志位,调用schedule()执行进程的调度。当一个进程的时间片为0时,也会执行进程抢占。
Linux2.6实现O(1)调度,每个CPU都有两个进程队列,采用优先级为基础的调度策略。内核为每个进程计算出一个反映其运行“资格”的权值,然后挑选权值最高的进程投入运行。在运行过程中,当前进程的资格随时间而递减,从而在下一次调度的时候原来资格较低的进程可能就有资格运行了。到所有进程的资格都为零时,就重新计算。
调度程序运行时,要在所有可运行的进程中选择最值得运行的进程。选择进程的依据主要有进程的调度策略(policy)、静态优先级(priority)、动态优先级(counter)、以及实时优先级(rt-priority)四个部分。首先,Linux从整体上区分为实时进程和普通进程,二者调度算法不同,实时进程优先于普通进程运行。进程依照优先级的高低被依次调用,实时优先级级别最高。
从某种意义上讲,所有位于当前队列的任务都将被执行并且都将被移到“过期”队列之中(实时进程则例外,交互性强的进程也可能例外)。当这种事情发生时,情况就会有所变化,队列就会被进行切换,原来的“过期”队列成为当前队列,而空的当前队列也就变成了过期队列。
schedule()函数是完成进程调度的主要函数,并完成进程切换的工作。schedule()用于确定最高优先级进程的代码非常快捷高效,其性能的好坏对系统性能有着直接影响,它在/kernel/sched.c 中的定义如下:
{
...
int idx;
...
preempt_disable();
...
idx = sched_find_first_bit( array -> bitmap);
queue = array -> queue + idx;
next = list_entry( queue -> next, task_t, run_list);
...
prev = context_switch( rq, prev, next);
...
}
其中,sched_find_first_bit()能快速定位优先级最高的非空就绪进程链表,运行时间和就绪队列中的进程数无关,是实现 O(1)调度算法的一个关键所在。schedule()的执行流程:
首先,调用 pre_empt_disable(),关闭内核抢占,因为此时要对内核的一些重要数据结构进行操作,所以必须将内核抢占关闭;其次,调用 sched_find_first_bit()找到位图中的第1个置1的位,该位正好对应于就绪队列中的最高优先级进程链表;再者,调用context_switch()执行进程切换,选择在最高优先级链表中的第1个进程投入运行;详细过程如图所示:
图中的网格为140位优先级数组,queue[7]为优先级为7的就绪进程链表。此种算法保证了调度器运行的时间上限,加速了候选进程的定位过程。
时间片的计算方法与时机:
Linux2.4 调度系统在所有就绪进程的时间片都耗完以后在调度器中一次性重新计算,其中重算是用for循环相当耗时。
Linux2.6为每个CPU保留 active和expired两个优先级数组,active 数组中包含了有剩余时间片的任务,expired数组中包含了所有用完时间片的任务。当一个任务的时间片用完了就会重新计算其时间片,并插入到expired队列中,当 active队列中所有进程用完时间片时,只需交换指向active和expired队列的指针即可。此交换是实现O(1)算法的核心,由schedule()中以下程序来实现:
array = rq ->active;
if (unlikely(!array->nr_active)) {
rq -> active = rq -> expired;
rq -> expired = array;
array = rq ->active;
...
}
Linux进程有140个优先级,前100个分配给实时进程,后40个给普通进程使用。
在 Linux2.6 中,仍有三种调度策略:SCHED_OTHER、SCHED_FIFO 和 SCHED_RR。
SCHED_ORHER:普通进程,基于动态优先级进行调度,其动态优先级可以理解为调度器为每个进程根据多种因素计算出的权值。
Linux2.6中,优先级prio的计算不再集中在调度器选择next进程时,而是分散在进程状态改变的任何时候,这些时机有:
进程被创建时;
休眠进程被唤醒时;
从TASK_INTERRUPTIBLE 状态中被唤醒的进程被调度时;
因时间片耗尽或时间片过长而分段被剥夺 CPU 时;
在这些情况下,内核都会调用 effective_prio()重新计算进程的动态优先prio并根据计算结果调整它在就绪队列中的位置。
struct task_struct{
...
int prio,static_prio;
prio 是动态优先级,static_prio 是静态优先级(与最初nice相关)
...
prio_array_t *array;
记录当前 CPU 的活跃就绪队列
unsigned long sleep_avg;
进程的平均等待时间,取值范围[0,MAX_SLEEP_AVG],初值为0。
sleep_avg反映了该进程需要运行的紧迫性。进程休眠该值增加,如果进程当前正在运行该值减少。是影响进程优先级最重要的元素。值越大,说明该进程越需要被调度。
...
};
SCHED_FIFO:实时进程,实现一种简单的先进先出的调度算法。
SCHED_RR:实时进程,基于时间片的SCHED_FIFO,实时轮流调度算法。
SCHED_FIFO与SCHED_RR的区别是:当进程的调度策略为前者时,当前实时进程将一直占用CPU直至自动退出,除非有更紧迫的、优先级更高的实时进程需要运行时,它才会被抢占CPU;当进程的调度策略为后者时,它与其它优先级相同的实时进程以实时轮流算法去共同使用CPU,用完时间片放到运行队列尾部,注意实时进程并不会放入过期队列中。
虽然在一个CPU内,实时进程的调度方式可以认为是严格优先级的,但是对于SMP系统,每个CPU都有自己的运行队列,实时进程被分配到各CPU队列,高优先级的实时进程并不一定比低优先级的先运行。
Linux2.6内核本身就是可抢占的,具有一定的实时性;而一些实时补丁的出现,更加增强了linux的实时性,达到软实时的标准,这其中著名的是Ingo's RT patch。
该补丁把中断(IRQ)和软中断(softIRQ)全部线程化并赋予不同的优先级,实时任务可以有比中断线程更高的优先级;它使用Mutex替代spinlock来使得自旋锁完全可抢占;另外分解了内核中锁的粒度,增加了内核抢占点,进一步降低了延时。由于中断已经线程化了,很多中断关闭就没必要了,因而消除了很多中断关闭区域。
为了能并入主流内核,Ingo Molnar的实时补丁也采用了非常灵活的策略,它支持四种抢占模式:
1.No Forced Preemption (Server),这种模式等同于没有使能抢占选项的标准内核,主要适用于科学计算等服务器环境。
2.Voluntary Kernel Preemption (Desktop),这种模式使能了自愿抢占,但仍然失效抢占内核选项,它通过增加抢占点缩减了抢占延迟,因此适用于一些需要较好的响应性的环境,如桌面环境,当然这种好的响应性是以牺牲一些吞吐率为代价的。
3.Preemptible Kernel (Low-Latency Desktop),这种模式既包含了自愿抢占,又使能了可抢占内核选项,因此有很好的响应延迟,实际上在一定程度上已经达到了软实时性。它主要适用于桌面和一些嵌入式系统,但是吞吐率比模式2更低。
4.Complete Preemption (Real-Time),这种模式使能了所有实时功能,因此完全能够满足软实时需求,它适用于延迟要求为几十微秒或稍低的实时系统。