进程调度中的所谓调度就是从就绪队列中选择一个进程投入CPU运行,则调度的主战场就是就绪队列,核心是调度算法,实质性的动作是进程的切换。
对于以时间片为主的调度,时钟中断就是驱动力,确保每个进程在CPU上运行一定的时间。在调度的过程中,用户还可以通过系统调用nice来调整优先级,比如降低自己的优先级等等。
当然也涉及进程状态的转换,新创建的进程就加入到了就绪队列中,推出的进程就从队列中删除。
从图中可以看出,所有CPU的所有进程都存放在了一个就绪队列中,那么我们从中选中一个进程进行调度的过程,实际上是从这个队列上的一种线性查找的过程,因此其算法复杂度为O(n)。
把就绪状态的进程组成一个双向循环链表,也叫就绪队列(runqueue)。
在task_struct结构里头定义的队列的结构就是一个list_head。
循环链表的队头是init_task结构,即0号进程的PCB。
在进程的调度算法中,进程优先级的重要性不言而喻,可从两个角度来看待优先级:
首先看用户态的空间,有两种优先级:
(1)普通优先级(nice):从-20~19,数据越小,优先级越高。可通过修改这个值,改变普通进程获取CPU资源的比例;
(2)调度优先级(scheduling priority):从1(最低)~99(最高)。
这是实时进程优先级,当然普通进程也有调度优先级,但是被设定为0。
每个普通进程都有它自己的静态优先级。内核用从100(最高优先级)到139(最低优先级)的数表示普通进程的静态优先级。值越大静态优先级越低。
新进程总是继承其父进程的静态优先级。不过,通过把某些“nice值”传递给系统调用nice()和setpriority(),用户可以改变自己拥有的进程的静态优先级。
静态优先级本质上决定了进程的基本时间片,即进程用完了以前的时间片时,系统分配给进程的时间片长度。
静态优先级和基本时间片的关系如下(基本时间片单位为ms):
若静态优先级<120 则基本时间片=(140-静态优先级)×20
若静态优先级 ≥120 则基本时间片=(140-静态优先级)×5
如上可知,静态优先级越高(其值越小),基本时间片就越长。
其结果是,与优先级低的进程相比,通常优先级较高的进程获得更长的CPU时间片。
说明 | 静态优先级 | nice值 | 基本时间片 | 交互式的δ值 | 睡眠时间的极限值 |
---|---|---|---|---|---|
最高静态优先级 | 100 | -20 | 800ms | -3 | 299ms |
高静态优先级 | 110 | -10 | 600ms | -1 | 499ms |
缺省静态优先级 | 120 | 0 | 100ms | +2 | 799ms |
低静态优先级 | 130 | +10 | 50ms | +4 | 999ms |
最低静态优先级 | 139 | +19 | 5ms | +6 | 1199ms |
以上为普通进程优先级的典型值。
普通进程动态优先级值的范围是100(最高优先级)~139(最低优先级)。
动态优先级是调度程序在选择新进程来运行的时候使用的数。它与静态优先级的关系用下面经验公式表示:
动态优先级=max(100,min(静态优先级-bonus+5,139))
bonus是范围从0~10的值,值小于5表示降低动态优先级以示惩罚,值大于5表示增加动态优先级以示奖赏。
bonus值依赖于进程过去的情况,与进程的平均睡眠时间相关。
平均睡眠时间: 进程在睡眠状态所消耗的平均纳秒数。进程在运行的过程中平均睡眠时间递减。最后,平均睡眠时间永远不会大于1s。
另外平均睡眠时间也被调度程序用来确定一个给定进程是交互式进程还是批处理进程。
若一个进程满足:动态优先级≤3×静态优先级/4+28
即 bonus-5≥静态优先级/4-28
表达式:静态优先级/4-28 被称为交互式的δ;一个具有缺省静态优先级(120)的进程,一旦其平均睡眠时间超过700ms,就成为交互式进程。
为避免进程饥饿,当一个进程用完它的时间片时,它应该被还没用完时间片的低优先级进程取代。为了实现这种机制,调度程序维持两个不相交的可运行进程的集合。
每个实时进程都与一个实时优先级有关,实时优先级是一个范围从1(最高优先级)~99(最低优先级)的值。实时进程总是被当做活动进程。
用户可通过系统调用改变进程的实时优先级。
实时进程被另一个进程取代的事件:
从内核空间来看有动态优先级(prio),静态优先级(static_prio),归一化优先级(normal_prio)和实时优先级(rt_priorit)。
归一化优先级是根据静态优先级、调度优先级和调度策略计算而得到的;
动态优先级是在运行的过程中可以动态地进行调整。
在task_struct结构中,这些优先级这样来表示:
struct task_ struct {
……
int prio, static prio, normal prio;
unsigned int rt_ priority;
……
unsigned int policy;
……
}
调度策略: 决定什么时候以怎样的方式选择一组新进程进行运行的的这组规则就是所谓的进程调度。
Linux调度基于分时(time sharing)技术,即多个进程以“时间多路复用”方式运行。因为CPU的时间被分为“片(slice)”,给每个可运行的进程分配一片。
若当前进程的时间片或时限(quantum)到期时,该进程还未运行完毕,进程切换就可以发生。
传统上进程可分为两类:
另一种进程区分为三类:
调度算法可明确确认所有实时程序的身份,无法区分交互式程序和批处理程序。
Linux的进程是抢占式的。这里给出一种情况以便来理解进程的抢占。
在此种情况中,只有两个程序:一个文本编辑程序和一个编译程序 —— 正在执行。
注:被抢占的进程并未被挂起,因为它还处于TASK_RUNNING状态,只不过不再使用CPU。
Linux对于时间片的选择单凭经验,选择尽可能长且同时能够保持良好响应时间的一个时间片。
每个Linux进程总是按照下面的调度类型被调度:
调度算法根据进程是普通进程还是实时进程而有很大不同。
前面介绍了O(n)调度器其中只有一个全局的就绪队列,严重的影响了其扩展性,因此引入了O(1)调度。
在O(1)调度器中引入了每个CPU一个就绪队列的概念,即系统中所有的就绪进程首先经过负载均衡模块挂入各个CPU的就绪队列上,然后由主调度器和周期性调度器驱动该CPU上的调度行为。
O(1)调度器的基本优化思路:把原来就绪队列上的单链表变成了多个链表,也就是说每一个优先级的进程被挂入到不同的链表里边。
优先级数组的结构:
struct prio_ array{
unsigned int nr_active; //nr_active表示这个队列中有多少个任务。
unsigned long bitmap[BITMAP SIZE]; //bitmap是表示各个优先级进程链表是空还是非空。
struct list_ head queuelMAX_PRIO];
};
O(1)由于支持140个优先级,因此队列成员中就有140个分别表示各个优先级的链表头,不同优先级的进程挂入不同的链表中。
在这些队列中,100~139是普通进程的优先级,其他的都是实时进程的优先级。因此在O(1)的调度器中,实时进程和普通进程被区分来对待,普通进程根本不会影响实时进程的调度。
就绪队列中有两个优先级队列,(struct prio array)分别用来管理活跃队列active(也就是时间片还有剩余)和expired(时间片耗尽)的进程。
随着系统的运行,活跃队列中的任务一个个耗尽其时间片,挂入到时间片耗尽的队列中,当活跃队列的任务为空时,两个队列互换开始新一轮的调度过程。
虽然在O(1)的调度器中任务组织的形式发生了变化,但是其核心思想仍和O(n)调度一致,都是把CPU的资源分成一个个时间片,分配给每一个就绪的队列,进程用完其额度后被抢占等待下一轮调度周期的到来。
主调度器(就是schedule函数)的主要功能是从该CPU的就绪队列中找到一个最合适的进程调度执行。
其基本的思路就是从活跃的优先级队列中查找,首先在当前活跃队列的位图中寻找第一个非空的进程链表,然后从在该链表中找到的第一个节点就是最适合下一个调度执行的进程。
由于没有遍历整个链表的操作,所以这个调度器的算法复杂度就是一个常量。从而解决了O(n)算法复杂度的问题。
但是O(1)调度器使用非常复杂的算法来判断进程是否是交互式进程以及进程的交互次数,即使如此,依然存在卡顿现象,那么如何解决此类问题,能否不被用户的具体需求所捆绑而又能够支持灵活多变的需求?
在此我们想到了机制与策略分离的机制,以下为调度器里的调度模型,机制与策略分离。
这种机制从功能层面上来看,仍然分为两部分:
有实时任务(RT task) ,普通任务( normal task ),最后期限任务(Dead line task),但是无论哪一种任务,它们都有共同的逻辑,这部分被抽象成核心调度器层(Core scheduler layer),类型的调度器定义自己的调度类(sched_ class) 。并以链表的形式加入系统中,这样的机制与策略分离的设计可以方便用户根据自己的场景定义特定的调度器,而无需改动核心调度层的逻辑。
下面简单介绍调度器类部分的成员作用:
其中第一个next指向下一个比自己低一级的优先级调度类,第二个字段指向入队的函数,第三个字段指向出队的函数,第四个字段表示当前CPU上正在运行的进程是否可被抢占,第五个就是核心的字段,也就是从就绪队列中选择一个最适合运行的进程,这是调度器较为核心的一个操作。
例如我们依据什么来挑选最合适运行的进程,这是每一个调度器需要关注的问题。
这是完全公平调度算法中所使用的红黑树,在具体实现的时候,CFS通过每个进程的虚拟运行时间(vruntime)来衡量哪个进程最值得被调度。
CFS中的就绪队列就是一颗以虚拟时间为键值的红黑树,虚拟时间越小的进程越靠近红黑树的最左边,因此调度器每次选择位于红黑树最左端的那个进程,该进程的虚拟时间是最小的。
虚拟运行时间是通过进程的实际运行时间和进程的权重(weight)计算出来的。
在CFS这个调度器中,将进程优先级这个概念就弱化了,而是强调了进程的权重。一个进程的权重越大说明这个进程越需要运行,因此它的虚拟运行时间就越小,这样被调度的机会就越大。
前面我们对linux调度器做了一个概要的介绍,下面给出一个总结:
进程调度是操作系统很重要的一个部件,它的主要功能是把系统中的任务调度到各个CPU上去执行以满足如下性能需求:
当然对于不同的任务有不同的需求,因此我们需要对任务进行分类。一般分为两大类:普通进程与实时进程。
为了达到这些目标,调度器在设计之时,必须综合考虑各种因素,更进一步详细探讨可结合源代码进行学习。
诚于中,形于外,故君子必慎其独也。