为什么Linux CFS调度器没有带来惊艳的碾压效果

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

然而这是错觉。

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


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

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

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

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

其次,我们应该看到,CFS调度器声称它会给交互式进程带来福音,在这方面CFS确实比 O ( 1 ) O(1) 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) O(1)做比较。

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

那么,CFS到底好在哪里?

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

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

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

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

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

C j = C j + 1 C_j=C_j+1 Cj=Cj+1

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

C j ( i ) = C j ( i − 1 ) 2 C_j(i)=\dfrac{C_j(i-1)}{2} Cj(i)=2Cj(i1)

P r i o j = B a s e _ N i c e j + C j ( i ) 2 Prio_j=Base\_Nice_j+\dfrac{C_j(i)}{2} Prioj=Base_Nicej+2Cj(i) 【此处为了简单,忽略 n i c e nice nice值的影响】

可以计算,在一个足够长的时间段内,两个进程运行的总时间比例,将和它们的 B a s e _ P r i o Base\_Prio Base_Prio优先级的比例相等。

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

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

V C k = V C k + B a s e _ N i c e k Σ i B a s e _ N i c e i VC_k=VC_k+\dfrac{Base\_Nice_k}{\Sigma_iBase\_Nice_i} VCk=VCk+ΣiBase_NiceiBase_Nicek

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

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

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

综上,我们总结了:

  • 现实世界很难碾压同级别的人或事。
  • 大量的Linux服务器不需要照顾交互进程,CFS优势无法凸显。
  • 大量的Android系统没有和 O ( 1 ) O(1) O(1)同台竞技的机会。
  • 大量的Android系统交互进程很难感知进程调度这件事。
  • CFS调度思想古已有之。

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

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

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


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

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

#define BASE_TIMESLICE(p) (MIN_TIMESLICE + /
((MAX_TIMESLICE - MIN_TIMESLICE) * /
(MAX_PRIO-1 - (p)->static_prio) / (MAX_USER_PRIO-1)))

static inline unsigned int task_timeslice(task_t *p)
{
	return BASE_TIMESLICE(p);
}

直观点显示:
在这里插入图片描述
针对上述问题,2.6内核的 O ( 1 ) O(1) O(1)引入了双斜率来解决:

static unsigned int task_timeslice(task_t *p)
{
	if (p->static_prio < NICE_TO_PRIO(0))
		return SCALE_PRIO(DEF_TIMESLICE*4, p->static_prio);
	else
		return SCALE_PRIO(DEF_TIMESLICE, p->static_prio);
}

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

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

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

  1. 固定的优先级映射到固定的时间片。
  2. 相对优先级和绝对优先级混杂。

那么这个问题如何解决?

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

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

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

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

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

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

  1. CFS的时间片是动态的,是系统负载均衡以及其优先级的函数,这便可以把进程调度动态适应到系统最佳,以节省切换开销。
  2. 即便是到了多核时代,对于实时进程依然像单核时代那般严格遵循最优先调度。

我还是想说,在调度器设计方面,大部分的人们关注点错了!

在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年前的大部头在啃。

当然了,Linux内核作为一个代码来讲,它是普适的,所以社区很难看到且关注单单是多核的问题,社区关注的最多的是可维护性,而不是性能。Linux新特性在128MB内存的i386机器上跑没有问题,那就是OK的。只要不是80%以上的人遭遇的新问题,社区是从不care的,同时,正因为如此,社区还会引入bug,这也是令人想叹息都不能叹息。

我的看法吧,社区只是一个一切以代码为准绳的程序员社区,社区不会过于关注体系结构的发展和新特性,这些都是厂商的事情。


回到进程调度的话题,正因为Linux一直在关注调度算法本身以及其实现的代码,才会出现 The Linux Scheduler: a Decade of Wasted Cores ,这篇十分中肯的paper:
http://www.ece.ubc.ca/~sasha/papers/eurosys16-final29.pdf


同样,我一向喷的TCP也是如此,人们关注TCP的实现代码本身,才会让它越来越复杂,然后越来越脆弱,也许你会说这就是进化,但是趁着万劫不复前,不是还有回炉的机会吗?还没有进化到必须继续进化的地步吧。如果站在外面看且具有强制措施,估计早就没有垃圾TCP了吧。


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

  • 14
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值