进程的优先级与调度策略—Linux

寵の児 提交于 2020-02-18 03:37:26

1.概述

进程调度中的所谓调度就是从就绪队列中选择一个进程投入CPU运行,则调度的主战场就是就绪队列,核心是调度算法,实质性的动作是进程的切换。

对于以时间片为主的调度,时钟中断就是驱动力,确保每个进程在CPU上运行一定的时间。在调度的过程中,用户还可以通过系统调用nice来调整优先级,比如降低自己的优先级等等。
当然也涉及进程状态的转换,新创建的进程就加入到了就绪队列中,推出的进程就从队列中删除。
在这里插入图片描述
从图中可以看出,所有CPU的所有进程都存放在了一个就绪队列中,那么我们从中选中一个进程进行调度的过程,实际上是从这个队列上的一种线性查找的过程,因此其算法复杂度为O(n)。

把就绪状态的进程组成一个双向循环链表,也叫就绪队列(runqueue)。
在task_struct结构里头定义的队列的结构就是一个list_head。
循环链表的队头是init_task结构,即0号进程的PCB。

1.1 进程优先级

在进程的调度算法中,进程优先级的重要性不言而喻,可从两个角度来看待优先级:
首先看用户态的空间,有两种优先级:
(1)普通优先级(nice):从-20~19,数据越小,优先级越高。可通过修改这个值,改变普通进程获取CPU资源的比例;
(2)调度优先级(scheduling priority):从1(最低)~99(最高)。

这是实时进程优先级,当然普通进程也有调度优先级,但是被设定为0。

1.2 普通进程的调度

每个普通进程都有它自己的静态优先级。内核用从100(最高优先级)到139(最低优先级)的数表示普通进程的静态优先级。值越大静态优先级越低。

新进程总是继承其父进程的静态优先级。不过,通过把某些“nice值”传递给系统调用nice()和setpriority(),用户可以改变自己拥有的进程的静态优先级。

1.2.1 静态优先级和基本时间片

静态优先级本质上决定了进程的基本时间片,即进程用完了以前的时间片时,系统分配给进程的时间片长度。

静态优先级和基本时间片的关系如下(基本时间片单位为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

以上为普通进程优先级的典型值。

1.2.2 动态优先级和平均睡眠

普通进程动态优先级值的范围是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.3 实时进程的调度

每个实时进程都与一个实时优先级有关,实时优先级是一个范围从1(最高优先级)~99(最低优先级)的值。实时进程总是被当做活动进程。
用户可通过系统调用改变进程的实时优先级。

实时进程被另一个进程取代的事件:

  • 进程被另一个具有更高实时优先级的实时进程抢占;
  • 进程执行了阻塞操作并进入了睡眠;
  • 进程停止或被杀死;
  • 进程通过调用系统调用sched_yield()自愿放弃CPU;
  • 进程是基于时间片轮转的实时进程,而且用完了它的时间片。

1.4 内核空间优先级

从内核空间来看有动态优先级(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;
……
}

2.调度策略

调度策略: 决定什么时候以怎样的方式选择一组新进程进行运行的的这组规则就是所谓的进程调度。

Linux调度基于分时(time sharing)技术,即多个进程以“时间多路复用”方式运行。因为CPU的时间被分为“片(slice)”,给每个可运行的进程分配一片。
若当前进程的时间片或时限(quantum)到期时,该进程还未运行完毕,进程切换就可以发生。

传统上进程可分为两类:

  • I/O受限(I/O-bound):频繁地使用I/O设备,并花费很多的时间等待I/O操作的完成;
  • CPU受限(CPU-bound):需要大量CPU时间的数值计算应用程序。

另一种进程区分为三类:

  • 交互式进程(interactive process):这些进程经常与用户进行交互,花很多时间用来等待键盘和鼠标操作。典型的有命令shell、文本编辑程序及图形应用程序;
  • 批处理进程(batch process):这些进程不必与用户进行交互,因此经常在后台运行。典型的有程序设计语言的编译程序、数据库搜索引擎及科学计算;
  • 实时进程(real-time process):这些进程有很强的调度需要。典型的有视频和音频应用程序、机器人控制程序及从物理传感器上收集数据的程序。

调度算法可明确确认所有实时程序的身份,无法区分交互式程序和批处理程序。

2.1 进程的抢占

Linux的进程是抢占式的。这里给出一种情况以便来理解进程的抢占。

在此种情况中,只有两个程序:一个文本编辑程序和一个编译程序 —— 正在执行。

  1. 文本编辑程序是一个交互式程序,因此它的动态优先级高于编译程序。不过因为编辑程序交替于用户暂停思考与数据输入之间,因此它经常被挂起;此外,两次击键之间的平均延迟相对较长。
  2. 然而,只要用户一按键,中断就发生,内核唤醒文本编辑进程。
  3. 内核也确定文本编辑进程的动态优先级确实高于current的优先级(当前正运行的进程,即编译进程),因此,编译进程的TIF_NEED_RESCHED标志被设置,如此来强迫内核处理完中断时激活调度程序。
  4. 调度程序选择编辑进程并执行进程切换;结果,编辑进程很快恢复执行,并把用户键入的字符回显在屏幕上。
  5. 当处理完字符时,文本编辑进程自己挂起等待下一次击键,编译进程恢复执行。

注:被抢占的进程并未被挂起,因为它还处于TASK_RUNNING状态,只不过不再使用CPU。
Linux对于时间片的选择单凭经验,选择尽可能长且同时能够保持良好响应时间的一个时间片。

2.2 调度算法

  • 该调度算法在固定的时间内(与可运行的进程数量无关)选中要进行的进程;
  • 每个CPU都拥有自己的可运行进程队列,很好的处理了与处理器数量的比例关系;
  • 较好地解决了区分交互式进程和批处理进程的问题。

每个Linux进程总是按照下面的调度类型被调度:

  • SCHED_FIFO:先进先出的实时进程。当调度程序把CPU分配给进城的时候,它把该进程描述符保留在运行队列链表的当前位置。如无其他可运行的更高优先级实时进程,进程就继续使用CPU,想用多久就用多久,即使还有其他具有相同优先级的实时进程处于可运行状态;
  • SCHED_RR:时间片轮转的实时进程。当调度程序把CPU分配给进程的时候,它把该进程的描述符放在运行队列链表的末尾。这种策略保证对所有具有相同优先级的SCHED_RR实时进程公平地分配CPU时间;
  • SCHED_NORMAL:普通的分时进程。

调度算法根据进程是普通进程还是实时进程而有很大不同。

2.3 O(1)调度

前面介绍了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)调度器使用非常复杂的算法来判断进程是否是交互式进程以及进程的交互次数,即使如此,依然存在卡顿现象,那么如何解决此类问题,能否不被用户的具体需求所捆绑而又能够支持灵活多变的需求?

2.4 调度模型——机制与策略分离

在此我们想到了机制与策略分离的机制,以下为调度器里的调度模型,机制与策略分离。
在这里插入图片描述
这种机制从功能层面上来看,仍然分为两部分:

  1. 第一个部分是通过负载均衡模块将各个就着状态的任务根据负载情况平均分配到各个CPU就绪队列上去;
  2. 第二部分的功能是在各个CPU的主调度器(Mainscheduler)和周期性调度器(Tick
    scheduler)的驱动下进行单个CPU上的调度。

有实时任务(RT task) ,普通任务( normal task ),最后期限任务(Dead line task),但是无论哪一种任务,它们都有共同的逻辑,这部分被抽象成核心调度器层(Core scheduler layer),类型的调度器定义自己的调度类(sched_ class) 。并以链表的形式加入系统中,这样的机制与策略分离的设计可以方便用户根据自己的场景定义特定的调度器,而无需改动核心调度层的逻辑。

下面简单介绍调度器类部分的成员作用:
其中第一个next指向下一个比自己低一级的优先级调度类,第二个字段指向入队的函数,第三个字段指向出队的函数,第四个字段表示当前CPU上正在运行的进程是否可被抢占,第五个就是核心的字段,也就是从就绪队列中选择一个最适合运行的进程,这是调度器较为核心的一个操作。
例如我们依据什么来挑选最合适运行的进程,这是每一个调度器需要关注的问题。

2.5 完全公平调度——CFS

  • CFS调度器的目标是保证每一个进程的完全公平调度。
  • CFS调度器与以往调度器的不同之处在于没有时间片的概念,而是分配CPU使用时间的比例。
  • 理想状态下每个进程都能获得相同的时间片而且能同时运行在CPU上,但实际上一个CPU同一时刻运行的进程只能有一个。也就是说当一个进程占用CPU的时候其他进程必须等待。
  • CFS为了实现公平,必须惩罚当前正在运行的进程以使那些正在等待的进程下次再被调用。

在这里插入图片描述
这是完全公平调度算法中所使用的红黑树,在具体实现的时候,CFS通过每个进程的虚拟运行时间(vruntime)来衡量哪个进程最值得被调度。

CFS中的就绪队列就是一颗以虚拟时间为键值的红黑树,虚拟时间越小的进程越靠近红黑树的最左边,因此调度器每次选择位于红黑树最左端的那个进程,该进程的虚拟时间是最小的。
虚拟运行时间是通过进程的实际运行时间和进程的权重(weight)计算出来的。

在CFS这个调度器中,将进程优先级这个概念就弱化了,而是强调了进程的权重。一个进程的权重越大说明这个进程越需要运行,因此它的虚拟运行时间就越小,这样被调度的机会就越大。
前面我们对linux调度器做了一个概要的介绍,下面给出一个总结:

2.6 调度器总结

进程调度是操作系统很重要的一个部件,它的主要功能是把系统中的任务调度到各个CPU上去执行以满足如下性能需求:

  1. 对于分时的进程,调度器必须是公平的;
  2. 快速的进程响应时间;
  3. 系统有高的吞吐量;
  4. 功耗要小。

当然对于不同的任务有不同的需求,因此我们需要对任务进行分类。一般分为两大类:普通进程与实时进程。

  • 对于实时进程来说,毫无疑问快速响应的需求是最重要的;
  • 对于普通进程而言,我们一般要兼顾以上三点的需求。但很明显会发现,这些需求互相是冲突的。

为了达到这些目标,调度器在设计之时,必须综合考虑各种因素,更进一步详细探讨可结合源代码进行学习。

诚于中,形于外,故君子必慎其独也。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!