调度程序,负责选择下一个要运行的进程.在单处理器上,LINUX系统也许有100多个进程在内存里面,但是只有一个处于可运行状态.调度程序选择下一个要运行的进程的依据是什么呢?
4.1 策略
这里所谓的策略,是指决定调度程序何时让什么进程运行.调度程序的执行意味着要从等待执行的进程队列里面选出一个合适的进程来运行.
有这样一个情景:
有这样一个系统,它拥有两个运行中的进程:一个文字编辑程序和一个视频编码程序.视频解码程序是一直运行的,比如我们在听音乐或看电影;而文字编辑程序只有我们需要往上而录入字符才会有反应的.一般而言,我们是希望一按下键盘,文字编辑程序就立马响应我们的输入动作.理论上讲,同一时刻,CPU只能处理一个进程,这两个程序之间的执行相互之间是有影响的.而调度程序是如何把二者处理得让用户感觉得两程序运行得都"正常"呢?这就是调度策略的一种活动方式.
4.1.1 I/O消耗型和处理器消耗型的进程
I/O消耗型的进程,是指交互型的进程,是指进程大部分时间用来提交I/O请求或等待I/O请求.典型的例子就是文字编辑,坐等键盘输入字符.主要特点是:等待时间长,执行时间短.其最主要的表现是即时响应性;
处理器消耗型的进程,是指把绝大部分时间消耗在代码的执行上.例如视频解码程序.极端的例子如while(1){}.主要特点是:执行时间比较长.此类进程,调度策略应该尽量降低它们的运行频率,延长它们的执行时间比较适合.其最主要的表现是系统的吞吐量.
4.1.2 进程优先级
回到开头的话题,调度程序要选定下一个进程运行,一个重要的依据便是等待运行的进程的优先级.一个合理的调度策略便是在系统的即时响应性和系统吞吐量之间的矛盾作出一种平衡的仲裁.基于此,LINUX内核实现了一种基于动态优先级的调度方法,即允许调度程序根据需要动态的升降某个进程的优先级.比如说,如果一个进程在I/O等待消耗的时间多于其运行时间,那么该进程明显是I/O消耗型进程,它的优先级会被动态提高;如果一个进程全部时间一下就被消耗干净,那么该进程属于处理器消耗型进程,它的优先级会被降低.
LINUX内核为解决实际应用中的进程调度管理,提供了两组优先级范围:
A.nice值:
Nice值的范围为-20到+19,其值越小,优先级越高,被分配的时间片也就越长;
B.实时优先级:
变化范围为0到99,任何实时进程的优先级都要比普通进程的优先级要高.
4.2 Linux的调度算法
Linux的调度程序定义于kernel/sched.c中.2.6内核的调度程序有以下特性:
.O(1)调度;
.实现SMP可扩展性.主要表现两方面:一、尽量将相关一组任务分配给一个CPU连续执行;二、在需要平衡任务队列大小时可以在CPU之间移动任务队列;
.加强交互性能.主要着重上述的I/O消耗型的进程;
.保证公平.没有进程处于饥饿态,也没有进程显失公平地得到大量时间片;
4.2.1 可执行队列
我们知道,LINUX是一个多任务系统,也就意味着有多个等待投入运行的进程.这些进程往往被组织起来.用来组织LINUX各个进程的组织者叫做运行队列,由结构struck runqueue表示.因此,可执行队列每个处理器上一个,每个可投入执行的进程都唯一地归属于一个执行队列(运行队列).
运行队列的相关操作如下:
/*返回给定处理器可执行队列的指针*/
cpu_rq(processor)
/*返回当前处理器的可执行队列*/
this_rq();
/*返回给定的任务所在的队列指针*/
task_rq()
运行队列的竞争操作:
锁住运行队列最常见的情况发生在你想锁住的运行队列恰巧有一个特定的任务在运行.
Struck runqueue *rq;
Unsigned long flags;
Rq = task_rq_lock(task,&flags);
/*这里是你对任务队列rq的操作代码*/
Task_rq_unlock(rq,&flags);
或者如下:
Struck runqueue *rq;
Rq = this_rq_lock();
/*这里是你对任务队列rq的操作代码*/
Rq_unlock(rq);
当持有两个锁以上的运行队列操作,必须避免死锁的情况出现.这种策略往往只要遵照一定的原则就可以避免死锁的情况--以同样的顺序进行.
如下:
Double_rq_lock(rq1,rq2);
/*对两个运行队列的操作代码*/
Double_rq_unlock(rq1,rq2);
4.2.2 优先级数组
每个运行队列有两个优先组数组,一个活跃的和一个过期的.
活跃的优先级数组:运行队列上还有时间片剩余的进程组织者.
过期的优先级数组:运行队列上没有时间片剩余的进程组织者.
无论是活跃的还是过期的,都属于优先级数组,其定义如下:
Struck prio_array
{
Int nr_active; /*任务数目*/
Unsigned long bitmap[BITMAP_SIZE]; /*优先级位图*/
Struck list_head queue[MAX_PRIO]; /*优先级队列*/
}
MAX_PRIO:定义了系统拥有的优先级个数;
BITMAP_SIZE:优先级位图数组的大小;
List_head:每个元素对应一个给定的优先级.
当某个拥有一定优先级的进程开始准备执行时(也就是状态变为TASK_RUNNING),位图中相应的位就会被设置为1.比如,如果一个优先级为7的进程变为可执行状态,第7位就被置1.这样,查找系统中最高优先级就变成了查找位图中被设置的第一个位.提供该功能的函数是sched_find_first_bit().
4.2.3 时间片的重新计算
上述提及有活跃和过期的优先级数组,对时间片的重新计算进行了优化--只需要简单地在活跃优先级数组和过期优先级数组进行指针交换即可.
时间片的重新计算由schedule()函数实现.示意代码如下:
Struct prio_array *array = rq->active;
If(!array->nr_active) //如果活跃优先级数组没有活跃进程,则交换活跃优先级数
{ //组和过期优先级数组
Rq->active = rq->expired;
Rq->expired = array;
}
4.2.4 schedule()
此函数是选定下一个进程并运行的实现.下述几种情况会调用此函数:
情景一:内核代码需要休眠;
情景二:进程被抢占;
情景三:根据需要显式调用此函数.
由上述可知,等待进入运行的进程很有可能存在多个,选定哪一个进程切换到当前运行最主要的依据就是优先级,而优先级的选定被演化为了优先级位图的查找过程.因此,schedule()函数实现的示意代码如下:
Struck task_struck *prev,*next;
Struck list_head *queue;
Struck prio_array *array;
Int idx;
Prev = current;
Array = rq->active;
Idx = sched_find_first_bit(array->bitmap);
Queue = array->queue + idx;
Next = list_entry(queue->next,struct task_struct,run_list);
4.2.5 计算优先级和时间片
LINUX的最终优先级由两部分组成:静态优先级 + 动态优先级
静态优先级:即nice值,范围为-20到+19,值越小优先级越高,被分配的时间片也就越长.存放在task_struct的static_prio域.一开始由用户指定,不可改变;
动态优先级:由effective_prio()返回进程的动态优先级,以nice值为基准,加上-5到+5之间的进程交互性的奖励或惩罚.
LINUX采用这种静态优先级和动态优先级去仲裁一个进程的最终优先级直接导致了下面这样一个结果:
一个交互性比较强的进程,即使它的nice值为10,结合它的动态优先级最终的优先级有可能变为5;一个温和的处理器消耗型进程,虽然本来的nice值为10,结合它的动态优先级最终的优先级有可能变为12.
动态优先级的计算一个最重要的依据就是进程的休眠时间的长短.如果休眠时间比较长的,是I/O消耗型进程,它的优先级就应该会被提升.反之亦然.这种推断的机制,其推断信息被记录在task_struct的sleep_avg域.
4.2.6 睡眠和唤醒
休眠(被阻塞)的进程处于一种特殊的不可执行的状态.用来组织这种性质的进程的组织者叫做等待队列.
等待队列:
由等待某些事件发生而进入休眠的进程的组织者.
内核表征等待队列:wake_queue_head_t;
等待队列的创建:
静态创建:DECLARE_WAITQUEUE();
动态创建:init_waitqueue_head().
示意代码如下:
/*'q'是我们希望睡眠的等待队列*/
DECLARE_WAITQUEUE(wait,current);
Add_wait_queue(q,&wait);
While(!condition) //condition是我们等待的事件
{
Set_current_state(TASK_INTERUPTIBLE);
If(signal_pending(current))
/*处理相关信号*/
Schedule();
}
Set_current_state(TASK_RUNNING);
Remove_wait_queue(q,&wait);
唤醒:
Wake_up()函数负责唤醒指定的等待队列上的所有进程.
4.2.7 负载平衡程序
负载平衡程序主要是针对SMP的一种策略.比如有下面一种情景,处理器1的队列有五个,处理器2的队列只有一个.这时候,处理器1就相对处于一种负载的状态.理想的状态,应该是处理器1和处理器2上的队列相等.LINUX内核通过kernel/sched.c中的load_balance()函数来解决这种问题.
4.3 抢占和上下文切换
上下文切换,意指从一个可执行进程切换到另一个可执行进程.由函数context_switch()来完成.此函数主要完成下面两项工作:
一、调用定义在<asm/mmu_context.h>中的switch_mm(),完成新进程的虚拟内存的切换;
二、调用定义在<asm/system.h>中的switch_to(),完成旧进程的现场保护和新进程的现场恢复.
[注:]查看内核源码(linux2.6.30.4)可知,__schedule(void)里面调用了context_switch()函数.
抢占:就是意味着另外一个进程不管三七二十一,把当前进程的执行权变成自己的--代码的表现为schedule()函数的调用.此函数的调用,有两种情景会触发--一种是用户显式调用;另一种是内核自己根据自己的仲裁调用.那么,内核如何知道schedule()什么时候调用呢?如下:
need_resched标志:
被设置,会导致schedule()函数的调用;
被清零,不会导致schedule()函数的调用.
4.4 实时
LINUX实时调度策略:SCHED_FIFO、SCHED_RR;
LINUX非实时调度:SCHED_NORMAL.
SCHED_FIFO级进程:不使用时间片,因此可以一直执行下去;
SCHED_RR级进程:基于时间片,时间片消耗完不再执行.
内核中SCHED_FIFO、SCHED_RR级别的进程都是静态优先级,不作动态优先级计算.这能保证给定的优先级别的实时进程总能抢占优先级比它低进程.
4.5 与调度相关的系统调用
LINUX内核为用户空间开辟了一系列针对调度程序操作的API,这使用户方便根据自己的需要获取一些程序调度的信息及操作.如下:
Nice() 设置进程的nice值
Sched_setscheduler() 设置进程的调度策略
Sched_getscheduler() 获取进程的调度策略
Sched_setparam() 设置进程的实时优先级
Sched_getparam() 获取进程的实时优先级
Sched_get_priority_max() 获取实时优先级的最大值
Sched_get_priority_min() 获取实时优先级的最小值
Sched_rr_get_interval() 获取进程的时间片值
Sched_setaffinity() 设置处理器的亲和力
Sched_getaffinity() 获取处理器的亲和力
Sched_yield() 暂时让出处理器