读者朋友们参考到的内核书和文献里面,往往会提到sysctl_sched_latency、sysctl_sched_min_granularity、sysctl_sched_wakeup_granularity等这些关键CFS调配参数,以及CFS的调度周期计算等:
对于任务 i,它的时间片ti由以下公式计算:
· weighti是任务i的权重。
· total_weight是当前系统中所有可运行任务的权重总和。
· sched_period是调度周期,表示所有任务在一个调度周期内应获得的总 CPU 时间,通常由 sched_latency_ns 和 sched_min_granularity_ns 决定。
但是这些内容,随着Linux 6.6内核引入EEVDF而逐步消亡了,这就是为什么我们在前速的代码上面加上了减号。码农的人生为什么异常痛苦,刚学会的姿势,其实已经不存在了:-)
EEVDF是Earliest Eligible Virtual Deadline First的缩写,它同时追求CFS追求的fairness(公平性),但是也强调 interactivity(交互性)。
EEVDF仍然强调公平,比如5个nice值相同的各跑20%的CPU。但是公平其实可以被另外一种方式来实现,CFS的算法是谁的虚拟运行时间最小,就跑谁(欠谁的钱多,先还谁);而EEVDF则优先跑虚拟截止时间最小的任务(谁催债催地急,先跑谁。理论上讲,欠的最多的也大抵是催地最急的)。
EEVDF引入的一个新概念叫做“lag”(滞后),所谓滞后,就是任务还没有收到它理应的配额(比如20%还只跑了15%),只有lag值为正数的任务才是“eligible”(有资格)可以跑的,lag为负数证明那个任务已经跑超过配额了属于你欠我钱不是我欠你钱了。
上述所有的虚拟运行时间的计算方法都还是考虑了权重,这点与CFS是一样的,nice低的权重大,任务的虚拟运行时间不太容易长大。公式中任务应该跑的虚拟运行时间就是虚拟运行时间公平情况下的值。
一个“lag”值为负数的任务,一段时间不跑,但是随着时间的推移,它跑的CPU比例显然也会再次低于配额,未来会再次“eligible”。这个很容易理解,比如平均年薪12万人民币才是公平的,结果有个人第一年赚了24万,它显然是不“lag”了(不拖后腿)。但是给他放假一年不工作不赚一毛钱,它显然就又要“lag”(拖后腿)了,再次“eligible”(有资格)工作。
这个里面EEVDF引入了一个虚拟截止时间的概念,它的值是:
按照这个算法,马爸爸可能要等一万年才能被EEVDF再次调度到了,因为它前面lag值实在负地太多了。
根据虚拟截止时间的公式,任务的slice在这个里面还是很讲究的,我们假设有2个任务,1个任务对延迟的要求较高;1个任务对延迟的要求较低。然后这2个任务的nice值是相同的。
l任务1:月薪1万
l任务2:年薪12万
这2个任务其实按照上面的方法跑肯定还是公平的,但是如何体现任务1要的是月薪,而任务2要的是年薪呢?我们把两个任务的slice配置的一个小,一个大就好,这样时间片小的任务的虚拟截止时间很容易到来。其实你想过没,年薪12万的人只需要一年的最好几天赚足12万就够了,前面300多天其实是可以不干活的(CPU可以不调度它)。而月薪1万的呢,它必须在每月的最后一天赚足1万,这个紧迫性是不一样的。
类似大洋洲的澳大利亚、新西兰这种地方,薪水按周发,发月薪的“调度器”,员工可能觉得你这个调度器是骗子。那么,这种任务的调度紧迫度就又胜过发月薪和年薪的。
那个任务如何表达自己对紧迫性的需求呢?nice值低可以给任务更多的权重,但是它仍然不能表达任务对延迟的诉求,所以在Peter Zijlstra的“sched/eevdf: Use sched_attr::sched_runtime to set request/slice suggestion”工作中,它允许任务通过sched_attr::sched_runtime设置一个自定义的slice。应用程序对这个值的设置必须谨慎,设置小了抢占多,设置大了延迟大。它最终可以被内核调整到如下范围:
其中0.1ms是HZ=1000情况下的1/10,100ms为HZ=100情况下的10倍。
如果任务没有设置sched_runtime,则系统采用默认值。EEVDF里面有个一个配置参数叫:sysctl_sched_base_slice(由CFS的sysctl_sched_min_granularity更名而来),默认情况下为0.75 msec * (1 + ilog(ncpus)):
假设你的系统有8个核,那么factor=4,最终的base_slice就为0.75ms*4 = 3ms。除了作为默认slice以外,这个时间还有另外一个重要意义,类似发薪粒度,比如你月薪1万500,但是我只能1000为单位发,所以有时候你会拿到1万1(lag为负值),总是精确地发到1万500我这个系统里面的粒度可能太碎了,抢占太多。
在EEVDF的情况下,调度周期的计算方法如下:
W是所有任务的权重累计;r_i是任务i的slice(request), w_i是任务i的权重;某个特定任务的平均周期是:
每个任务公平地获得调度周期中的如下时间:
由此可见,整个EEVDF的调度周期,也是所有任务中平均调度周期最大的那个任务的平均调度周期。
在EEVDF的世界里,假设我们有如下3个任务:
l发周薪, 2307.69元
l发月薪,1万
l发年薪,12万
这3个任务共同决定了总的权重W,假设这3个任务的nice值如果都相同,那么它们的虚拟时间的行走速度是一致的。发年薪的人的调度周期也是所有EEVDF的调度周期。假设调度器旁边摆了一个小目标(一个亿),EEVDF的工作逻辑是:
快到周末了,发周薪的人离满足周薪2307.69元的截止日期近了,快发;快到月底了,发月薪的人离满足月薪1万的截止日期近了,快发;快到年底了,发年薪的人离满足年薪12万截止日期近了,快发。
所以,基本上,EEVDF强调一个“先闹先得”,但是最终公平因子实现“共同富裕”。
于是,短任务的调度频度可能很大,但是每次运行的时间不长;长任务的调度频度可能很小,但是每次运行的时间很长。
在EEVDF的论文中,有一个推演。下图中,有2个任务client1和client2,它们的权重都是2(仅用于推导,注意不要类比Linux里面可以规整到nice 0的1024权重周围的权重)。client 1在A时刻enqueue的,client2在B时刻enqueue。
这样,在A到B时刻之间,所有任务的权重是2,系统虚拟时间是真实时间的1/2。在B时间点后,由于系统有2个任务,W=4,所以虚拟时间变成真实时间的1/4。
client1和client2的request长度分别为2(拿年薪)和1(拿月薪),对应着我们设置的sched_attr::sched_runtime的slice的大小。显然,client2似乎更加紧迫。
B时刻(虚拟时间V=0.5,真实时间1),B由于还一毛钱没拿,所以它直接是eligible的,它的虚拟截止时间应该是再往后推
1(client2的request) / 2 (client2的权重) = 0.5
所以它的虚拟截止时间是 0.5 + 0.5 = 1,这个时候它和client1的虚拟截止时间都为1,我们可以选择让client2抢占client1。client2跑到了时间点C(真实时间2,虚拟时间0.75)。技术上来讲,在时间点C的时候,它已经拿到了它请求的长度1,于是它更新自己的eligible虚拟时间为1,虚拟截止时间为1.5,等着virtual time走到1.5之前再次被调度器“宠幸”。
如果留意看红色正方形里面的那一块的画,就可以看出B-C占B-D的1/2,这样其实也满足了client1/client2权重相等,应该各占1/2 CPU的fairness:
随着时间的推移,client2会在时刻D开始,再次有资格(eligible)领工资。
时刻C开始,因为client1的(0, 1)中的1小于client2(1, 1.5)中的1.5,client1抢占client2投入运行。
运行到时刻D,client1在A-B和C-D之间累计拿到了2个时间,年薪已达标,已经满足,于是更新自己的新的eligible和虚拟截止时间为(1,2),你应该有发现 2 - 1 = 1,不像client2的虚拟截止时间只是把eligible时间大0.5。
在时间点D, client1的虚拟截止时间是2,而client2的虚拟截止时间是1.5,我们让client2抢占client1。
这样client2跑到时间点E,时机时间点E它已经达标了,client2拿足配额了。它更新自己的eligible和虚拟截止时间为(1.5,2)。注意这个client2的E时刻的(1.5, 2)和client1 D时刻更新的(1,2)中的2是相等的,由于client2是“闪客”,我们也可以选择不要让client1抢占client2,于是client2继续跑到真实时间5(时间点F),虚拟时间点1.5,更新自己的eligible和虚拟截止时间为(2, 2.5),这样在F时间点,client1的(1, 2)中的2显然小于2.5了,client1抢占client2。