Linux进程调度策略的发展和演变(转)

 

 转发:http://blog.csdn.net/gatieme/article/details/51701149html

 1 前言linux

1.1 进程调度

内存中保存了对每一个进程的惟一描述, 并经过若干结构与其余进程链接起来.程序员

调度器面对的情形就是这样, 其任务是在程序之间共享CPU时间, 创造并行执行的错觉, 该任务分为两个不一样的部分, 其中一个涉及调度策略, 另一个涉及上下文切换.算法

1.2 进程的分类

linux把进程区分为实时进程和非实时进程, 其中非实时进程进一步划分为交互式进程和批处理进程shell

 

类型数据库

描述数组

 

交互式进程(interactive process)缓存

此类进程常常与用户进行交互, 所以须要花费不少时间等待键盘和鼠标操做. 当接受了用户的输入后,   进程必须很快被唤醒, 不然用户会感受系统反应迟钝服务器

shell, 文本编辑程序和图形应用程序网络

批处理进程(batch process)

此类进程没必要与用户交互, 所以常常在后台运行. 由于这样的进程没必要很快相应,   所以常受到调度程序的怠慢

程序语言的编译程序, 数据库搜索引擎以及科学计算

实时进程(real-time process)

这些进程由很强的调度须要, 这样的进程毫不会被低优先级的进程阻塞. 而且他们的响应时间要尽量的短

视频音频应用程序,   机器人控制程序以及从物理传感器上收集数据的程序

在linux中, 调度算法能够明确的确认全部实时进程的身份, 可是没办法区分交互式程序和批处理程序, linux2.6的调度程序实现了基于进程过去行为的启发式算法, 以肯定进程应该被当作交互式进程仍是批处理进程. 固然与批处理进程相比, 调度程序有偏心交互式进程的倾向

 

1.3 不一样进程采用不一样的调度策略

根据进程的不一样分类Linux采用不一样的调度策略.

对于实时进程,采用FIFO或者Round Robin的调度策略.

对于普通进程,则须要区分交互式和批处理式的不一样。传统Linux调度器提升交互式应用的优先级,使得它们能更快地被调度。而CFS和RSDL等新的调度器的核心思想是”彻底公平”。这个设计理念不只大大简化了调度器的代码复杂度,还对各类调度需求的提供了更完美的支持.

注意Linux经过将进程和线程调度视为一个,同时包含两者。进程能够看作是单个线程,可是进程能够包含共享必定资源(代码和/或数据)的多个线程。所以进程调度也包含了线程调度的功能.

目前非实时进程的调度策略比较简单, 由于实时进程值只要求尽量快的被响应, 基于优先级, 每一个进程根据它重要程度的不一样被赋予不一样的优先级,调度器在每次调度时, 总选择优先级最高的进程开始执行. 低优先级不可能抢占高优先级, 所以FIFO或者Round Robin的调度策略便可知足实时进程调度的需求.

可是普通进程的调度策略就比较麻烦了, 由于普通进程不能简单的只看优先级, 必须公平的占有CPU, 不然很容易出现进程饥饿, 这种状况下用户会感受操做系统很卡, 响应老是很慢,所以在linux调度器的发展历程中通过了屡次重大变更, linux老是但愿寻找一个最接近于完美的调度策略来公平快速的调度进程。

 

1.4 linux调度器的演变

一开始的调度器是复杂度为O(n)的始调度算法(实际上每次会遍历全部任务,因此复杂度为O(n)), 这个算法的缺点是当内核中有不少任务时,调度器自己就会耗费很多时间,因此,从linux2.5开始引入赫赫有名的O(1)调度器

然而,linux是集全球不少程序员的聪明才智而发展起来的超级内核,没有最好,只有更好,在O(1)调度器风光了没几天就又被另外一个更优秀的调度器取代了,它就是CFS调度器Completely Fair Scheduler. 这个也是在2.6内核中引入的,具体为2.6.23,即今后版本开始,内核使用CFS做为它的默认调度器,O(1)调度器被抛弃了。

因此彻底有理由相信,后续若是再会出现一个更优秀的调度器,CFS也不会幸免。由于linux只要最好的那个。

 

2 O(n)的始调度算法

2.1 Linux2.4以前的内核调度器

早期的Linux进程调度器使用了最低的设计,它显然不关注具备不少处理器的大型架构,更不用说是超线程了。

Linux调度器使用了环形队列用于可运行的任务管理, 使用循环调度策略.

此调度器添加和删除进程效率很高(具备保护结构的锁)。简而言之,该调度器并不复杂可是简单快捷.

Linux版本2.2引入了调度类的概念,容许针对实时任务、非抢占式任务、非实时任务的调度策略。调度器还包括对称多处理 (SMP) 支持。

 

2.2 Linux2.4的调度器

2.2.1 概述

在Linux2.4.18中(linux-2.5)以前的内核, 当不少任务都处于活动状态时, 调度器有很明显的限制. 这是因为调度器是使用一个复杂度为O(n)的算法实现的.

调度器采用基于优先级的设计,这个调度器和Linus在1992年发布的调度器没有大的区别。该调度器的pick next算法很是简单:对runqueue中全部进程的优先级进行依次进行比较,选择最高优先级的进程做为下一个被调度的进程。(Runqueue是Linux 内核中保存全部就绪进程的队列). pick next用来指从全部候选进程中挑选下一个要被调度的进程的过程。

这种调度算法很是简单易懂: 在每次进程切换时, 内核扫描可运行进程的链表, 计算优先级,然胡选择”最佳”进程来运行.

在这种调度器中, 调度任务所花费的时间是一个系统中任务个数的函数. 换而言之, 活动的任务越多, 调度任务所花费的时间越长. 在任务负载很是重时, 处理器会因调度消耗掉大量的时间, 用于任务自己的时间就很是少了。所以,这个算法缺少可伸缩性

2.2.2 详情

每一个进程被建立时都被赋予一个时间片。时钟中断递减当前运行进程的时间片,当进程的时间片被用完时,它必须等待从新赋予时间片才能有机会运行。Linux2.4调度器保证只有当全部RUNNING进程的时间片都被用完以后,才对全部进程从新分配时间片。这段时间被称为一个epoch。这种设计保证了每一个进程都有机会获得执行。每一个epoch中,每一个进程容许执行到其时间切片用完。若是某个进程没有使用其全部的时间切片,那么剩余时间切片的一半将被添加到新时间切片使其在下个epoch中能够执行更长时间。调度器只是迭代进程,应用goodness函数(指标)决定下面执行哪一个进程。固然,各类进程对调度的需求并不相同,Linux 2.4调度器主要依靠改变进程的优先级,来知足不一样进程的调度需求。事实上,全部后来的调度器都主要依赖修改进程优先级来知足不一样的调度需求。

实时进程:实时进程的优先级是静态设定的,并且始终大于普通进程的优先级。所以只有当runqueue中没有实时进程的状况下,普通进程才可以得到调度。

实时进程采用两种调度策略,SCHED_FIFO 和 SCHED_RR

FIFO 采用先进先出的策略,对于全部相同优先级的进程,最早进入 runqueue 的进程总能优先得到调度;Round Robin采用更加公平的轮转策略,使得相同优先级的实时进程可以轮流得到调度。

普通进程:对于普通进程,调度器倾向于提升交互式进程的优先级,由于它们须要快速的用户响应。普通进程的优先级主要由进程描述符中的Counter字段决定 (还要加上 nice 设定的静态优先级) 。进程被建立时子进程的 counter值为父进程counter值的一半,这样保证了任何进程不能依靠不断地 fork() 子进程从而得到更多的执行机会。

Linux2.4调度器是如何提升交互式进程的优先级的呢?如前所述,当全部 RUNNING 进程的时间片被用完以后,调度器将从新计算全部进程的 counter 值,全部进程不只包括 RUNNING 进程,也包括处于睡眠状态的进程。处于睡眠状态的进程的 counter 原本就没有用完,在从新计算时,他们的 counter 值会加上这些原来未用完的部分,从而提升了它们的优先级。交互式进程常常因等待用户输入而处于睡眠状态,当它们从新被唤醒并进入 runqueue 时,就会优先于其它进程而得到 CPU。从用户角度来看,交互式进程的响应速度就提升了。

该调度器的主要缺点:

•       可扩展性很差

调度器选择进程时须要遍历整个 runqueue 从中选出最佳人选,所以该算法的执行时间与进程数成正比。另外每次从新计算 counter 所花费的时间也会随着系统中进程数的增长而线性增加,当进程数很大时,更新 counter 操做的代价会很是高,致使系统总体的性能降低。

•       高负载系统上的调度性能比较低

2.4的调度器预分配给每一个进程的时间片比较大,所以在高负载的服务器上,该调度器的效率比较低,由于平均每一个进程的等待时间于该时间片的大小成正比。

•       交互式进程的优化并不完善

Linux2.4识别交互式进程的原理基于如下假设,即交互式进程比批处理进程更频繁地处于SUSPENDED状态。然而现实状况每每并不是如此,有些批处理进程虽然没有用户交互,可是也会频繁地进行IO操做,好比一个数据库引擎在处理查询时会常常地进行磁盘IO,虽然它们并不须要快速地用户响应,仍是被提升了优先级。当系统中这类进程的负载较重时,会影响真正的交互式进程的响应时间。

•       对实时进程的支持不够

Linux2.4内核是非抢占的,当进程处于内核态时不会发生抢占,这对于真正的实时应用是不能接受的。

为了解决这些问题,Ingo Molnar开发了新的$O(1)调度器,在CFS和RSDL以前,这个调度器不只被Linux2.6采用,还被backport到Linux2.4中,不少商业的发行版本都采用了这个调度器.

 

3 O(1)的调度算法

3.1 概述

因为进程优先级的最大值为139,所以MAX_PRIO的最大值取140(具体的是,普通进程使用100到139的优先级,实时进程使用0到99的优先级).

所以,该调度算法为每一个优先级都设置一个可运行队列, 即包含140个可运行状态的进程链表,每一条优先级链表上的进程都具备相同的优先级,而不一样进程链表上的进程都拥有不一样的优先级。

除此以外, 还包括一个优先级位图bitmap。该位图使用一个位(bit)来表明一个优先级,而140个优先级最少须要5个32位来表示, 所以只须要一个int[5]就能够表示位图,该位图中的全部位都被置0,当某个优先级的进程处于可运行状态时,该优先级所对应的位就被置1。

若是肯定了优先级,那么选取下一个进程就简单了,只需在queue数组中对应的链表上选取一个进程便可。

最后,在早期的内核中,抢占是不可能的;这意味着若是有一个低优先级的任务在执行,高优先级的任务只能等待它完成。

3.2 详情

从名字就能够看出O(1)调度器主要解决了之前版本中的扩展性问题。

O(1)调度算法所花费的时间为常数,与当前系统中的进程个数无关。

此外Linux 2.6内核支持内核态抢占,所以更好地支持了实时进程。

相对于前任,O(1)调度器还更好地区分了交互式进程和批处理式进程。

Linux 2.6内核也支持三种调度策略。其中SCHED_FIFO和SCHED_RR用于实时进程,而SCHED_NORMAL用于普通进程。

O(1)调度器在两个方面修改了Linux 2.4调度器,一是进程优先级的计算方法;二是pick next算法。

O(1)调度器跟踪运行队列中可运行的任务(实际上,每一个优先级水平有两个运行队列,一个用于活动任务,一个用于过时任务), 这意味着要肯定接下来执行的任务,调度器只需按优先级将下一个任务从特定活动的运行队列中取出便可。

3.2.1 普通进程的优先级计算

不一样类型的进程应该有不一样的优先级。每一个进程与生俱来(即从父进程那里继承而来)都有一个优先级,咱们将其称为静态优先级。普通进程的静态优先级范围从100到139,100为最高优先级,139 为最低优先级,0-99保留给实时进程。当进程用完了时间片后,系统就会为该进程分配新的时间片(即基本时间片),静态优先级本质上决定了时间片分配的大小。

静态优先级和基本时间片的关系以下:

静态优先级<120,基本时间片=max((140-静态优先级)*20, MIN_TIMESLICE)

静态优先级>=120,基本时间片=max((140-静态优先级)*5, MIN_TIMESLICE)

其中MIN_TIMESLICE为系统规定的最小时间片。从该计算公式能够看出,静态优先级越高(值越低),进程获得的时间片越长。其结果是,优先级高的进程会得到更长的时间片,而优先级低的进程获得的时间片则较短。进程除了拥有静态优先级外,还有动态优先级,其取值范围是100到139。当调度程序选择新进程运行时就会使用进程的动态优先级,动态优先级和静态优先级的关系可参考下面的公式:

动态优先级=max(100 , min(静态优先级 – bonus + 5) , 139)

从上面看出,动态优先级的生成是以静态优先级为基础,再加上相应的惩罚或奖励(bonus)。这个bonus并非随机的产生,而是根据进程过去的平均睡眠时间作相应的惩罚或奖励。

所谓平均睡眠时间(sleep_avg,位于task_struct结构中)就是进程在睡眠状态所消耗的总时间数,这里的平均并非直接对时间求平均数。平均睡眠时间随着进程的睡眠而增加,随着进程的运行而减小。所以,平均睡眠时间记录了进程睡眠和执行的时间,它是用来判断进程交互性强弱的关键数据。若是一个进程的平均睡眠时间很大,那么它极可能是一个交互性很强的进程。反之,若是一个进程的平均睡眠时间很小,那么它极可能一直在执行。另外,平均睡眠时间也记录着进程当前的交互状态,有很快的反应速度。好比一个进程在某一小段时间交互性很强,那么sleep_avg就有可能暴涨(固然它不能超过 MAX_SLEEP_AVG),但若是以后都一直处于执行状态,那么sleep_avg就又可能一直递减。理解了平均睡眠时间,那么bonus的含义也就显而易见了。交互性强的进程会获得调度程序的奖励(bonus为正),而那些一直霸占CPU的进程会获得相应的惩罚(bonus为负)。其实bonus至关于平均睡眠时间的缩影,此时只是将sleep_avg调整成bonus数值范围内的大小。可见平均睡眠时间能够用来衡量进程是不是一个交互式进程。若是知足下面的公式,进程就被认为是一个交互式进程:

动态优先级≤3*静态优先级/4 + 28

平均睡眠时间是进程处于等待睡眠状态下的时间,该值在进程进入睡眠状态时增长,而进入RUNNING状态后则减小。该值的更新时机分布在不少内核函数内:时钟中断scheduler_tick();进程建立;进程从TASK_INTERRUPTIBLE状态唤醒;负载平衡等。

3.2.2 实时进程的优先级计算

实时进程的优先级由sys_sched_setschedule()设置。该值不会动态修改,并且老是比普通进程的优先级高。在进程描述符中用rt_priority域表示。

3.2.3 pick next算法

普通进程的调度选择算法基于进程的优先级,拥有最高优先级的进程被调度器选中。

2.4中,时间片counter同时也表示了一个进程的优先级。2.6中时间片用任务描述符中的time_slice域表示,而优先级用prio(普通进程)或者rt_priority(实时进程)表示。调度器为每个CPU维护了两个进程队列数组:指向活动运行队列的active数组和指向过时运行队列的expire数组。数组中的元素着保存某一优先级的进程队列指针。系统一共有140个不一样的优先级,所以这两个数组大小都是140。它们是按照先进先出的顺序进行服务的。被调度执行的任务都会被添加到各自运行队列优先级列表的末尾。每一个任务都有一个时间片,这取决于系统容许执行这个任务多长时间。运行队列的前100个优先级列表保留给实时任务使用,后40个用于用户任务,参见下图:

                         

当须要选择当前最高优先级的进程时,2.6调度器不用遍历整个runqueue,而是直接从active数组中选择当前最高优先级队列中的第一个进程。假设当前全部进程中最高优先级为50(换句话说,系统中没有任何进程的优先级小于50)。则调度器直接读取 active[49],获得优先级为50的进程队列指针。该队列头上的第一个进程就是被选中的进程。这种算法的复杂度为O(1),从而解决了2.4调度器的扩展性问题。为了实现O(1)算法active数组维护了一个由5个32位的字(140个优先级)组成的bitmap,当某个优先级别上有进程被插入列表时,相应的比特位就被置位。 sched_find_first_bit()函数查询该bitmap,返回当前被置位的最高优先级的数组下标。在上例中sched_find_first_bit函数将返回49。在IA处理器上能够经过bsfl等指令实现。可见查找一个任务来执行所须要的时间并不依赖于活动任务的个数,而是依赖于优先级的数量。这使得 2.6 版本的调度器成为一个复杂度为 O(1) 的过程,由于调度时间既是固定的,并且也不会受到活动任务个数的影响。

为了提升交互式进程的响应时间,O(1)调度器不只动态地提升该类进程的优先级,还采用如下方法:每次时钟tick中断时,进程的时间片(time_slice)被减一。当time_slice为0时,表示当前进程的时间片用完,调度器判断当前进程的类型,若是是交互式进程或者实时进程,则重置其时间片并从新插入active数组。若是不是交互式进程则从active数组中移到expired数组,并根据上述公式从新计算时间片。这样实时进程和交互式进程就总能优先得到CPU。然而这些进程不能始终留在active数组中,不然进入expire数组的进程就会产生饥饿现象。当进程已经占用CPU时间超过一个固定值后,即便它是实时进程或者交互式进程也会被移到expire数组中。当active数组中的全部进程都被移到expire数组中后,调度器交换active数组和expire数组。所以新的active数组又恢复了初始状况,而expire数组为空,从而开始新的一轮调度。

Linux 2.6调度器改进了前任调度器的可扩展性问题,schedule()函数的时间复杂度为O(1)。这取决于两个改进:

•       pick next算法借助于active数组,无需遍历runqueue;

•       消了按期更新全部进程counter的操做,动态优先级的修改分布在进程切换,时钟tick中断以及其它一些内核函数中进行。

O(1)调度器区分交互式进程和批处理进程的算法与之前虽大有改进,但仍然在不少状况下会失效。有一些著名的程序总能让该调度器性能降低,致使交互式进程反应缓慢。例如fiftyp.c, thud.c, chew.c, ring-test.c, massive_intr.c等。并且O(1)调度器对NUMA支持也不完善。为了解决这些问题,大量难以维护和阅读的复杂代码被加入Linux2.6.0的调度器模块,虽然不少性能问题所以获得了解决,但是另一个严重问题始终困扰着许多内核开发者,那就是代码的复杂度问题。不少复杂的代码难以管理而且对于纯粹主义者而言未能体现算法的本质。

为了解决O(1)调度器面临的问题以及应对其余外部压力, 须要改变某些东西。这种改变来自Con Kolivas的内核补丁staircase scheduler(楼梯调度算法),以及改进的RSDL(Rotating Staircase Deadline Scheduler)。它为调度器设计提供了一个新的思路。Ingo Molnar在RSDL以后开发了CFS,并最终被2.6.23内核采用。接下来咱们开始介绍这些新一代调度器。

4 Linux 2.6的新一代调度器CFS

4.1 楼梯调度算法staircase scheduler

楼梯算法(SD)在思路上和O(1)算法有很大不一样,它抛弃了动态优先级的概念。而采用了一种彻底公平的思路。前任算法的主要复杂性来自动态优先级的计算,调度器根据平均睡眠时间和一些很难理解的经验公式来修正进程的优先级以及区分交互式进程。这样的代码很难阅读和维护。楼梯算法思路简单,可是实验证实它对应交互式进程的响应比其前任更好,并且极大地简化了代码。

和O(1)算法同样,楼梯算法也一样为每个优先级维护一个进程列表,并将这些列表组织在active数组中。当选取下一个被调度进程时,SD算法也一样从active数组中直接读取。与O(1)算法不一样在于,当进程用完了本身的时间片后,并非被移到expire数组中。而是被加入active数组的低一优先级列表中,即将其下降一个级别。不过请注意这里只是将该任务插入低一级优先级任务列表中,任务自己的优先级并无改变。当时间片再次用完,任务被再次放入更低一级优先级任务队列中。就象一部楼梯,任务每次用完了本身的时间片以后就下一级楼梯。任务下到最低一级楼梯时,若是时间片再次用完,它会回到初始优先级的下一级任务队列中。好比某进程的优先级为1,当它到达最后一级台阶140后,再次用完时间片时将回到优先级为2的任务队列中,即第二级台阶。不过此时分配给该任务的time_slice将变成原来的2倍。好比原来该任务的时间片time_slice为10ms,则如今变成了20ms。基本的原则是,当任务下到楼梯底部时,再次用完时间片就回到上次下楼梯的起点的下一级台阶。并给予该任务相同于其最初分配的时间片。总结以下:设任务自己优先级为P,当它从第N级台阶开始下楼梯并到达底部后,将回到第N+1级台阶。而且赋予该任务N+1倍的时间片。

以上描述的是普通进程的调度算法,实时进程仍是采用原来的调度策略,即FIFO或者Round Robin。

楼梯算法能避免进程饥饿现象,高优先级的进程会最终和低优先级的进程竞争,使得低优先级进程最终得到执行机会。对于交互式应用,当进入睡眠状态时,与它同等优先级的其余进程将一步一步地走下楼梯,进入低优先级进程队列。当该交互式进程再次唤醒后,它还留在高处的楼梯台阶上,从而能更快地被调度器选中,加速了响应时间。

楼梯算法的优势:从实现角度看,SD基本上仍是沿用了O(1)的总体框架,只是删除了O(1)调度器中动态修改优先级的复杂代码;还淘汰了expire数组,从而简化了代码。它最重要的意义在于证实了彻底公平这个思想的可行性。

4.2 RSDL(Rotating Staircase Deadline Scheduler)

RSDL也是由Con Kolivas开发的,它是对SD算法的改进。核心的思想仍是”彻底公平”。没有复杂的动态优先级调整策略。RSDL从新引入了expire数组。它为每个优先级都分配了一个 “组时间配额”,记为Tg;同一优先级的每一个进程都拥有一样的”优先级时间配额”,用Tp表示。当进程用完了自身的Tp时,就降低到下一优先级进程组中。这个过程和SD相同,在RSDL中这个过程叫作minor rotation(次轮询)。请注意Tp不等于进程的时间片,而是小于进程的时间片。下图表示了minor rotation。进程从priority1的队列中一步一步下到priority140以后回到priority2的队列中,这个过程以下图左边所示,而后从priority 2开始再次一步一步下楼,到底后再次反弹到priority3队列中,以下图所示。

                                                   

在SD算法中,处于楼梯底部的低优先级进程必须等待全部的高优先级进程执行完才能得到CPU。所以低优先级进程的等待时间没法肯定。RSDL中,当高优先级进程组用完了它们的Tg(即组时间配额)时,不管该组中是否还有进程Tp还没有用完,全部属于该组的进程都被强制下降到下一优先级进程组中。这样低优先级任务就能够在一个能够预计的将来获得调度。从而改善了调度的公平性。这就是RSDL中Deadline表明的含义。

进程用完了本身的时间片time_slice时(下图中T2),将放入expire数组指向的对应初始优先级队列中(priority 1)。

                                                      

 

当active数组为空,或者全部的进程都下降到最低优先级时就会触发主轮询major rotation。Major rotation交换active数组和expire数组,全部进程都恢复到初始状态,再一次重新开始minor rotation的过程。

RSDL对交互式进程的支持:和SD一样的道理,交互式进程在睡眠时间时,它全部的竞争者都由于minor rotation而降到了低优先级进程队列中。当它从新进入RUNNING状态时,就得到了相对较高的优先级,从而能被迅速响应。

4.3 彻底公平的调度器CFS

CFS是最终被内核采纳的调度器。它从RSDL/SD中吸收了彻底公平的思想,再也不跟踪进程的睡眠时间,也再也不企图区分交互式进程。它将全部的进程都统一对待,这就是公平的含义。CFS的算法和实现都至关简单,众多的测试代表其性能也很是优越。

按照做者Ingo Molnar的说法(参考Documentation/scheduler/sched-design-CFS.txt),

CFS百分之八十的工做能够用一句话归纳:CFS在真实的硬件上模拟了彻底理想的多任务处理器。在真空的硬件上,同一时刻咱们只能运行单个进程,所以当一个进程占用CPU时,其它进程就必须等待,这就产生了不公平。可是在“彻底理想的多任务处理器 “下,每一个进程都能同时得到CPU的执行时间,即并行地每一个进程占1/nr_running的时间。例如当系统中有两个进程时,CPU的计算时间被分红两份,每一个进程得到50%。假设runqueue中有n个进程,当前进程运行了10ms。在“彻底理想的多任务处理器”中,10ms应该平分给n个进程(不考虑各个进程的nice值),所以当前进程应得的时间是(10/n)ms,可是它却运行了10ms。因此CFS将惩罚当前进程,使其它进程可以在下次调度时尽量取代当前进程。最终实现全部进程的公平调度。

与以前的Linux调度器不一样,CFS没有将任务维护在链表式的运行队列中,它抛弃了active/expire数组,而是对每一个CPU维护一个以时间为顺序的红黑树。

该树方法可以良好运行的缘由在于:

•       红黑树能够始终保持平衡,这意味着树上没有路径比任何其余路径长两倍以上。

•       因为红黑树是二叉树,查找操做的时间复杂度为O(log n)。可是除了最左侧查找之外,很难执行其余查找,而且最左侧的节点指针始终被缓存。

•       对于大多数操做(插入、删除、查找等),红黑树的执行时间为O(log n),而之前的调度程序经过具备固定优先级的优先级数组使用 O(1)。O(log n) 行为具备可测量的延迟,可是对于较大的任务数可有可无。Molnar在尝试这种树方法时,首先对这一点进行了测试。

•       红黑树可经过内部存储实现,即不须要使用外部分配便可对数据结构进行维护。

要实现平衡,CFS使用”虚拟运行时”表示某个任务的时间量。任务的虚拟运行时越小,意味着任务被容许访问服务器的时间越短,其对处理器的需求越高。CFS还包含睡眠公平概念以便确保那些目前没有运行的任务(例如,等待 I/O)在其最终须要时得到至关份额的处理器。

4.3.1 CFS如何实现pick next

下图是一个红黑树的例子。

                            

全部可运行的任务经过不断地插入操做最终都存储在以时间为顺序的红黑树中(由 sched_entity 对象表示),对处理器需求最多的任务(最低虚拟运行时)存储在树的左侧,处理器需求最少的任务(最高虚拟运行时)存储在树的右侧。 为了公平,CFS调度器会选择红黑树最左边的叶子节点做为下一个将得到cpu的任务。这样,树左侧的进程就被给予时间运行了。

4.3.2 tick中断

在CFS中,tick中断首先更新调度信息。而后调整当前进程在红黑树中的位置。调整完成后若是发现当前进程再也不是最左边的叶子,就标记need_resched标志,中断返回时就会调用scheduler()完成进程切换。不然当前进程继续占用CPU。从这里能够看到 CFS抛弃了传统的时间片概念。Tick中断只需更新红黑树,之前的全部调度器都在tick中断中递减时间片,当时间片或者配额被用完时才触发优先级调整并从新调度。

4.3.3 红黑树键值计算

理解CFS的关键就是了解红黑树键值的计算方法。该键值由三个因子计算而得:一是进程已经占用的CPU时间;二是当前进程的nice值;三是当前的cpu负载。进程已经占用的CPU时间对键值的影响最大,其实很大程度上咱们在理解CFS时能够简单地认为键值就等于进程已占用的 CPU时间。所以该值越大,键值越大,从而使得当前进程向红黑树的右侧移动。另外CFS规定,nice值为1的进程比nice值为0的进程多得到10%的 CPU时间。在计算键值时也考虑到这个因素,所以nice值越大,键值也越大。

CFS为每一个进程都维护两个重要变量:fair_clock和wait_runtime。这里咱们将为每一个进程维护的变量称为进程级变量,为每一个CPU维护的称做CPU级变量,为每一个runqueue维护的称为runqueue级变量。进程插入红黑树的键值即为fair_clock – wait_runtime。其中fair_clock从其字面含义上讲就是一个进程应得到的CPU时间,即等于进程已占用的CPU时间除以当前 runqueue中的进程总数;wait_runtime是进程的等待时间。它们的差值表明了一个进程的公平程度。该值越大,表明当前进程相对于其它进程越不公平。对于交互式任务,wait_runtime长时间得不到更新,所以它能拥有更高的红黑树键值,更靠近红黑树的左边。从而获得快速响应。

红黑树是平衡树,调度器每次总最左边读出一个叶子节点,该读取操做的时间复杂度是O(LogN)

4.3.4 调度器管理器

为了支持实时进程,CFS提供了调度器模块管理器。各类不一样的调度器算法均可以做为一个模块注册到该管理器中。不一样的进程能够选择使用不一样的调度器模块。2.6.23中,CFS实现了两个调度算法,CFS算法模块和实时调度模块。对应实时进程,将使用实时调度模块。对应普通进程则使用CFS算法。CFS 调度模块(在 kernel/sched_fair.c 中实现)用于如下调度策略:SCHED_NORMAL、SCHED_BATCH 和 SCHED_IDLE。对于 SCHED_RR 和 SCHED_FIFO 策略,将使用实时调度模块(该模块在 kernel/sched_rt.c 中实现)。

4.3.5 CFS组调度

CFS组调度(在 2.6.24 内核中引入)是另外一种为调度带来公平性的方式,尤为是在处理产生不少其余任务的任务时。 假设一个产生了不少任务的服务器要并行化进入的链接(HTTP 服务器的典型架构)。不是全部任务都会被统一公平对待, CFS 引入了组来处理这种行为。产生任务的服务器进程在整个组中(在一个层次结构中)共享它们的虚拟运行时,而单个任务维持其本身独立的虚拟运行时。这样单个任务会收到与组大体相同的调度时间。您会发现 /proc 接口用于管理进程层次结构,让您对组的造成方式有彻底的控制。使用此配置,您能够跨用户、跨进程或其变体分配公平性。

考虑一个两用户示例,用户 A 和用户 B 在一台机器上运行做业。用户 A 只有两个做业正在运行,而用户 B 正在运行 48 个做业。组调度使 CFS 可以对用户 A 和用户 B 进行公平调度,而不是对系统中运行的 50 个做业进行公平调度。每一个用户各拥有 50% 的 CPU 使用。用户 B 使用本身 50% 的 CPU 分配运行他的 48 个做业,而不会占用属于用户 A 的另外 50% 的 CPU 分配。

更多CFS的信息, 请参照

http://www.ibm.com/developerworks/cn/linux/l-completely-fair-scheduler/index.html?ca=drs-cn-0125

另外内核文档sched-design-CFS.txt中也有介绍。

 

5.返璞归真的Linux BFS调度器

BFS 是一个进程调度器,能够解释为“脑残调度器”。这古怪的名字有多重含义,比较容易被接受的一个说法为:它如此简单,却如此出色,这会让人对本身的思惟能力产生怀疑。

BFS 不会被合并进入 Linus 维护的 Linux mainline,BFS 自己也不打算这么作。但 BFS 拥有众多的拥趸,这只有一个缘由:BFS 很是出色,它让用户的桌面环境达到了史无前例的流畅。在硬件愈来愈先进,系统却依然常显得迟钝的时代,这实在让人兴奋。

进入 2010 年,Android 开发一个分支使用 BFS 做为其操做系统的标准调度器,这也证实了 BFS 的价值。后来放弃。

5.1 BFS的引入

前些天忽然在网上看到了下面的图片

                                             

后来发现该图片是BFS调度器的引子, 太具备讽刺意义了。

5.2 可配置型调度器的需求

为了不小手段,那就要完全抛弃“鱼与熊掌可兼得”的思想,采用“一种调度器只适用于一种场景”的新思路. 如此咱们能够设计多种调度器, 在安装操做系统的时候能够由管理员进行配置, 好比咱们将其用于桌面,那么就使用”交互调度器”, 若是用于路由器, 那就使用”大吞吐调度器”, …消除了兼顾的要求,调度器设计起来就更佳简单和纯粹了.

面对须要大吞吐量的网络操做系统, 咱们有传统的UNIX调度器, 然而面对日益桌面化的操做系统好比Android手机, 咱们是否能摒弃那种大而全的调度策略呢?

Con Kolivas老大设计出的BFS调度器就是为桌面交互式应用量身打造的.

5.3 问题在哪?

Linux 2.6内核实现了那么多的调度器,然而其效果老是有美中不足的地方,到底问题出在哪里?事实上,Linux 2.6的各类调度器的实现都不是彻底按照理论完成的,其中都添加了一些小手段. 好比虽然CFS号称支持大于2048的CPU个数,然而实际应用中,效果未必好,由于CFS调度器继承了O(1)调度器的load_balance特性,所以在那么多处理器之间进行基于调度域的load_balance,锁定以及独占的代价将会十分大,从而抵消了每CPU队列带来的消除锁定的优点.

总之,这些调度器太复杂了,并且愈来愈复杂,将80%的精力消耗在了20%的场景中. 实际上,作设计不要联想,彻底依照咱们目前所知道的和所遇到的来,在可用性和效率上被证实是明智的,固然不考虑太多的可扩展性。

5.4 回到O(n)调度器

BFS调度器用一句话来总结就是”回到了O(n)调度器”,它在O(n)调度器的基础上进行了优化,而没有引入看起来很好的O(1)调度器, 这就是其实质.

O(n)调度器有什么很差么?有的, 大不了就是遍历的时间太长,BFS根据实际的测试数据忽略之;每一个处理器都要锁定整个队列,BFS改之,作到这些既可,这才叫基于O(n)调度器的优化而不是完全颠覆O(n)调度器而引入O(1)调度器-固然前提是桌面环境。若是说能回到原始的O(n)调度器进行修改使之从新发挥其做用而不是完全抛弃它,这才是最佳的作法,反之,若是咱们把问题的解决方案搞的愈来愈复杂,最终就是陷入一个泥潭而不可自拔。要知道方案复杂性的积累是一个笛卡儿积式的积累,你必须考虑到每一种排列组合才能,当你作不到这一点的时候,你就须要返璞归真。

5.5 BFS调度器的原理_

BFS的原理十分简单,其实质正是使用了O(1)调度器中的位图的概念,全部进程被安排到103个queue中,各个进程不是按照优先级而是按照优先级区间被排列到各自所在的区间,每个区间拥有一个queue,以下图所示:

                                                     

内核在pick-next的时候,按照O(1)调度器的方式首先查找位图中不为0的那个queue,而后在该queue中执行O(n)查找,查找到virtual deadline(以下所述)最小的那个进程投入执行。过程很简单,就像流水同样。之因此规划103个队列而不是一个彻底是为了进程按照其性质而分类,这个和每CPU没有任何关系,将进程按照其性质(RT?优先级?)分类而不是按照CPU分类是明智之举。内核中只有一个“103队列”,m个CPU和“103队列”彻底是一个“消费者-生产者”的关系。O(1)调度器,内核中拥有m(CPU个数)个“消费者-生产者”的关系,每个CPU附带一个“生产者(140队列组)”。

只有统一的,单一的“消费者-生产者”的关系才能作到调度的公平,避免了多个关系之间踢皮球现象,这是事实。在结构单一,功能肯定且硬件简单的系统中,正确的调度器架构以下图所示:

                                       

在结构单一,功能肯定且硬件简单的系统中,不正确的调度器架构以下图所示:

         

虚拟 Deadline ( Virtual Deadline )

当一个进程被建立时,它被赋予一个固定的时间片,和一个虚拟 Deadline。该虚拟 deadline 的计算公式很是简单:

Virtual Deadline = jiffies + (user_priority * rr_interval)

其中 jiffies 是当前时间 , user_priority 是进程的优先级,rr_interval 表明 round-robin interval,近似于一个进程必须被调度的最后期限,所谓 Deadline 么。不过在这个 Deadline 以前还有一个形容词为 Virtual,所以这个 Deadline 只是表达一种愿望而已,并不是不少领导们常说的那种 deadline。

虚拟 Deadline 将用于调度器的 picknext 决策

进程队列的表示方法和调度策略

在操做系统内部,全部的 Ready 进程都被存放在进程队列中,调度器从进程队列中选取下一个被调度的进程。所以如何设计进程队列是咱们研究调度器的一个重要话题。BFS 采用了很是传统的进程队列表示方法,即 bitmap 加 queue。

BFS 将全部进程分红 4 类,分别表示不一样的调度策略 :

Realtime,实时进程 SCHED_ISO,isochronous 进程,用于交互式任务 SCHED_NORMAL,普通进程 SCHED_IDELPRO,低优先级任务 实时进程总能得到 CPU,采用 Round Robin 或者 FIFO 的方法来选择一样优先级的实时进程。他们须要 superuser 的权限,一般限于那些占用 CPU 时间很少却很是在意 Latency 的进程。

SCHED_ISO 在主流内核中至今仍未实现,Con 早在 2003 年就提出了这个 patch,但一直没法进入主流内核,这种调度策略是为了那些 near-realtime 的进程设计的。如前所述,实时进程须要用户有 superuser 的权限,这类进程可以独占 CPU,所以只有不多的进程能够被配置为实时进程。对于那些对交互性要求比较高的,又没法成为实时进程的进程,BFS 将采用 SCHED_ISO,这些进程可以抢占 SCHED_NORMAL 进程。他们的优先级比 SCHED_NORMAL 高,但又低于实时进程。此外当 SCHED_ISO 进程占用 CPU 时间达到必定限度后,会被降级为 SCHED_NORMAL,防止其独占整个系统资源。

SCHED_NORMAL 相似于主流调度器 CFS 中的 SCHED_OTHER,是基本的分时调度策略。

SCHED_IDELPRO 相似于 CFS 中的 SCHED_IDLE,即只有当 CPU 即将处于 IDLE 状态时才被调度的进程。

在这些不一样的调度策略中,实时进程分红 100 个不一样的优先级,加上其余三个调度策略,一共有 103 个不

同的进程类型。对于每一个进程类型,系统中都有可能有多个进程同时 Ready,好比极可能有两个优先级为 10 的 RT 进程同时 Ready,因此对于每一个类型,还须要一个队列来存储属于该类型的 ready 进程。

BFS 用 103 个 bitmap 来表示是否有相应类型的进程准备进行调度。如图所示:

当任何一种类型的进程队列非空时,即存在 Ready 进程时,相应的 bitmap 位被设置为 1。

调度器如何在这样一个 bitmap 加 queue 的复杂结构中选择下一个被调度的进程的问题被称为 Task Selection 或者 pick next。

Task Selection i.e. Pick Next

当调度器决定进行进程调度的时候,BFS 将按照下面的原则来进行任务的选择:

首先查看 bitmap 是否有置位的比特。好比上图,对应于 SCHED_NORMAL 的 bit 被置位,代表有类型为 SCHED_NORMAL 的进程 ready。若是有 SCHED_ISO 或者 RT task 的比特被置位,则优先处理他们。

选定了相应的 bit 位以后,便须要遍历其相应的子队列。假如是一个 RT 进程的子队列,则选取其中的第一个进程。若是是其余的队列,那么就采用 EEVDF 算法来选取合适的进程。

EEVDF,即 earliest eligible virtual deadline first。BFS 将遍历该子队列,一个双向列表,比较队列中的每个进程的 Virtual Deadline 值,找到最小的那个。最坏状况下,这是一个 O(n) 的算法,即须要遍历整个双向列表,假如其中有 n 个进程,就须要进行 n 此读取和比较。

但实际上,每每不须要遍历整个 n 个进程,这是由于 BFS 还有这样一个搜索条件:

当某个进程的 Virtual Deadline 小于当前的 jiffies 值时,直接返回该进程。并将其从就绪队列中删除,下次再 insert 时会放到队列的尾部,从而保证每一个进程都有可能被选中,而不会出现饥饿现象。

这条规则对应于这样一种状况,即进程已经睡眠了比较长的时间,以致于已经睡过了它的 Virtual Deadline,

5.6 BFS调度器初始版本的链表的非O(n)遍历

BFS调度器的发展历程中也经历了一个为了优化性能而引入“小手段”的时期,该“小手段”是如此合理,以致于每个细节都值得品味,现表述以下:

你们都知道,遍历一个链表的时间复杂度是O(n),然而这只是遍历的开销,在BFS调度器中,遍历的目的其实就是pick-next,若是该链表某种意义上是预排序的,那么pick-next的开销能够减小到接近O(1)。BFS如何作到的呢?

咱们首先看一下virtual deadline的概念

virtual deadline(VD)

VD=jiffies + (prio_ratio * rr_interval)

其中prio_ratio为进程优先级,rr_interval为一个Deadline,表示该进程在最多多久内被调度,链表中的每个entry表明一个进程,都有一个VD与之相关。VD的存在使得entry在链表的位置得以预排序,这里的预排序指的是vitrual deadline expire的影响下的预排序,BFS和O(n)的差异就在于这个expire,因为这个expire在,通常都会在遍历的途中遇到VD expire,进而不须要O(n)。基于VD的O(n)和基于优先级的O(n)是不一样的,其区别在于根据上述的计算公式,VD是单调向前的,而优先级几乎是不怎么变化的,所以基于VD的O(n)调度器某种程度上和基于红黑树的CFS是同样的,VD也正相似于CFS中的虚拟时钟,只是数据结构不一样而已,BFS用链表实现,CFS用红黑树实现。

其实,O(n)并无那么可怕,特别是在桌面环境中,你却是有多少进程须要调度呢?理论上O(n)会随着进程数量的增长而效率下降,然而桌面环境下实际上没有太多的进程须要被调度,因此采用了BFS而抛弃了诸多小手段的调度器效果会更好些。理论上,CFS或者O(1)能够支持SMP下的诸多进程调度的高效性,然而,桌面环境下,第一,SMP也只是2到4个处理器,进程数也大多不超过1000个,进程在CPU之间蹦来蹦去,很累,何须杀鸡用牛刀呢?瓶颈不是鸡,而是杀鸡的刀,是吧!

5.7 pick-next算法

BFS的pick-next算法对于SCHED_ISO进程依照如下的原则进行:

•       依照FIFO原则进行,再也不遍历链表

BFS的pick-next算法对于SCHED_NORMAL或者SCHED_IDLEPRIO进程依照如下的原则进行:

•       遍历运行链表,比较每个entry的VD,找出最小的entry,从链表中删除,投入运行

•       若是发现有entry的VD小于当前的jiffers,则中止遍历,取出该entry,投入运行–小手段

以上的原则能够总结为“最小最负最优先”原则。做者一席话以下:

BFS has 103 priority queues. 100 of these are dedicated to the static priority of realtime tasks, and the remaining 3 are, in order of best to worst priority, SCHED_ISO (isochronous), SCHED_NORMAL, and SCHED_IDLEPRIO (idle priority scheduling). When a task of these priorities is queued, a bitmap of running priorities is set showing which of these priorities has tasks waiting for CPU time. When a CPU is made to reschedule, the lookup for the next task to get CPU time is performed in the following way:

First the bitmap is checked to see what static priority tasks are queued. If any realtime priorities are found, the corresponding queue is checked and the first task listed there is taken (provided CPU affinity is suitable) and lookup is complete. If the priority corresponds to a SCHED_ISO task, they are also taken in FIFO order (as they behave like SCHED_RR). If the priority corresponds to either SCHED_NORMAL or SCHED_IDLEPRIO, then the lookup becomes O(n). At this stage, every task in the runlist that corresponds to that priority is checked

to see which has the earliest set deadline, and (provided it has suitable CPU affinity) it is taken off the runqueue and given the CPU. If a task has an expired deadline, it is taken and the rest of the lookup aborted (as they are

chosen in FIFO order).

Thus, the lookup is O(n) in the worst case only, where n is as described earlier, as tasks may be chosen before the whole task list is looked over.

使用virtual deadline,相似于CFS的virtual runtime的概念,然而不要红黑树,而采用了双向链表来实现,由于红黑树的插入效率不如链表插入效率,在pick-next算法上虽然红黑树占优点,然而因为VD expire的存在也使得pick-next再也不是O(n)了

BFS初始版本的小手段的意义在于减小O(n)遍历比较时间复杂度带来的恐惧。

5.8 去除了小手段的BFS调度器

最终将小手段去除是重要的,不然BFS最终仍是会陷入相似O(1),CFS等复杂化的泥潭里面不可自拔,所以在后续的patch中,BFS去除了上述的小手段,用统一的O(n)复杂度来pick-next,毕竟前面已经说了O(n)在特定环境下并非问题的关键,该patch在2.6.31.14-bfs318-330test.patch中体现。

5.9 队列外部执行

BFS调度器和CFS是同样的,都是队列外执行进程的,这样能够减小锁争用带来的性能问题。再列出做者的一席话:

BFS has one single lock protecting the process local data of every task in the global queue. Thus every insertion, removal and modification of task data in the global runqueue needs to grab the global lock. However, once a task is taken by a CPU, the CPU has its own local data copy of the running process’ accounting information which only that CPU accesses and modifies (such as during a timer tick) thus allowing the accounting data to be updated lockless. Once a CPU has taken a task to run, it removes it from the global queue. Thus the

global queue only ever has, at most,

(number of tasks requesting cpu time) - (number of logical CPUs) + 1

tasks in the global queue. This value is relevant for the time taken to look up tasks during scheduling. This will increase if many tasks with CPU affinity set in their policy to limit which CPUs they’re allowed to run on if they outnumber the number of CPUs. The +1 is because when rescheduling a task, the CPU’s currently running task is put back on the queue. Lookup will be described after the virtual deadline mechanism is explained.

在schedule核心函数中,使用return_task来把prev进程从新入队,在earliest_deadline_task这个pick-next中,使用take_task将选中的next从队列取出,从而实现队列外执行。

5.10 结论

从上面的论述,咱们丝毫没有看到有任何的诸如“SMP负载均衡”,“CPU亲和力”,“补偿”,“惩罚”之类的字眼,是的,这些字眼在BFS中彻底不须要,BFS也正是摒弃了这些字眼才得到成功的,毕竟在一个通常人使用的桌面操做系统中,没有这么多的套套,大多数人使用的就是一个只有一个到两个处理器核心的系统,难道有必要搞什么调度域么?难道有必要搞什么NUMA么?需求决定一切,面对大型服务器,有UNIX的机制站在那里,而若是咱们想把Linux推广到每个掌上设备,那就不必复制UNIX的那套了,BFS彻底能够完美的搞定一切。小手段的去除,说明BFS调度器的发展方向起码是正确的。

BFS对SMP的支持如何呢?答案是它仅仅支持少许CPU的SMP体系,别忘了BFS的应用场合。由于在调度过程当中须要一个遍历全部CPU的O(m)复杂度的计算,这就明确告诉人们,别期望BFS使用在拥有4096个CPU的系统上,正如没人用这种系统看视频同样,那样的话,仍是乖乖使用CFS吧。

BFS调度器思想很简单:集中精力作好一件事,适应一种场景,代码一样十分简单,所以即便贴上代码整个文章也不会显得过于冗长,你再也看不到诸如load_balance或者for_each_domain之类的东西了,至于CPU cache的亲和力智能判断,若是你非要作,那么就本身调用sched_setaffinity系统调用设置吧,把一个线程或者一组相关的进程设置到一个或者一组共享Cache的CPU上,让内核这些,在进程不那么多,CPU个数不那么多,没有NUMA的系统上,真的太累了。

相关文章
相关标签/搜索