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

为什么Linux CFS调度器没有带来惊艳的碾压效果?| CSDN博文精选,第1张

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

作者 | 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】

上回书说到 Linux进程的由来 和 Linux进程的创建 ,其实在同一时刻只能支持有限个进程或线程同时运行(这取决于CPU核数量,基本上一个进程对应一个CPU),在一个运行的 *** 作系统上可能运行着很多进程,如果运行的进程占据CPU的时间很长,就有可能导致其他进程饿死。为了解决这种问题, *** 作系统引入了 进程调度器 来进行进程的切换,轮流让各个进程使用CPU资源。

1)rq: 进程的运行队列( runqueue), 每个CPU对应一个 ,包含自旋锁(spinlock)、进程数量、用于公平调度的CFS信息结构、当前运行的进程描述符等。实际的进程队列用红黑树来维护(通过CFS信息结构来访问)。

2)cfs_rq: cfs调度的进程运行队列信息 ,包含红黑树的根结点、正在运行的进程指针、用于负载均衡的叶子队列等。

3)sched_entity: 把需要调度的东西抽象成调度实体 ,调度实体可以是进程、进程组、用户等。这里包含负载权重值、对应红黑树结点、 虚拟运行时vruntime 等。

4)sched_class:把 调度策略(算法)抽象成调度类 ,包含一组通用的调度 *** 作接口。接口和实现是分离,可以根据调度接口去实现不同的调度算法,使一个Linux调度程序可以有多个不同的调度策略。

1) 关闭内核抢占 ,初始化部分变量。获取当前CPU的ID号,并赋值给局部变量CPU, 使rq指向CPU对应的运行队列 。 标识当前CPU发生任务切换 ,通知RCU更新状态,如果当前CPU处于rcu_read_lock状态,当前进程将会放入rnp->blkd_tasks阻塞队列,并呈现在rnp->gp_tasks链表中。 关闭本地中断 ,获取所要保护的运行队列的自旋锁, 为查找可运行进程做准备 。

2) 检查prev的状态,更新运行队列 。如果不是可运行状态,而且在内核态没被抢占,应该从运行队列中 删除prev进程 。如果是非阻塞挂起信号,而且状态为TASK_INTER-RUPTIBLE,就把该进程的状态设置为TASK_RUNNING,并将它 插入到运行队列 。

3)task_on_rq_queued(prev) 将pre进程插入到运行队列的队尾。

4)pick_next_task 选取将要执行的next进程。

5)context_switch(rq, prev, next)进行 进程上下文切换 。

1) 该进程分配的CPU时间片用完。

2) 该进程主动放弃CPU(例如IO *** 作)。

3) 某一进程抢占CPU获得执行机会。

Linux并没有使用x86 CPU自带的任务切换机制,需要通过手工的方式实现了切换。

进程创建后在内核的数据结构为task_struct , 该结构中有掩码属性cpus_allowed,4个核的CPU可以有4位掩码,如果CPU开启超线程,有一个8位掩码,进程可以运行在掩码位设置为1的CPU上。

Linux内核API提供了两个系统调用 ,让用户可以修改和查看当前的掩码:

1) sched_setaffinity():用来修改位掩码。

2) sched_getaffinity():用来查看当前的位掩码。

在下次task被唤醒时,select_task_rq_fair根据cpu_allowed里的掩码来确定将其置于哪个CPU的运行队列,一个进程在某一时刻只能存在于一个CPU的运行队列里。

在Nginx中,使用了CPU亲和度来完成某些场景的工作:

worker_processes      4

worker_cpu_affinity 0001001001001000

上面这个配置说明了4个工作进程中的每一个和一个CPU核挂钩。如果这个内容写入Nginx的配置文件中,然后Nginx启动或者重新加载配置的时候,若worker_process是4,就会启用4个worker,然后把worker_cpu_affinity后面的4个值当作4个cpu affinity mask,分别调用ngx_setaffinity,然后就把4个worker进程分别绑定到CPU0~3上。

worker_processes      2

worker_cpu_affinity 01011010

上面这个配置则说明了两个工作进程中的每一个和2个核挂钩。

Linux的调度策略区分实时进程和普通进程,实时进程的调度策略是SCHED_FIFO和SCHED_RR,普通的,非实时进程的调度策略是SCHED_NORMAL(SCHED_OTHER)。

实时调度策略被实时调度器管理,普通调度策略被完全公平调度器来管理。实时进程的优先级要高于普通进程(nice越小优先级越高)。

SCHED_FIFO实现了一种简单的先入先出的调度算法,它不使用时间片,但支持抢占,只有优先级更高的SCHED_FIFO或者SCHED_RR进程才能抢占它,否则它会一直执行下去,低优先级的进程不能抢占它,直到它受阻塞或自己主动释放处理器。

SCHED_RR是带有时间片的一种实时轮流调度算法,当SCHED_RR进程耗尽它的时间片时,同一优先级的其它实时进程被轮流调度,时间片只用来重新调用同一优先级的进程,低优先级的进程决不能抢占SCHED_RR任务,即使它的时间片耗尽。SCHED_RR是带时间片的SCHED_FIFO。

Linux的实时调度算法提供了一种软实时工作方式,软实时的含义是尽力调度进程,尽力使进程在它的限定时间到来前运行,但内核不保证总能满足这些进程的要求,相反,硬实时系统保证在一定的条件下,可以满足任何调度的要求。

SCHED_NORMAL使用完全公平调度算法(CFS),之前的算法直接将nice值对应时间片的长度,而在CFS中,nice值只作为进程获取处理器运行比的权重,每个进程都有一个权重,nice优先级越高,权重越大,表示应该运行更长的时间。Linux的实现中,每个进程都有一个vruntime字段,vruntime是经过量化的进程运行时间,也就是实际运行时间除以权重,所以每个量化后的vruntime应该相等,这就体现了公平性。

CFS当然也支持抢占,但与实时调度算法不同,实时调度算法是根据优先级进行抢占,CFS是根据vruntime进行抢占,vruntime小就拥有优先被运行的权利。

为了计算时间片,CFS算法需要为完美多任务中的无限小调度周期设定近似值,这个近似值也称作目标延迟,指每个可运行进程在目标延迟内都会调度一次,如果进程数量太多,则时间粒度太小,所以约定时间片的默认最小粒度是1ms。

进程可以分为I/O消耗型和处理器消耗型,这两种进程的调度策略应该不同,I/O消耗型应该更加实时,给对端的感觉是响应很快,同时它一般又不会消耗太多的处理器,因而I/O消耗型需要调度频繁。相对来说,处理器消耗型不需要特别实时,应该尽量降低它的调度频度,延长其运行时间。

参考: linux内核分析——CFS(完全公平调度算法) - 一路向北你好 - 博客园


欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/yw/5892202.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-03-06
下一篇 2023-03-06

发表评论

登录后才能评论

评论列表(0条)

保存