为什么需要PELT?
之前CFS以每个运行队列为单位跟踪负载;存在几个问题:
- 一个运行队列存在很多个调度实体,所以无法存在当前的负载来源;
- 即使工作负载相对稳定的情况下,在rq级别跟踪负载,其值也会产生很大变化。(为什么?)
为了解决以上问题:提出了PETL算法,PELT算法跟踪每个调度实体;
如何进行PELT
- 时间(物理时间,不是虚拟时间)被分成了1024us的序列(以1024us为单位划分)
- 在每一个1024us的周期中(注意这里是一个周期),一个entity对系统负载的贡献可以根据该实体处于runnable状态(正在CPU上运行或者等待cpu调度运行)的时间进行计算。(负载就是runable的时间占比)
- 比如:该entity在该周期内,runnable的时间是x,那么对系统负载的贡献就是(x/1024);
- 一个实体在一个计算周期内(注意是计算周期,有多个1024us)的负载可能会超过1024us(这里以us时间为单位的原因是负载就是runable时间的比值),这是因为我们会累积在过去周期中的负载。当然,对于过去的负载我们在计算的时候需要乘一个衰减因子。
- 如果我们让Li表示在周期pi中该调度实体的对系统负载贡献,那么一个调度实体对系统负荷的总贡献可以表示为:
L = L0 + L1 * y + L2 * y2 + L3 * y3 + ... + Ln * yn
y32 = 0.5, y = 0.97857206
如何计算第n个周期的衰减值?
- 内核提供decay_load()函数用于计算第n个周期的衰减值;
- 为了避免浮点数运算,采用移位和乘法运算提高计算速度:
- decay_load(val, n) = val * y^n = val * y^n * 2^32 >> 32 = val * (y^n * 2^32) >> 32
- 将 (y^n * 2^32) 的值提前计算好放入数组
runnable_avg_yN_inv
中;
如何计算当前负载贡献
某个时间点的负载u'过了p个周期后的负载值是多少?
- 假如某个时间点的负载: u = xy^3 + 1024y^2 + 1024*y + x;
- 相当于分为了四段;
- 过了p个周期之后,每一段负载变为:
- x*y^3 * y^p
- 1024*y^2 * y^p
- 1024*y * y^p
- x * y^p
- u的负载是这四段之和,过了p个周期之后,u的负载是:xy^3 * y^p + 1024y^2 * y^p + 1024*y * y^p + x * y^p = u * y^p;
- 也就是说u过了p个周期后的负载值是: u' * y^p
当前负载计算
d1 d2 d3^ ^ ^| | ||<->|<----------------->|<--->|... |---x---|------| ... |------|-----x (now)p-1u' = (u + d1) y^p + 1024 \Sum y^n + d3 y^0n=1= u y^p + (Step 1)p-1d1 y^p + 1024 \Sum y^n + d3 y^0 (Step 2)n=1
首先分为两大段:
- (Step 1):计算历史负载u对当前负载的贡献
u y^p
- (Step 2):计算经过时间的负载贡献;
- 假设时间d是经过p(就是完整的周期数+1)个周期(d=d1+d2+d3, p=1+d2/1024),把当前经过的时间分为了三段:
- d1:d1是离当前时间最远(不完整的)period 的剩余部分
- d2: 是完整period时间
- d3: d3是(不完整的)当前 period 的剩余部分
- 假设时间d是经过p(就是完整的周期数+1)个周期(d=d1+d2+d3, p=1+d2/1024),把当前经过的时间分为了三段:
代码实现
数据结构
第n个周期的负载
if (unlikely(local_n >= LOAD_AVG_PERIOD)) {val >>= local_n / LOAD_AVG_PERIOD; /* 左移一位表示除2,y^32=1/2,也就是左移一位相当于计算y^32;这里计算可以整除多少个32,也就是左移多少位; */local_n %= LOAD_AVG_PERIOD;}
- y^32 = 1/2;
- 左移一位表示除2;
- 也就是左移一位相当于计算y^32
- local_n / LOAD_AVG_PERIOD的值,表示可以整除多少个32,也就是左移多少位;
- mul_u64_u32_shr 计算剩余部分的值;
(u64)(((unsigned __int128)a * mul) >> shift);