深入思考:如何理解CPU的负载

思考一个问题,CPU的负载代表什么?该如何理解CPU的负载呢?

我们以Linux为例。在Linux内核中,会定时统计CPU的负载。用uptime命令,可以看到CPU最近1分钟、5分钟、15分钟的负载。

上述命令获取的负载是一个数值。很多资料都会告诉你,如果是单核心CPU,那么小于等于1的负载被认为是正常的。否则,则认为任务工作量超过了CPU的能力。如果是多核心的CPU,小于等于核心数目的值被认为是正常的,否则,则认为CPU超负载工作了。

那么,我们到底该如何理解CPU的负载呢?

在Understanding Linux CPU Load - when should you be worried?中,负载被比喻为桥梁的通行能力。如下图:https://scoutapm.com/blog/understanding-load-averages

 

假设桥梁上可以同时通行六辆汽车,那么负载为1表示现在桥上刚好有六辆车。如果负载为0.5,则表示桥上有三辆车,使用了能力的一半。如果负载大于1,则表示有车辆在桥下排队等待上桥。通过这个例子,将桥的通行能力比作了CPU的处理能力,而汽车则表示了处理的工作或者时间片占用。对于多核心的情况,则表示大桥有多个车道。其他的概念则类同。

在负载值大于CPU数量的情况下,说明任务的执行会被排队,也就是要执行的工作量超过了CPU的处理能力,有些工作的完成要滞后了。

到此,这些概念还算是比较清晰的。

通过上面的例子,你是否掌握了CPU的负载概念了呢?是否感觉差不多了?好了,让我们先暂停这块,再插一个问题:知道CPU的负载有什么用?

对用户来讲,可以判断当前系统是否处于超负荷状态。比如,发现有些应用响应很慢,那么我们可以看看CPU的负载值,据此判断一下是否是过载了。

对于应用层运维人员来讲,可以判断自己的系统是否运行良好,是否有未期望的应用占用了大量CPU,据此可以优化系统。对于运维人员,这是基础能力要求。如果能据此作出优化最好,否则,了解这些内容,也可以提供更加丰富的信息给开发人员,所以,这跟看系统内存、IO情况等同等重要。

对于应用层开发人员来讲,通过负载能够判断自己构建的系统是否配合良好,是否做了很好的同步异步处理,阻塞非阻塞处理,是否有意外的死循环等等。

对于内核层开发人员来讲,通过了解负载,可以做负载均衡(对于对称多处理系统),负载优化,避免大材小用(特别是对于非对称处理器)等。

所以,总的来讲,了解CPU的负载,对各个层次的相关人员都是十分重要的。这也是为啥,我们要在这里深入探讨CPU的负载。

回到主题,我们进一步思考负载到底代表了什么。

前面用桥做比喻,虽然简单明了,但是,跟实际对比起来,似乎还是有些差异。比如,CPU执行的任务多种多样,有些任务耗时,有些任务不那么耗时,就好比虽然都是汽车,但是有些像是大卡车,有些则像小汽车。其次,CPU能力更像是一个时间概念,而非空间概念,用空间的事物来类比,就不好理解后续的衰减概念。也因为是时间概念,所以,即使是空闲状态,CPU也并没有停下来,而是任然在运转中,这对于负载的理解也产生了一些干扰。

其实,我们可以从CPU的特点来重新梳理负载概念,而不着急去找一个现实世界的类比物。

我们说CPU的负载,其实隐含了一个概念,那就是CPU的能力。同样的负载,或者说工作,能力强的CPU就可能在预期时间内完成,对外表现为响应及时;而能力弱的CPU则可能花费更多的时间,不仅对外表现为响应慢,也会因为处理慢,而一直忙于处理,使得CPU不得闲。在前面讨论CPU的发热时,已经有提到,得闲的CPU就像发动机的怠速状态,虽然也在转,但是耗电少,可以理解为处于休息状态。

那CPU的能力该如何衡量呢?

现在云计算和人工智能的流行,引出了一个热门的概念,那就是CPU的运算算力。现代人工智能芯片的算力都以TOPS为单位表示,也就是一秒钟多少万亿次运算。这是比较贴近CPU的本质的。CPU是做什么的?大家都知道,执行指令。这些指令所做的工作无非就是三大类:地址相关、数据相关、控制相关。控制类实现CPU的逻辑分叉,数据类完成运算,地址类则主要与指令和数据的访存有关。

CPU每秒钟完成的指令数是不是固定的呢?显然不是。访存相关的指令很可能要比内部的运算指令要慢。从微观角度来看,就是占用的机器周期数目不一致。CPU本身是一个复杂的系统,如果尽可能的将其看做一个白盒,我们会发现,CPU吃指令的速度是不一样的。从白盒看进去,你会发现,执行某些指令时,CPU内部的部分器件可能处于等待状态,这会导致CPU整体消耗指令的速度变慢。本来下一个周期就可以处理新的指令了,此时,可能要多等几个周期。

CPU的本质是执行指令,这是由其职责所决定的。而不同情况,CPU执行指令的速度可能不一样,这是否就是体现CPU能力的根本所在?

我们所有要CPU完成的工作,最终都体现为一系列的指令组合,而CPU所做的,就是一刻不停,不知疲倦的执行指令,从不偷懒。如果在单位时间内,CPU执行的指令数量多,那么CPU很大概率就是能力强,可以完成更多的工作,反之亦然。我们也可以换个说法,就成了完成同样的工作,对有的CPU来讲,是小菜一碟,对有的CPU来讲,则可能超出负载能力,导致延迟排队。

因此,所谓的CPU负载小于1的情况,就是CPU在规定的时间内完成了规定的任务。否则,则是无法完成,需要超时处理了。这样来看,CPU的负载是有限制的。也就是单位时间内,CPU可以执行的指令数。如果给它过多的指令,那么可能就会超出其能力范围,导致任务完成时间过长,表现为负载过大,响应过慢;举个例子,你可以在系统中创建大量的计算PI的进程,那么到一定数量后,可能就会存在计算变慢的情况,这就是CPU过载导致的。

有了上面的准备内容后,我们再来看CPU的能力受什么影响:

1 CPU是一个时钟驱动系统,所以它的极限能力受限于时钟频率。频率越高,周期就越短,完成同样数量的指令,所消耗的时间就越少,自然,单位时间内能够处理的指令数量就越多,可以据此认为CPU的能力就越大。

2 在时钟频率确定的情况,CPU执行一条指令的时间就是固定的,上面的算力也就是确定的。但这并不代表CPU的能力就到此了。因为还可以通过其他空间手段来优化时间效率。最常见的就是流水线。流水线的终极目标就是实现指令的单周期时间消耗。这是理想情况。

3 是否上面的2就是CPU能力的极限了?也不是。因为还可以进行优化,进一步的实现空间换时间。这就是现代CPU都采用的超标量和乱序执行。虽然可以通过流水线,实现理论上指令的单周期消耗,但是如果集成更多的执行部件到CPU内部,那么就相当于并行流水线,可以进一步的缩短指令平均执行时间。从这一点发散开来,多核心架构的原理也是如此。

4 上面的手段都是提高指令的平均周期消耗。但是我们知道,上面的只是理想情况,实际中指令不同,对总体执行时间的花费也是有影响的。比如指令之间的相关依赖等,还有指令的访存等。为此,CPU内部设计了很多优化措施,包括多层次多级别的cache等。这些手段,都是期望降低CPU指令执行的平均消耗,尽可能让其接近理想状态。

上面这些优化的目标,终归可以总结为一点,就是提升CPU的执行能力,再微观一点,就是缩短指令周期,让CPU单位时间内执行的指令数更多。有了这些概念,我们再回过头来理解负载。

CPU执行不同类型程序,单位时间完成的指令数虽然是不同的,但是从统计的角度来看,总归还是比较符合自身的能力的,这个能力受主频和架构设计影响。好的设计,本质上就是能量效率高,单位能量做功多。相对而言,负载能力强。

对于负载小于等于CPU核心数目的情况,就可以理解为交给CPU执行的指令数在CPU的吞吐能力范围内,否则,就是超出了CPU的能力范围,超出范围的结果,就是超出能力范围的指令会被延迟执行。这会反压到程序层面,表现为有的程序响应慢,出结果时间长等等。

我们再进一步理解交给CPU的指令数目。如果指令涉及到IO等操作,那么在CPU能力范围内则意味着CPU总是主动处于等待IO的状态。比如,从驱动来理解,就是在中断的间隔时间内完成了中断要处理的工作。如果指令为计算型居多的,比如CPU消耗型的程序,就以计算PI为例,是没有一个用户层面超出能力范围的界限的。在操作系统调度该程序时,其总是几乎要消耗完自己的时间片。这样的话,我们只能笼统的得到不同能力CPU计算出的结果精度或者同样精度所消耗的时间差异来做评估了。但是这种情况,CPU占用是100%的。这就像执行死循环。一个死循环就会耗尽一个CPU核心,多核心CPU上,如果有多个死循环占用多个核心,那么其他任务得到执行的时间就会受影响,最终可能表现为较多任务的排队等待。此时,我们说CPU负载大,其实是因为计算任务影响了非计算任务。所以,提交给CPU的指令要分情况来看:

无限循环型的任务,无论CPU的能力多强,都会吃掉CPU的所有能力。这就像是一个无底洞,倒多少水都装不满。这种情况导致的负载过高,就需要充分理解任务的目标来决定。比如,我就是要快速计算一个工程算式,那么是可以让CPU处于满负载的,以期尽可能缩短执行时间。如果是程序bug导致的死循环,那么在什么样的CPU上都是需要避免的。

非无限循环型的任务,这种一般是有交互的,无论是人机交互还是跟其他IO交互,我们可以以交互的完成情况来评估负载,就像前面的驱动例子。只要限制在交互的要求范围内,就可以以此评估CPU的负载。等待过程,CPU处于空闲,处理过程CPU处于占用工作状态,这样,如果所有任务的指令下发给CPU,最终CPU还有能力按要求时限完成,那么就在CPU的负载范围内,否则,可能会超出CPU的负载范围。包括过多的任务或者过长的处理。

总结一下:

CPU是一个不得闲的家伙。它的负载,其实是人为标注的。CPU自身并不知道自己负载是高还是低,除非自己不得闲。即使如此,CPU并不知道自己是否过载,即便不得闲。所以,我们说CPU过载,其实是说CPU没有按照我们的要求时限完成任务。这可能是任务过多或者处理过长等导致。只给CPU一个死循环,如果从任务的角度来看,负载可能是1(满负载),但是如果加上时间要求,那么负载可能是1,可能是2(两倍于满负载),也可能是很大很大的值。可以将其想象为很多连续小任务,且每个任务时间要求很短,这样假象的任务越大,定义出来的CPU负载可能就越大。

现在,我们可以把CPU设想为一个齿轮系统,简单指令只需少量齿轮转动即可完成,花费时间较少,复杂指令需要多个齿轮参与才可以完成,就需要花费较多时间。想提高这个齿轮系统的处理能力,既可以提高转速(频率),也可以优化结构,让平均参与齿轮数量减少。最终,从外部来看,这个系统有一个大概的吞吐能力,这个能力就是负载能力。

 

通过上面的分解,系统层面、全局层面的负载已经比较清晰了。这里补充一点,从内核角度来看不同CPU的负载。这涉及到调度和负载均衡。

内核如何认定各个核心的负载大小?大家都知道,内核有一个运行队列的概念。满足运行条件的进程,都会在运行队列上排队。对于文章开头所提的1分钟、5分钟和15分钟负载概念,有很多资料会根据队列排队进程的数目来讲解。如果排队的进程数目大于核心的数目,那么CPU很可能就处于过载的状态。

CPU处于过载状态,很大可能会导致运行队列上排队进程数目增加,这个很好理解;但是反过来,就需要一些条件了。举个比较极端的例子,内核中有一个计算任务和多个IO任务。在计算任务执行过程中,多个IO任务都处于就绪状态,此时,运行队列可能排队多个IO任务。但是,多个IO任务对CPU的占用都很小,也就是CPU花费很少的时间就可以完成所有IO任务的处理(比如CPU只做一个计数)。这样来看,虽然存在排队的情况,但是负载可能并不高。

那如何区分这类假排队负载情况呢?这就需要从宏观一点的视角来看了。如果以1秒为单位来看,可能会发现,上述情况只是一种巧合,CPU在大部分时间里都是空闲状态,只是在很短的一段时间内恰好进行排队处理。这样,如果我们考虑1分钟甚至更长时间,那么CPU大部分时间处于空间,自然占用率就是很低的状态。所以,上面队列数目要做参考,就需要加一个限定条件,那就是在一段时间内持续排队。这种情况就说明CPU无法在规定的一段时间内完成所有任务的一次处理。此时,我们才认为,给CPU的负载超出了CPU的处理能力。所以,在内核里,掌握一个CPU核心的负载情况,不能完全按照队列长度来决定,而是要考虑时间因素。

好了,现在换做你,要你来计算CPU的负载,你该如何对CPU的负载进行评估?

以上的讨论,都是说如何定性的评估CPU的负载。现在的计算,就是要细化到该如何定量的计算负载。

如前所述,CPU的负载并不是一个CPU可准确感知的量。CPU可以通过自己处理任务的复杂度,或者空间时间的占比,大概的评估出自身的负载,到底是不是忙的状态,但是,这种状态,到底有没有过载,还需要外部也就是这里的内核,来做出最终的判断。

内核所做的判断,就是人做的判断。这个判断该如何判断?

我们可以这样来类比CPU的工作,以便辅助理解负载。CPU就好比是一个如前所述的带很多齿轮的大机器,指令就好比是各种类型的包裹。没有包裹处理时,CPU的齿轮处于怠速状态,也就是只有很少量的齿轮在旋转。当有任务要处理时,也就是有一堆包裹要处理,此时,CPU中参与处理的齿轮就相应的要多一些。且不同类型的包裹,需要参与的齿轮数量不同,消耗的时间有所差异。任务相关的包裹由车辆运输过来,等待运输的过程,可以类比为进程的等待过程,比如IO等待。这样,我们来看CPU的负载概念。

CPU低负载的情况就是要处理的包裹很少,大部分时间在等待中。CPU自身多处于空转状态。这种情况,CPU处于耗电少,发热少的状态。

CPU中等负载的情况就是包裹有一些,但是CPU仍然有不少时间是在等待中,处理速度相比待处理量还是有优势的。

CPU高负载的情况就是包裹接连不断,CPU很少、甚至几乎没有时间等待。这种情况,CPU处于高占用状态。相对而言,这种情况可以理解为处于一种平衡状态。

CPU过负载的情况就是包裹接连不断,CPU得闲时间几乎没有了。即便如此,还是有包裹不能及时处理完,出现了堆积情况。堆积的包裹因为得不到及时处理,最终会延迟处理,表现在程序上,就是响应迟钝或者长时间得不到响应。

基于上面的论述,我们该用什么变量来衡量CPU的负载呢?

显然,CPU设计并制造完成后,其特性就定下来了。当然,部分特性还是可以调整的,比如频率。这里,我们为了讨论方便,假定这些都不再变化。这样的话,哪些东西会影响到CPU的负载呢?CPU的负载如何来定量的衡量呢?其实,这两个问题并不独立,因为它们说的是一件事的不同方面。这里分开,只是为了讨论方便。

从前面CPU的几种负载情况说明来看,包裹数量也就是任务数会影响CPU的负载。其实,不单是任务数,任务特性也会影响CPU负载。这里的任务特性就是类比而言,就是运输特性。计算密集型的任务,运输的块,包裹源源不断的到来,而IO型的任务,对应的就是运输的慢,可以认为走的是山路,CPU对这类任务,大部分时间可能是处于等待状态。所以,即使是很多任务,如果都是IO密集型的任务,CPU的负载也可能很小;相反,如果是计算密集型的任务,即使数量很少,CPU的负担也可能很重。综合来看,就是要处理的包裹数量(指令数量)对CPU的负载起决定性影响。

除了包裹数量,还有其他影响因素吗?显然,包裹本身的特性也会有影响。这就是反映到指令特点上了。不同的指令,其对CPU的影响不同。比如,运算型指令,CPU处理起来得心应手,访存型指令处理起来就比较耗时,因为要等到指令从内存搬运过来。虽然有cache做了缓冲,但是中间仍可能出现意想不到的波折。指令的这些特点,其实也是由任务的特点决定的。比如图像处理类任务,很可能既是计算密集型的,又是访存型的。不但要处理的多,而且处理还相对慢。对于指令的这些特点,在此处分析中,并不做过多展开,我们还是统一将其当做指令好了,大不了就是一条指令等价于其他类型的两条指令或者多条指令,最终都是能够对应到指令量上的。

既然指令量影响CPU负载,那么具体该如何定量评估CPU的负载呢?

前面已经说了,负载本质是人看到的,CPU本身并不感知。特别是对于高负载和过负载情况。CPU认为自己勤勤恳恳的处理呢,并没有偷懒,那最终这个处理速度,对使用者人而言,到底是可接受还是不可接受,其并不能左右。对CPU自身而言,其能看到的就是空闲时间有多少。如果有空闲时间,那么就说明还没有过负载,否则就需要使用者来判断了。对使用者而言,过负载情况,就需要根据排队的任务来衡量了。但是,这里排队的任务之间,该如何比较呢?任务是什么类型的,这显然不是事先能够预测的,就目前计算机的设计而言。能够明确的变量就是数量,等待状态任务的数量。这个值也是在一定程度上有参考价值,就是数量越多,CPU的负担越重。但显然也并不绝对。如之前所述,多个IO型任务的排队对CPU增加的负载可能比不上一个计算密集型任务。不过,因为这很难预测,所以能直接拿来用的还是任务数量。虽然无法直接获取任务类型的影响程度,但是间接而言,每一个任务都对其他任务有影响,任务的特点本身带来的影响,会最终间接反馈到任务积累的数量上。所以,拿数量来做参考,还是有广泛意义的。

好了,到此我们分析了两个评估因素:

一个是CPU时间占比。这反映了CPU的空闲程度。

一个是等待完成的任务数量。这表达了CPU的繁忙程度。

这两个因素跟包裹数量也是相互影响的。包裹数量多,CPU空闲时间就会少,等待完成的任务数量可能就会多。但是,最终我们能够直接获取的参考量,目前来看就是CPU的空闲时间占比和排队任务数量。指令量的获取并不现实。

取一个参考标准1,将CPU不得闲的情况对标到这个标准上,那么小于1的情况就是CPU还能得闲,负载处于可控状态。这只是一般情况而言,因为有一些任务可能是有实时性要求的,比如音视频,虽然CPU能够在一定的时间范围内完成处理,也就是任务还没有超过CPU的处理负载,但是可能因为不符合人的延迟要求(间隔不够均匀),而给你一种过载的错觉。这里面就还涉及到调度的影响。总之,小于1 的情况,就是CPU的处理和包裹的搬运(任务调度)处于可平衡的状态(搬运不可忽略,因为CPU无法做到瞬时处理完一次搬运的包裹,也就是任何任务都会有执行时间,而非瞬间完成)。当CPU不得闲,那么可能就会出现包裹积压,持续的积压,可能导致排队任务数量增加。

此时,该如何划定CPU的负载值,相对1而言的。

假设我们有预测能力,获取说能够看到未来,那么我们就可以计算出积压的包裹量,并据此计算出需要消耗的CPU时间。这种情况,只需要划定一个单位时间,将CPU满载状态(进一步假定CPU匀速运动)时在该单位时间内处理的工作量定位1,那么,积压的工作量与CPU的处理能力一比较,就可以算出精确的负载来。比如,1代表CPU刚好满载。2代表了二倍的负载,也就是需要两个CPU满负载才可以完成。以此类推。

但是,显然,这个世界并不存在魔法球,让我们一窥未来。所以,即便有预测,那也是猜测,而非真实。既然这样,该如何界定负载2呢?其实这里,对于1我们也没有很好的定义,而是单纯根据单位时间内CPU的占比来划定1,所以对于2,就更难以计算了。更准确的结果应该是根据CPU的算力来评估。

这时候,我们就需要回到最初,我们评估或者计算CPU的负载到底是为了什么?就算你给CPU安排10倍于它能力的计算量,CPU也不会像驴子一样,甩一鞭子就会快一点。10倍的计算量得到的只有10倍的计算时间。所以,通过负载,我们应该要获得的是:

1 是否可以优化我们的程序,让其能量密度更高一些,这样完成同样的事情花费的指令数量更少,从而让其处于CPU的能力范围。毕竟CPU是设定死的了,但是程序却是可以动态改变的。

2 优化我们的调度。这隐含的至少包括两方面,一是是否可以优化任务的顺序,让重要的任务先安排上,就跟各种公共机构中的军人优先一样。另外,优先的任务也能够获取更多的CPU时间。二是对于多核心或者多CPU的情况下,可以优化任务的实时调度,避免CPU的负载不平衡。如果有的CPU或者核心没事可干,有的CPU忙不过来,显然这是不合理的。

3 还有一点就是人类自身的需求。通过计算负载量,让我们对CPU的执行环境有一个相对准确的评估。

对于上面的第一点,程序的优化并不能够在运算过程中进行,所以负载只是给我们一个程序是否需要优化的提示。这种情况,我们只需要知道CPU占用率很高就可以了,至于是否能够精确估计出负载为1还是2关系并不大

那么,对于上面第二点呢?因为任务贡献的负载本身很难通过指令量来计算,所以,优先级是人为添加上去,而非计算出来的。这就排除了2中的第一条。2中的第二条,则是实实在在可以动态计算调整的。

对于上面的第三条,其实跟1是有点类似。因为很难有精确的计算,所以只要有一个统一的标准,那也算初步实现了目标。至于2是否就是精确的两倍于1,在我们能够实现所有CPU都可以不长时间过载的目标时,则显得并不那么重要。

现在,所有的焦点都聚集到了多CPU或者核心的调度上了。为了完成这个目标,我们只需要知道两个要素就可以了:1是CPU当前的负载情况;2是每个任务的负载,也就是任务会在CPU上占用多少负载。如此一来, 目标就变成了一个类似动态规划的问题。已知有N个CPU(或核心),每个CPU的当前负载,任务池里的任务数量及每个任务的贡献负载,计算一个最优的调度。

上面的已知量中,CPU数目已知,CPU的负载已知,任务数目可知,任务的贡献负载未知。现在要计算这个问题。目标是实现负载均衡,就是让每个CPU的负载都尽可能接近,该怎么做。

这其中还有很多关联问题:任务的特点;cache的热度等等。如果我们可以站在上帝的角度,一窥全局,那么问题还是可以处理的,但是,现在有很多因素,并不能预先得知,虽然有一些可以间接判断(比如优先级),但是最终毕竟还是预测过程。所以这个问题其实就变成了不断的试错过程。但是,指导原则还是要合理的。CPU数目和任务数目是无争议的。CPU当前负载和任务的贡献负载,其实需要统一按一个标准来。当前负载贡献大的任务,下一个单位时间负载贡献还可能是大的。这属于合理的假设了。为了解决这个问题,该如何统一CPU当前负载和任务负载?另外还要考虑任务的后续影响。

可以想见,最简单模型可以构建如下:

1 CPU的负载根据空闲占比来评估;

2 任务数量少于CPU数目的情况下,将任务随机分配到各个CPU;

3 新任务优先分配给CPU占比负载小的那个;

4 任务数目大于CPU数目,出现排队的情况时,此时假设单位时间内CPU过载了。在无法评估任务的情况下,对任务同等对待,就是按数目,均分给各个CPU。

以上,就是一个最简单的负载关联模型了。CPU的负载按照空闲占比来计算,任务的负载按单位时间满负载来参与计算。也就是一个任务,占用一个1,如果某个CPU排队了两个任务,则认为该CPU的负载很可能是2.

简单模型的问题就是无法应对复杂的情况。最直接的就是任务的差异。前面也已经提到过了,有的任务运输包裹(指令)的过程比较慢(存在等待),其对CPU的负载,就要小点。当然,这都是拉长评估时间的情况下。

那有没有更好一点的模型呢?其实这里的问题还是在于负载的评价标准上。如何将任务的贡献统一起来?

因为CPU的负载跟时间占比有关,我们就将时间拉长一点,划定一个所谓的有一定持续量的所谓单位时间n。在这个时间量里,任务对CPU的占用就是CPU的负载。比如,以1ms为单位,任务运行占了700us,那么CPU的负载就是0.7。对于未来将要执行的任务,其负载怎么计算?在上面的模型中,我们将其假设为了1。当这个单位时间划分的比较小的情况下,我们就认为任务在这个单位时间内对CPU的占用是1,似乎也是合理的。这样我们只评估了任务未来的很小一段时间的负载贡献。随着任务的执行,任务的特点就会展示出来,比如自身可能就会让出CPU,此时,该任务自然不对CPU贡献负载。只要任务还排着队,任务就可能对CPU贡献负载。存在多个任务时,在单位时间内,任务并不能并行(对于一个CPU或核心的情况),所以任务将长期对CPU贡献负载,自然,两个任务就贡献2,以此类推。

到此,我们看了当前任务和未来单位时间的任务负载。但是,短时间内的负载值价值并不大,我们更需要的是持续一段时间的负载。如此,任务的特性在上述模型的计算中就会弱化。其实,在考虑特性时,我们一直都忽略了任务的过去。一个过去一段时间对CPU高占比的任务,对CPU的贡献显然要大于当前突然出现的,小段时间占用CPU的任务。即使该任务当前占用了CPU。

考虑到时间的持续后,我们就可以思考任务的特性了。这涉及两个点,一个是任务的未来,一个是任务的过去。

对于任务的未来,能直接使用的参考量就是任务的优先级。高优先级的任务,我们推定未来占用CPU的比率要高一些。这只是在一定程度上,而非必然。对于任务的过去,参考linux中的pelt算法,引入一个衰减概念,这样,任务的历史贡献也就可以考虑进来了。

假定基准任务的贡献标准值为100,那么高优先级的任务,贡献值就高于100,低优先级的任务,贡献就低于100。将这个贡献值与任务运行的时间结合起来,我们就可以得到一个带时间和优先级属性的负载贡献值。任务的当前和未来,就按这个值评估,任务的过去,则对这个值进行一个随时间变化的减少,以此作为对任务的评估。那么在进行调度时,我们就以任务对CPU的负载贡献来评估任务,以此对任务进行合理安排。

上面我们其实是为了调度,评价了任务的负载。在具体调度时,还需要选择CPU,那么如何评价CPU然后做出选择呢?这可以有下面几种方式:

1 是任然按照CPU的使用占比,就是当前CPU的使用占比,显然,这种仅仅适用于单位时间的情况。实际中,CPU的占比是快速变化的,所以,瞬时的占比并不能代表太多含义。

2 是按照一段时间的占比平均值。这有一定参考意义,但是,这个占比其实是任务对CPU影响的二级表达量。考虑这个因素,不如直接考虑下面3这个因素。

3 按照一段时间CPU运行任务的负载贡献来评估。在评估时间段内,运行了那几个任务,每个任务在这段时间内的CPU使用占比,然后将这贡献值合并起来,就可以作为CPU的负载评价。比如,在1ms时间内,任务A使用了200us,任务B使用300us,任务C使用了400us,三个任务的优先级分别为90,100,110,那么这段时间CPU的负载值就是0.2*90 + 0.3*100 + 0.4*110 = 92. 我们满负载参考值为100优先级的任务100%占用CPU,就是100.可见,最终,这个负载评估,包含了CPU的使用率和任务的优先级。

到此,我们用了统一的标准来评价任务负载和CPU负载。这个模型,相对于简单任务数量考量和CPU时间占比考量,是否要更合理和准确,可能需要在实际系统中来验证一下。验证标准就是,同样的CPU和同样的任务,系统的响应、任务的响应和时间的消耗等,是否更优。

上面的模型只是为了实现更好的调度和负载均衡,并没有抛弃CPU的使用占比。CPU的利用率仍然可以作为一个独立的参考量,提供给用户。

关于负载的相关讨论就到此。

其实,到这里,我们就可以引出linux的pelt算法了。该算法对负载的计算和评价,有了新的考量,特别是参考信号领域的衰减概念。从能量守恒的角度来看,似乎是要更合理的。

最近在看这个算法的实现内容。这里补充记录几点。

在这个算法中,对于负载的计算或者说更新是基于下面这样一个图例:

 

上图中的计算分为了三部分。在看这部分的内容时,对于这三部分的计算,有一些让人疑惑的地方,思考后整理如下:

Pelt算法将前述的单位时间设定为1024us。为啥不是1000呢,因为1024就是2的10次方,在程序中便于通过移位来快速计算。时间轴为从左到右。左边为历史时间。

上图中,T0为上一次计算负载的时间,T1为当前计算负载的时间,因此经过的时间为T1-T0。在这一段时间(T1-T0)中,经历了px+p个完整周期。D0为上次统计时,不满足一个周期的部分,凑上D1为一个周期。D3为这次统计时,不满足一个周期的部分,加上后续阴影部分才是一个周期,不过阴影部分属于未来时间。

所以,本次时间段,实际的完整周期为px部分,还有两头的不完整部分,包括D1和D3。在本次更新负载时,需要计算的就是D1加D2加D3部分的负载之和。上一次的负载其实是Dp加D0段的负载之和。

因为pelt考虑了历史因素,也就是根据时间周期,对相应的负载做了衰减处理,所以,某一个时间点的负载为

L = L0*y0 + L1*y1 + L2*y2 + … + Ln*yn

其中,y0=1,也就是说,对于当前周期来讲,没有衰减。这里的yi在很多书和资料里,都被写成了次幂的形式,个人理解不是这样的,因为如果不注意这一点,会影响对后面计算的理解。虽然衰减会越来越大,也就是yn会越来越小,到一定周期过去后,会衰减为零,但是并不是y3=y1*y2.,不存在这种计算关系。当然,可以根据物理中的衰减理论,或者对应的曲线,抽样出几个离散的点,作为衰减因子,比如如下图这样,不过计算时,我们需要将其理解为各个离散独立的变量。

 

上图中,竖线为采样点,固定间隔,图中只是示意了部分。水平为获取的衰减系数。

我们将不同采样点获取的衰减系数表示为yi,其中y0为1,y32为0.5。具体的值,在内核中通过查表获得。

假设time0时刻更新的负载为loadT0,那么,T1时刻的负载就应该是

LoadT1 = loadD3 + loadD2 + loadD1 + loadD0

以w表示weight,即代表任务的权重,sp表示scal_percent,也就是任务在一个周期里的CPU占比,那么有:(注意,这里是就一个进程来统计,所以weight是固定的)

loadD3 = w*spD3*y0

,其中n表示D2阶段持续的周期数。

 

LoadD1 = w*spD1*y(px+p),也就是其经历的px+p个周期的衰减。

LoadD0 = loadT0*y(px+p),即T0时刻的负载经过px+p个周期衰减后的值。

LoadD1+loadD0 = (w*spD1 + loadT0)* y(px+p)。因为p之前的周期已经在loadT0计算时包括了,所以不用重复计算了。

这里我们没有把衰减系数的表达式写成指数形式,因为y的N+1次幂是y的N次幂乘以y,所以,写成指数形式,容易将上面的y(px+p)理解成y(px+1),实际上y(px+p)是查表获得的一个新值,并不是y(px)*y1。记住上图的衰减函数,取值是离散的。当然,我们可以设计这个衰减函数满足指数幂运算关系,比如就是1/n的幂函数,结果就会满足幂运算,但只能是作为特例存在,而不是普遍形式。

对于loadD0的计算,可以这样辅助理解:

因为L = L0*y0 + L1*y1 + L2*y2 + … + Ln*yn

所以loadD0 = Ldp+Ld0。其中Ldp是如上L的表达形式,表示经过了dp个周期后的累积负载值。Ld0是d0时间段的负载值。在当时统计时刻T0来讲,就跟当前T1时刻的D3类似,是w*spD0*y0,经过px+p个周期后,loadD0就成为了

Ldp*y(px+p) + w*spD0*y0*y(px+p),也就是loadT0*y(px+p)了。

上面整理的负载计算公式中,w和sp及y相关的参与量都是已知值,所以计算不存在问题。类似y(px+p)和y(px)的计算,都是查表获得的,注意,y(px+p) 不等于y(px)*y(p)。唯一需要注意的是,loadD2的计算。因为它跨了多个周期的衰减,这些周期的增加值是逐步累积过来的,所以,表达式中存在连加的运算。根据一些资料,在linux4系列版本中,这部分也是通过查表获得的,内核计算了1024*(y0+y1+…+yi)的值。1024是周期长度,1024us,在运算中代表单位时间段。通过预先计算y0、y0+y1、y0+y1+y2…,以此避免后续的运算量。但是在linux5.0中,上述loadD2的计算表达式为:

LOAD_AVG_MAX – decay_load(LOAD_AVG_MAX, periods)- 1024;

该如何理解这个计算方式呢?我是这样理解的:

 

以上图为参考。每一个方格代表一个周期的负载。随着时间的推移,左边的方格逐步移到右边的方格之下,并且需要乘上一个衰减系数,代表这个周期的负载随着时间的推移在衰减。

现在我们将时间拉到最后一列。为了简化讨论,将第一个方格代表前面的D3段,第二个和第三个代表D2段。

LOAD_AVG_MAX代表了负载的最大值,也就是统计了无限个周期后的值。对应到图中,就是每一列的最大值。上图中最后一列第四和第五个方格及之下部分,就是第二列经过px周期(图中示意为2个周期)后的衰减负载值。

这样来看上面的计算公式,periods部分的负载就是LOAD_AVG_MAX减去第二列的最大负载经过px周期后的衰减值,即decay_load(LOAD_AVG_MAX, periods),再减去第一个的1024后的剩余值。

大家如果有更好的理解,可以一起讨论。

感觉说了很多废话。可能是因为边思考,边敲键盘的结果。

  • 5
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

龙赤子

你的小小鼓励助我翻山越岭

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值