为啥Linux CFS调度器没有带来惊艳的碾压效果? CSDN博文精选

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了为啥Linux CFS调度器没有带来惊艳的碾压效果? CSDN博文精选相关的知识,希望对你有一定的参考价值。

参考技术A

任何领域,革命性的碾压式推陈出新并不是没有,但是概率极低,人们普遍的狂妄在于,总是认为自己所置身的环境正在发生着某种碾压式的变革,但其实,最终大概率不过是一场平庸。

作者 | dog250

责编 | 刘静

出品 | CSDN博客

但凡懂Linux内核的,都知道Linux内核的CFS进程调度算法,无论是从2.6.23将其初引入时的论文,还是各类源码分析,文章,以及Linux内核专门的图书,都给人这样一种感觉,即 CFS调度器是革命性的,它将彻底改变进程调度算法。 预期中,人们期待它会带来令人惊艳的效果。

然而这是错觉。

人们希望CFS速胜,但是分析来分析去, 却只是在某些方面比O(1)调度器稍微好一点点 。甚至在某些方面比不上古老的4.4BSD调度器。可是人们却依然对其趋之若鹜,特别是源码分析,汗牛塞屋!

为什么CFS对别的调度算法没有带来碾压的效果呢?

首先,在真实世界,碾压是不存在的,人与人,事与事既然被放在了同一个重量级梯队比较,其之间的差别没有想象的那么大,根本就不在谁碾压谁。不能被小说电视剧电影蒙蔽了,此外,徐晓冬大摆拳暴打雷雷也不算数,因为他们本就不是一个梯队。

任何领域,革命性的碾压式推陈出新并不是没有,但是概率极低,人们普遍的狂妄在于,总是认为自己所置身的环境正在发生着某种碾压式的变革,但其实,最终大概率不过是一场平庸。

最终就出现了角力,僵持。

其次,我们应该看到,CFS调度器声称它会给交互式进程带来福音,在这方面CFS确实比O(1)做得好,但是惊艳的效果来自于粉丝的认同。Linux系统交互进程本来就不多,Linux更多地被装在服务器,而在服务器看来,吞吐是要比交互响应更加重要的。

那么以交互为主的android系统呢?我们知道,Android也是采用了CFS调度器,也有一些事BFS,为什么同样没有带来惊艳的效果呢?

我承认,2008年前后出现CFS时还没有Android,等到Android出现时,其采用的Linux内核已经默认了CFS调度器,我们看下Android版本,Linux内核版本以及发行时间的关系:

Linux内核在2.6.23就采用了CFS调度器。所以一个原因就是没有比较。Android系统上,CFS没有机会和O(1)做比较。

另外,即便回移一个O(1)调度器到Android系统去和CFS做AB,在我看来,CFS同样不会惊艳,原因很简单,Android系统几乎都是交互进程,却前台进程永远只有一个,你几乎感受不到进程的切换卡顿,换句话说,即便CFS对待交互式进程比O(1)好太多,你也感受不到,因为对于手机,平板而言,你切换 APP 的时间远远大于进程切换的时间粒度。

那么,CFS到底好在哪里?

简单点说,CFS的意义在于, 在一个混杂着大量计算型进程和IO交互进程的系统中,CFS调度器对待IO交互进程要比O(1)调度器更加友善和公平 。理解这一点至关重要。

其实,CFS调度器的理念非常古老,就说在业界,CFS的思想早就被应用在了磁盘IO调度,数据包调度等领域,甚至最最古老的SRV3以及4.3BSD UNIX系统的进程调度中早就有了CFS的身影,可以说,Linux只是 使用CFS调度器 ,而不是 设计了CFS调度器

就以4.3BSD调度器为例,我们看一下其调度原理。

4.3BSD采用了1秒抢占制,每间隔1秒,会对整个系统进程进行优先级排序,然后找到优先级最高的投入运行,非常简单的一个思想,现在看看它是如何计算优先级的。

首先,每一个进程j均拥有一个CPU滴答的度量值Cj,每一个时钟滴答,当前在运行的进程的CPU度量值C会递增:

当一个1秒的时间区间ii过去之后,Cj被重置,该进程jj的优先级采用下面的公式计算:

可以计算,在一个足够长的时间段内,两个进程运行的总时间比例,将和它们的Base_PrioBase_Prio优先级的比例相等。

4.3BSD的优先级公平调度是CPU滴答驱动的。

现在看Linux的CFS,CFS采用随时抢占制。每一个进程j均携带一个 虚拟时钟VCj ,每一个时钟滴答,当前进程k的VCk会重新计算,同时调度器选择VC最小的进程运行,计算方法非常简单:

可见, Linux的CFS简直就是4.3BSD进程调度的自驱无级变速版本!

如果你想了解CFS的精髓,上面的就是了。换成语言描述,CFS的精髓就是 “ n个进程的系统,任意长的时间周期TT,每一个进程运行T/n的时间!

当然,在现实和实现中,会有80%的代码处理20%的剩余问题,比如如何奖励睡眠太久的进程等等,但是这些都不是精髓。

综上,我们总结了:

所以无论从概念还是从效果,Linux CFS调度器均没有带来令人眼前一亮的哇塞效果。但是还缺点什么。嗯,技术上的解释。

分析和解释任何一个机制之前,必然要先问,这个机制的目标是什么,它要解决什么问题,这样才有意义。而不能仅仅是明白了它是怎么工作的。

那么Linux CFS调度器被采用,它的目标是解决什么问题的呢?它肯定是针对O(1)算法的一个问题而被引入并取代O(1),该问题也许并非什么臭名昭著,但是确实是一枚钉子,必须拔除。

O(1)调度器的本质问题在于 进程的优先级和进程可运行的时间片进行了强映射!

也就是说,给定一个进程优先级,就会计算出一个时间片与之对应,我们忽略奖惩相关的动态优先级,看一下原始O(1)算法中一个进程时间片的计算:

直观点显示:

针对上述问题,2.6内核的O(1)O(1)引入了双斜率来解决:

直观图示如下:

貌似问题解决了,但是如果单单揪住上图的某一个优先级子区间来看,还是会有问题,这就是相对优先级的问题。我们看到,高优先级的时间片是缓慢增减的,而低优先级的时间片却是陡然增减,同样都是相差同样优先级的进程,其优先级分布影响了它们的时间片分配。

本来是治瘸子,结果腿好了,但是胳臂坏了。

本质上来讲,这都源自于下面两个原因:

固定的优先级映射到固定的时间片。

相对优先级和绝对优先级混杂。

那么这个问题如何解决?

优先级和时间片本来就是两个概念,二者中间还得有个变量沟通才可以。优先级高只是说明该进程能运行的久一些,但是到底久多少,并不是仅仅优先级就能决定的,还要综合考虑,换句话距离来说,如果只有一个进程,那么即便它优先级再低,它也可以永久运行,如果系统中有很多的进程,即便再高优先级的进程也要让出一些时间给其它进程。

所以,考虑到系统中总体的进程情况,将优先级转换为权重,将时间片转换为份额,CFS就是了。最终的坐标系应该是 权重占比/时间片 坐标系而不是 权重(或者优先级)/时间片 。应该是这个平滑的样子:

看来,Linux CFS只是为了解决O(1)O(1)中一个 “静态优先级/时间片映射” 问题的,那么可想而知,它又能带来什么惊艳效果呢?这里还有个“但是”,这个O(1)O(1)调度器的问题其实在计算密集型的守护进程看来,并不是问题,反而是好事,毕竟高优先级进程可以 无条件持续运行很久而不切换 。这对于吞吐率的提高,cache利用都是有好处的。无非也就侵扰了交互进程呗,又有何妨。

当然,使用调优CFS的时候,难免也要遇到IO睡眠奖惩等剩余的事情去设计一些trick算法,这破费精力。

对了,还要设置你的内核为HZ1000哦,这样更能体现CFS的平滑性,就像它宣称的那样。我难以想象,出了Ubuntu,Suse等花哨的桌面发行版之外,还有哪个Linux需要打开HZ1000,服务器用HZ250不挺好吗?

关于调度的话题基本就说完了,但是在进入下一步固有的喷子环节之前,还有两点要强调:

在CPU核数越来越多的时代,人们更应该关心 把进程调度到哪里CPU核上 而不是 某个CPU核要运行哪个进程

单核时代一路走过来的Linux,发展迅猛,这无可厚非,但是成就一个操作系统内核的并不单单是技术,还有别的。这些当然程序员们很不爱听,程序员最烦非技术方面的东西了,程序员跟谁都比写代码,程序员特别喜欢喷领导不会写代码云云。

Linux在纯技术方面并不优秀,Linux总体上优秀的原因是因为有一群非代码不明志的程序员在让它变得越来越优秀,另一方面还要归功于开源和社区。Linux的学习门槛极低,如果一个公司能不费吹灰之力招聘到一个Linux程序员的话,那它干嘛还要费劲九牛二虎之力去招聘什么高端的BSD程序员呢?最终的结果就是,Linux用的人极多,想换也换不掉了。

但无论如何也没法弥补Linux内核上的一些原则性错误。

Linux内核还是以原始的主线为base,以讲Linux内核的书为例,经典的Robert Love的《Linux内核设计与实现》,以及《深入理解Linux内核》,在讲进程调度的时候,关于多核负载均衡的笔墨都是少之又少甚至没有,如此经典的著作把很多同好引向了那万劫不复的代码深渊。于是乎,铺天盖地的CFS源码分析纷至沓来。

但其实,抛开这么一个再普通不过的Linux内核,现代操作系统进入了多核时代,其核心正是在cache利用上的革新,带来的转变就是进程调度和内存管理的革新。review一下Linux内核源码,这些改变早就已经表现了出来。

可悲的是,关于Linux内核的经典书籍却再也没有更新,所有的从传统学校出来的喜欢看书学习的,依然是抱着10年前的大部头在啃。

http :// www. ece.ubc.ca/~sasha/papers/eurosys16-final29.pdf

浙江温州皮鞋湿,下雨进水不会胖。

作者:CSDN博主「dog250」,本文首发于作者CSDN博客https://blog.csdn.net/dog250/article/details/957298 30 。

【END】

CFS Scheduler(CFS调度器)

前面我们分享了O(n)和O(1)调度器的实现原理,同时也了解了各个调度器的缺陷和面临的问题。总的来说O(1)调度器的出现是为了解决O(n)调度器不能解决的问题,而O(1)调度器在Linux2.4内核的在服务器的变形是可行的,但是Linux2.4以后随着移动设备的逐渐普遍,面临的卡顿问题逐渐明晰,这才导致后来的CFS调度器的出现。

 

本节我们重点来关注下CFS调度器实现,在学习CFS代码之前,我们先看CFS的实现原理,搞清楚它的来龙去脉,以及为啥CFS调度器需要这样设计,基本就可以掌握CFS调度器了。

 

CFS引入

完全公平调度器(CFS)最早是在2017年merged进Linux2.6.23版本中的,一直到现在都是系统中默认的调度器。内核文章中的sched-design-CFS.txt文档对CFS调度器有一个简单的介绍。

80% of CFS's design can be summed up in a single sentence: CFS basically models
an "ideal, precise multi-tasking CPU" on real hardware.

这句话的意思是CFS的80%的设计总结起来就一句话“在一个真实的硬件上,实现公平,精确的多任务CPU”

"理想的,精确的,多任务CPU"这句话是啥意思呢? 到底怎么理解呢?我们来通过例子做下解释

"Ideal multi-tasking CPU" is a (non-existent  :-)) CPU that has 100% physical power and which can run each task at precise equal speed, in parallel, each at 1/nr_running speed.  For example: if there are 2 tasks running, then it runs
each at 50% physical power --- i.e., actually in parallel.

内核文档是这样说的。"理想的,多任务CPU"是在同一时刻每个任务以1/nr_running_speed来运行,也就是同一时刻每个进程瓜分CPU的时间是相同的。例如如果有两个进程运行的话,每个进程占有50%的CPU时间。

举一个例子:

两个批处理进程,总共只能运行10ms。

实际情况:每个进程运行5ms,占有100%的CPU利用率

理想情况:每个进程运行10ms,占有50%的CPU利用率。

而所谓的理想情况就是CFS提到的"Ideal multi-tasking CPU"

上述的例子在一个单核CPU,只有一个处理核心的CPU上是完全不可能的,因为同一时刻只能运行一个进程,另外一个进程必须等待。而CFS就是为了的达到完全公平调度,它应该怎么做呢?

 

如何才能实现完全公平调度

在O(n)调度器和O(1)调度器中,我们知道都是通过优先级来分配对应的timeslice,也就是时间片。而这些时间片都是固定的。比如在O(n)调度器中nice0对应的时间片是60ms。而在CFS调度器中,不再有时间片的概念。而是根据当前系统中可运行进程的总数来计算出进程的可运行时间的。

在O(n)调度器和O(1)调度器中,普通进程都是通过nice值来获取对应时间片,nice值越大获取的时间片就越多,运行机会就越多。而在CFS调度器中引入了权重weight的概念,通过nice值转化为对应的权重,优先级越高的进程对应的权重就越大,意味着就可以获得更多的CPU时间。

                                   则进程占用CPU的时间 = 进程的weight / 总的可运行进程weight

CFS是让进程通过运行一段时间来达到公平,进程占用的时间就是进程的weight占总的可运行进程的总权重的比值。

举例:总共10ms的时间,单核cpu

  • 进程的优先级相同:

如果两个进程的优先级相同,则对应的权重相同,则每个进程占用5ms的CPU时间;如果有5个进程,每个进程占用2ms的CPU时间;如果共10个进程,每个进程占用1ms的CPU时间。

  • 进程的优先级不同:

如果两个进程的优先级不同,比如A进程nice是0,B的nice值1,则A进程的优先级就高,weight就越大,对应B的优先级小,weight也小于A。假设A的权重是6,B的权重是4。则A占2/3的CPU时间,B占1/3的CPU时间。

这样一来就达到了公平性,每个进程在各子拥有的权重比例下,占用不同份额的CPU时间。

再结合生活举一例:

公司发年终奖,一般来说一个部门的总包(CPU时间)是固定的。而为了公平老板不会给每个人发同样的奖金的,这样反而不公平了。而是通过平时在公司的表现,工作的认真态度之类(进程的weight)来衡量,比如张XX很辛苦,经常加班,进程出差,年终奖(进程占用CPU的时间)就多发。刘XX经常迟到,下班就没人了,年终奖(进程占用CPU的时间)少发。这样就显得公平。

 

CFS调度器是如何选择进程的

CFS的目标是让各个进程在一段时间内实现公平,也就是根据进程的权重来瓜分CPU的时间。权重越大则瓜分的CPU时间就越多,分配的CPU时间多就等同于有更大的机会得到CPU。

CFS调度是通过进程的虚拟时间vruntime来选择要运行的进程。vruntime的计算公式如下:

vruntime = (wall_time * NICE0_TO_weight) / weight

其中,wall_time代表的是进程实际运行的时间,NICE0_TO_Weight代表的是nice值等于0对应的权重,weight就是该进程对应的权重。可以看出vruntime的值其实是实际运行时间乘以nice0对应的weight除以进程的权重。

/*
 * Nice levels are multiplicative, with a gentle 10% change for every
 * nice level changed. I.e. when a CPU-bound task goes from nice 0 to
 * nice 1, it will get ~10% less CPU time than another CPU-bound task
 * that remained on nice 0.
 *
 * The "10% effect" is relative and cumulative: from _any_ nice level,
 * if you go up 1 level, it's -10% CPU usage, if you go down 1 level
 * it's +10% CPU usage. (to achieve that we use a multiplier of 1.25.
 * If a task goes up by ~10% and another task goes down by ~10% then
 * the relative distance between them is ~25%.)
 */
const int sched_prio_to_weight[40] = 
 /* -20 */     88761,     71755,     56483,     46273,     36291,
 /* -15 */     29154,     23254,     18705,     14949,     11916,
 /* -10 */      9548,      7620,      6100,      4904,      3906,
 /*  -5 */      3121,      2501,      1991,      1586,      1277,
 /*   0 */      1024,       820,       655,       526,       423,
 /*   5 */       335,       272,       215,       172,       137,
 /*  10 */       110,        87,        70,        56,        45,
 /*  15 */        36,        29,        23,        18,        15,
;

此表就是nice值和weight的转换变,此表已经计算好了,在代码中需要计算vruntime的时候,只需要根据nice值查表即可。

从注释上可以看出,当nice增大一个台阶,将会减少10%的CPU占用时间;当nice值减少一个台阶,将会获得10%的cpu时间。

从上面计算vruntime的公式可以得出,nice等于0的进程的虚拟时间等于物理时间。当一个进程的weight越大,则对应的进程的vruntime就越小;当一个进程的weight越小,则对应的vruntime就越大。

CFS每次调度原则是,总是选择vruntime最小的进程来调度,vruntime最小的进程weight越大,优先级越高,则就需要更高的获取CPU的时间。

 

举例说明:总共6ms的时间,有3个进程,一个进程A的权重是1024,另外一个进程B的权重是335,进程C的权重是3121

进程A vruntime = (2ms * 1024) / 1024 = 2ms, CPU占用率 = 1024/(1024+335+3121) = 22%

进程B vruntime = (2ms * 1024) / 335  = 6ms,CPU占用率 = 335/ (1024+335+3121) = 7%

进程C vruntime = (2ms * 1024) / 3121 = 0.65ms,CPU占用率 = 3121/ (1024+335+3121) = 70%

可以看出

  1. 各个CPU利用率都是相差50%,因为nice值每增加一个台阶,CPU占用率有10%的差别
  2. 进程的权重越大,分母也就越大,则vruntime则就越小,而在下一次选择进程时则高优先级选择它
  3. nice0=1024权重的进程的虚拟时间和物理时间是一样的
  4. 可以理解权重越大,虚拟时间越小,对应的虚拟时间轴跑的越快
  5. 权重越小,虚拟时间越大,对应的虚拟时间轴跑的越慢

 

调度周期(sched_period)

之前说过一个进程占用的CPU时间是根据进程的weight和系统中总的可运行进程的权重的比值。

                    进程占用CPU的时间 = 进程的weight / 总的可运行进程weight

比如两个优先级相同进程,总共10ms的时间,每个进程占用5ms。当系统中可运行的进程数目逐渐增多,则每个进程占用的cpu的时间就会越来越小,趋近于0。这就会导致进程之前频繁的发生上下文切换,CPU的大多数时间是用来处理进程的上下文切换了,导致系统效率下降。

所以对于此问题再CFS中则引入了调度周期,调度周期的计算通过如下代码

/*
 * The idea is to set a period in which each task runs once.
 *
 * When there are too many tasks (sched_nr_latency) we have to stretch
 * this period because otherwise the slices get too small.
 *
 * p = (nr <= nl) ? l : l*nr/nl
 */
static u64 __sched_period(unsigned long nr_running)

	if (unlikely(nr_running > sched_nr_latency))
		return nr_running * sysctl_sched_min_granularity;
	else
		return sysctl_sched_latency;


static unsigned int sched_nr_latency = 8;
unsigned int sysctl_sched_latency			= 6000000ULL;
unsigned int sysctl_sched_min_granularity			= 750000ULL;

从注释上看,这个函数的目的是为了让每个进程都可以运行一次。当系统中的进程数目逐渐增大时,则需要增大调度周期。

当进程的数目小于8时,则调度周期等于调度延迟等于6ms。当系统的进程数目大于8时,则调度器周期等于进程的数目乘以0.75ms。sysctl_sched_min_granularity可以理解为在一个调度周期中一个进程至少保证执行0.75ms。

 

CFS总结:

  • 在O(n)和O(1)调度器中都是通过nice值来分配固定的时间片,CFS中没有时间片的概念
  • CFS调度器中通过进程的静态优先级来计算进程的权重,进程的权重就代表了此进程需要获取的CPU的时间比例
  • 通过进程的weight和进程的实际运行时间来计算进程的vruntime虚拟时间。
  • 当进程加入到运行队列,调度器会时刻来更新进程的vruntime,来达到公平
  • 调度器每次调度的时候只选择运行队列中虚拟时间最小的进程,当此进程运行一段时间后,vruntime就会变大
  • 这时候就需要调度时候就需要重新选择新的最小vruntime的进程来执行,上次被调度出去的进程则就需要根据vrumtime的值来选择自己在运行队列的位置

以上是关于为啥Linux CFS调度器没有带来惊艳的碾压效果? CSDN博文精选的主要内容,如果未能解决你的问题,请参考以下文章

Linux 内核CFS 调度器 ② ( CFS 调度器 “ 权重 “ 概念 | CFS 调度器调度实例 | 计算进程 “ 实际运行时间 “ )

Linux 内核CFS 调度器 ① ( CFS 完全公平调度器概念 | CFS 调度器虚拟时钟 Virtual Runtime 概念 | 四种进程优先级 | 五种调度类 )

Linux 内核CFS 调度器 ⑥ ( CFS 调度器就绪队列 cfs_rq | Linux 内核调度实体 sched_entity | “ 红黑树 “ 数据结构 rb_root_cached )

Linux 内核CFS 调度器 ③ ( 计算进程 “ 虚拟运行时间 “ )

CFS Scheduler(CFS调度器)

Linux CFS调度器之唤醒抢占--Linux进程的管理与调度(三十)