CPU负载和系统负载计算原理
调度中的负载概念,与平时熟知的cpu占用率并不是一回事,两者间有较大差别。本文分析了cpu负载和系统负载,并非CPU使用率。代码基于CAF- SM8250 - kernel 4.19。
负载计算中,其实主要分为3大部分,由小到大依次为:
1、调度实体负载:update_load_avg()----PELT
2、CPU负载:update_cpu_load_active()
3、系统负载:calc_global_load_tick()
这3个级别的负载计算分别体现了不同维度下的当前负载情况。
之前blog分析了调度实体sched entity级别的负载计算,是通过PELT机制来统计的。它体现了每一个调度实体的对cpu产生的负载情况,并进行了实时统计更新。(这块之前分析,复盘了下,还是不太详细和明确,后续还要再总结一下)
这次主要分析余下2个:CPU负载和系统负载的计算原理。
CPU负载计算原理
CPU负载是用来体现当前CPU的工作任务loading情况,和CPU繁忙程度的。其主要通过统计CPU rq上task处于runnable的平均时间(runnable_load_avg = runnable_load_sum / LOAD_AVG_MAX)。并根据不同周期,统计出不同的k线,来体现CPU负载的变化趋势。
我们知道单个task处于runnable的平均时间是由PELT算法机制来完成统计的。所以,我们此次分析更偏向于如何利用统计出的单个task数据,再进一步统计出不同周期的均线,来表示CPU负载。
代码路径如下:
scheduler_tick()
-> cpu_load_update_active()
void cpu_load_update_active(struct rq *this_rq) { unsigned long load = weighted_cpuload(this_rq); //load = cfs_rq->avg.runnable_load_avg if (tick_nohz_tick_stopped()) cpu_load_update_nohz(this_rq, READ_ONCE(jiffies), load); //(1) else cpu_load_update_periodic(this_rq, load); //(2) }
上面根据是否配置nohz而可能停止了tick走向不同的分支:
(1)cpu_load_update_nohz(this_rq, READ_ONCE(jiffies), load) //更新cpu负载,基于no HZ场景
(2)cpu_load_update_periodic() //周期性更新cpu负载,基于有HZ场景
NO_HZ的情况下,会走这个分支。pending_updates表示jiffies数是否更新,即表示tick数。 经过的秒数 = jiffies / HZ(平台当前HZ=250);一个tick为HZ倒数,即4ms。
/* * There is no sane way to deal with nohz on smp when using jiffies because the * CPU doing the jiffies update might drift wrt the CPU doing the jiffy reading * causing off-by-one errors in observed deltas; {0,2} instead of {1,1}. * * Therefore we need to avoid the delta approach from the regular tick when * possible since that would seriously skew the load calculation. This is why we * use cpu_load_update_periodic() for CPUs out of nohz. However we'll rely on * jiffies deltas for updates happening while in nohz mode (idle ticks, idle * loop exit, nohz_idle_balance, nohz full exit...) * * This means we might still be one tick off for nohz periods. */ static void cpu_load_update_nohz(struct rq *this_rq, unsigned long curr_jiffies, unsigned long load) { unsigned long pending_updates; pending_updates = curr_jiffies - this_rq->last_load_update_tick; //计算pending_updates if (pending_updates) { this_rq->last_load_update_tick = curr_jiffies; //更新时间戳 /* * In the regular NOHZ case, we were idle, this means load 0. * In the NOHZ_FULL case, we were non-idle, we should consider * its weighted load. */ cpu_load_update(this_rq, load, pending_updates); //(2-1)更新cpu rq的cpu load数据 } }
我们再看(2)这个分支:
``` static void cpu_load_update_periodic(struct rq *this_rq, unsigned long load) { #ifdef CONFIG_NO_HZ_COMMON /* See the mess around cpu_load_update_nohz(). */ this_rq->last_load_update_tick = READ_ONCE(jiffies); //记录更新的时间戳 #endif cpu_load_update(this_rq, load, 1); //(2-1)更新cpu rq的cpu load数据 } ```
最终都是调用了cpu_load_update()来更新cpu rq的cpu load数据。
(2-1)更新cpu rq的cpu load数据
/** * __cpu_load_update - update the rq->cpu_load[] statistics * @this_rq: The rq to update statistics for * @this_load: The current load * @pending_updates: The number of missed updates * * Update rq->cpu_load[] statistics. This function is usually called every * scheduler tick (TICK_NSEC). * * This function computes a decaying average: * * load[i]' = (1 - 1/2^i) * load[i] + (1/2^i) * load * * Because of NOHZ it might not get called on every tick which gives need for * the @pending_updates argument. * * load[i]_n = (1 - 1/2^i) * load[i]_n-1 + (1/2^i) * load_n-1 * = A * load[i]_n-1 + B ; A := (1 - 1/2^i), B := (1/2^i) * load * = A * (A * load[i]_n-2 + B) + B * = A * (A * (A * load[i]_n-3 + B) + B) + B * = A^3 * load[i]_n-3 + (A^2 + A + 1) * B * = A^n * load[i]_0 + (A^(n-1) + A^(n-2) + ... + 1) * B * = A^n * load[i]_0 + ((1 - A^n) / (1 - A)) * B * = (1 - 1/2^i)^n * (load[i]_0 - load) + load * * In the above we've assumed load_n := load, which is true for NOHZ_FULL as * any change in load would have resulted in the tick being turned back on. * * For regular NOHZ, this reduces to: * * load[i]_n = (1 - 1/2^i)^n * load[i]_0 * * see decay_load_misses(). For NOHZ_FULL we get to subtract and add the extra * term. */ static void cpu_load_update(struct rq *this_rq, unsigned long this_load, unsigned long pending_updates) { unsigned long __maybe_unused tickless_load = this_rq->cpu_load[0]; //获取之前周期为0的均线cpu load int i, scale; this_rq->nr_load_updates++; //统计cpu load更新次数 /* Update our load: */ this_rq->cpu_load[0] = this_load; /* Fasttrack for idx 0 */ //更新cpu load[0] for (i = 1, scale = 2; i < CPU_LOAD_IDX_MAX; i++, scale += scale) { //更新cpu load[1-4] unsigned long old_load, new_load; /* scale is effectively 1 << i now, and >> i divides by scale */ old_load = this_rq->cpu_load[i]; #ifdef CONFIG_NO_HZ_COMMON old_load = decay_load_missed(old_load, pending_updates - 1, i); //(2-1-1)将原先的old load做老化;如果pending_update == 1,就不用做负载老化 if (tickless_load) { //如果之前cpu load[0]有负载 old_load -= decay_load_missed(tickless_load, pending_updates - 1, i); //那么还要对tickless_load进行老化【针对这里为什么要添加tickless_load的考虑,后面说明】 /* * old_load can never be a negative value because a * decayed tickless_load cannot be greater than the * original tickless_load. */ old_load += tickless_load; } #endif new_load = this_load; /* * Round up the averaging division if load is increasing. This * prevents us from getting stuck on 9 if the load is 10, for * example. */ if (new_load > old_load) //这里是做了一个补偿,防止由于old load小于new load情况下,最终的cpu load都不可能达到最大值 new_load += scale - 1; this_rq->cpu_load[i] = (old_load * (scale - 1) + new_load) >> i; //计算当前新的cpu load } }
最后计算更新当前新的cpu load,实际为5份数据。分别对应不同周期长度的均线数据:
解释一下上面表格的意思:
针对CPU负载计算,会统计5条不同周期的均线,周期分别为{0,8,32,64,128},单位为tick。可以理解为统计了从当前tick开始往前推周期tick个数的load数据,周期内没有load,则load就会归0。
而在更新时,采用的计算公式:load = (2^idx - 1) / 2^idx * load + 1 / 2^idx * cur_load ----idx就是表格中【i】,load为old load,cur_load为新的load
那么整理后,为:当前load = 更新系数 * 旧的load + (1-更新系数)* 新的load ---更新系数为 (2^idx - 1) / 2^idx,最终参考【cpu_load】计算公式
现在我们来看,关于tickless系统(我们当前就是tickless/NO_HZ系统),错过了一些tick,那么就需要先将old load先老化,再考虑new load,重新计算当前的cpu load[1-4]。这里使用了查表法来减少计算量,提升性能。
PS:因为NO_HZ一般是出现休眠。那么休眠时,load是被认为为0的,所以,其实只需要考虑将旧的load进行衰减就可以了。
(2-1-1)将原先的old load做老化;如果pending_update == 1,就不用做负载老化
/* * The exact cpuload calculated at every tick would be: * * load' = (1 - 1/2^i) * load + (1/2^i) * cur_load * * If a CPU misses updates for n ticks (as it was idle) and update gets * called on the n+1-th tick when CPU may be busy, then we have: * * load_n = (1 - 1/2^i)^n * load_0 * load_n+1 = (1 - 1/2^i) * load_n + (1/2^i) * cur_load * * decay_load_missed() below does efficient calculation of * * load' = (1 - 1/2^i)^n * load * * Because x^(n+m) := x^n * x^m we can decompose any x^n in power-of-2 factors. * This allows us to precompute the above in said factors, thereby allowing the * reduction of an arbitrary n in O(log_2 n) steps. (See also * fixed_power_int()) * * The calculation is approximated on a 128 point scale. */ #define DEGRADE_SHIFT 7 static const u8 degrade_zero_ticks[CPU_LOAD_IDX_MAX] = {0, 8, 32, 64, 128}; static const u8 degrade_factor[CPU_LOAD_IDX_MAX][DEGRADE_SHIFT + 1] = { { 0, 0, 0, 0, 0, 0, 0, 0 }, { 64, 32, 8, 0, 0, 0, 0, 0 }, { 96, 72, 40, 12, 1, 0, 0, 0 }, { 112, 98, 75, 43, 15, 1, 0, 0 }, { 120, 112, 98, 76, 45, 16, 2, 0 } }; /* * Update cpu_load for any missed ticks, due to tickless idle. The backlog * would be when CPU is idle and so we just decay the old load without * adding any new load. */ static unsigned long decay_load_missed(unsigned long load, unsigned long missed_updates, int idx) { int j = 0; if (!missed_updates) return load; if (missed_updates >= degrade_zero_ticks[idx]) //不同的统计周期,超过了一定tick数,说明系统已经sleep这么长时间,那么old load就需要被清空了 return 0; if (idx == 1) return load >> missed_updates; //如果是周期为2均线,就直接根据missed_updates的个数,除以2次幂就行了,因为old和new的占比是各为1/2 while (missed_updates) { if (missed_updates % 2) load = (load * degrade_factor[idx][j]) >> DEGRADE_SHIFT; missed_updates >>= 1; j++; } return load; }
针对不同周期线的均线数据,老化的计算都是使用同一个公式:
load_n = (1 - 1/2^i)^n * l
oad_0 ---i就是idx,n则是pending_update-1,也就是
missed_updates
tickless_load
再说说tickless_load,我发现在之前的kernel版本上是没有的。也就是说,原先cpu负载的老化是完全按照上述2个公式严格计算的。
而现在加入了tickless_load,是考虑了上一次更新tick中cpu_load[0]的负载数据。将其也更新进了旧load(old_load)中。
个人认为这样做的目的是这样的,假设如下场景:
1. 上一次该均线load更新,此时load较小-----这部分load视作旧load
2. 期间有短暂的load剧增------这部分load视作tickless_load
3. 还没有到该均线load更新,进入休眠
4. 唤醒时刻load也比较小
5. 此次该均线load更新,此时load也比较小(与唤醒时刻load一致)
但是此时,如果不考虑ticklees load。那么该均线load此次更新会仅考虑旧load的衰减,再加上更新均线此时load,经过算法计算得出。但是此时的load与实际体现有较大差距,因为周期越长,那么新load更新时所占比例小,旧load所占比例大。所以,导致此时最终load结果与平均load有一部分相差,因为没有考虑到tickless load的负载,无法体现CPU平均负载真实情况。
而加入tickless_load之后,即将休眠前那部分load也会加入旧load计算。这样避免了那部分短暂load的计算缺失。
所以考虑tickless_load的最终老化的公式应该是这样的:
load_n = (1 - 1/2^i)^n * l
oad_0 + [1 - (1 - 1/2^i)^n ] *
tickless_load
最后结果load_n就是更新后的old_load,最后再用上面表格【cpu_load】计算公式,计算出最新的cpu负载数据。
tickless load这块的理解属于我个人理解,最好还是通过检查kernel提交记录查看commit信息。如有理解不对的地方,请指出。
CPU负载为何如此设计?
1、不同周期的均线用来反应不同时间窗口长度下的负载情况,主要供load_balance()在不同场景判断是否负载平衡的比较基准,常用为cpu_load[0]和cpu_load[1](这块后续分析load_balance时,需要check清楚)
2、使用不同周期的均线目的在于平滑样本的抖动,确定趋势的变化方向
系统负载计算原理
系统级的平均负载(load average)可以通过以下命令(uptime、top、cat /proc/loadavg)查看:
$ uptime 16:48:24 up 4:11, 1 user, load average: 25.25, 23.40, 23.46 $ top - 16:48:42 up 4:12, 1 user, load average: 25.25, 23.14, 23.37 $ cat /proc/loadavg 25.72 23.19 23.35 42/3411 43603
“load average:”后面的3个数字分别表示1分钟、5分钟、15分钟的load average。可以从几方面去解析load average:
If the averages are 0.0, then your system is idle.
If the 1 minute average is higher than the 5 or 15 minute averages, then load is increasing.
If the 1 minute average is lower than the 5 or 15 minute averages, then load is decreasing.
If they are higher than your CPU count, then you might have a performance problem (it depends).
最早的系统级平均负载(load average)只会统计runnable状态,但是linux后面觉得这种统计方式代表不了系统的真实负载。
举一个例子:系统换一个低速硬盘后,他的runnable负载还会小于高速硬盘时的值;linux认为睡眠状态(TASK_INTERRUPTIBLE/TASK_UNINTERRUPTIBLE)也是系统的一种负载,系统得不到服务是因为io/外设的负载过重。
系统级负载统计函数calc_global_load_tick()中会把(this_rq->nr_running+this_rq->nr_uninterruptible)都计入负载;
代码解析如下:
scheduler_tick()
-> calc_global_load_tick()
/* * Called from scheduler_tick() to periodically update this CPU's * active count. */ void calc_global_load_tick(struct rq *this_rq) { long delta; if (time_before(jiffies, this_rq->calc_load_update)) //过滤系统负载已经更新了的情况 return; delta = calc_load_fold_active(this_rq, 0); //(1)更新nr_running + uninterruptible的task数量 if (delta) atomic_long_add(delta, &calc_load_tasks); //统计task数量到系统全局变量calc_load_tasks中 this_rq->calc_load_update += LOAD_FREQ; //下一次更新系统负载的时间(当前时间+5s) }
//(1)更新nr_running + uninterruptible的task数量
long calc_load_fold_active(struct rq *this_rq, long adjust) { long nr_active, delta = 0; nr_active = this_rq->nr_running - adjust; nr_active += (long)this_rq->nr_uninterruptible; //统计所有nr_running和uninterruptible的task数量 if (nr_active != this_rq->calc_load_active) { //this_rq->calc_load_active表示当前nr_running + uninterruptible的task数量 delta = nr_active - this_rq->calc_load_active; //计算差值 this_rq->calc_load_active = nr_active; //更新task数量 } return delta; }
上面这部分主要是每隔5s以上,更新系统中nr_running + uninterruptible的task数量的,并统计到全局变量calc_load_tasks中。
此外,还有一部分是在系统jiffies更新时,触发计算系统负载动作。
系统中注册了tick_setup_sched_timer,用于模拟tick、高精度定时器、以及更新jiffies等,其中也会对系统负载进行计算。
当timer触发时,就会执行tick_sched_timer。当前cpu为tick_do_timer_cpu时,就会进行系统负载计算。所以,计算负载仅由一个从cpu完成,它就是tick_do_timer_cpu。
tick_sched_timer() -> tick_sched_do_timer() ->tick_do_update_jiffies64() -> do_timer() -> calc_global_load()
/* * calc_load - update the avenrun load estimates 10 ticks after the * CPUs have updated calc_load_tasks. * * Called from the global timer code. */ void calc_global_load(unsigned long ticks) { unsigned long sample_window; long active, delta; sample_window = READ_ONCE(calc_load_update); //获取scheduler_tick中更新的新时间戳 if (time_before(jiffies, sample_window + 10)) //确保在时间戳之后的10个tick后(确保所有cpu都更新完calc_load_tasks),进行系统负载计算(所以总的间隔时间时5s + 10 tick) return; /* * Fold the 'old' NO_HZ-delta to include all NO_HZ CPUs. */ delta = calc_load_nohz_fold(); //(2)统计NO_HZ cpu的task数量(是否因为idle而错过了统计task数量,所以在这里更新一下?) if (delta) atomic_long_add(delta, &calc_load_tasks); //更新nr_running + uninterrunptible的task数量全局变量 active = atomic_long_read(&calc_load_tasks); //获取nr_running + uninterrunptible的task数量全局变量 active = active > 0 ? active * FIXED_1 : 0; //乘FIXED_1系数 avenrun[0] = calc_load(avenrun[0], EXP_1, active); //(3)计算1分钟的系统负载 avenrun[1] = calc_load(avenrun[1], EXP_5, active); //计算5分钟的系统负载 avenrun[2] = calc_load(avenrun[2], EXP_15, active); //计算15分钟的系统负载 WRITE_ONCE(calc_load_update, sample_window + LOAD_FREQ); //更新时间戳 /* * In case we went to NO_HZ for multiple LOAD_FREQ intervals * catch up in bulk. */ calc_global_nohz(); //(4) }
(2)统计NO_HZ cpu的task数量(是否因为idle而错过了统计task数量,所以在这里更新一下?)
static long calc_load_nohz_fold(void) { int idx = calc_load_read_idx(); long delta = 0; if (atomic_long_read(&calc_load_nohz[idx])) delta = atomic_long_xchg(&calc_load_nohz[idx], 0); return delta; }
/* * Handle NO_HZ for the global load-average. * * Since the above described distributed algorithm to compute the global * load-average relies on per-CPU sampling from the tick, it is affected by * NO_HZ. * * The basic idea is to fold the nr_active delta into a global NO_HZ-delta upon * entering NO_HZ state such that we can include this as an 'extra' CPU delta * when we read the global state. * * Obviously reality has to ruin such a delightfully simple scheme: * * - When we go NO_HZ idle during the window, we can negate our sample * contribution, causing under-accounting. * * We avoid this by keeping two NO_HZ-delta counters and flipping them * when the window starts, thus separating old and new NO_HZ load. * * The only trick is the slight shift in index flip for read vs write. * * 0s 5s 10s 15s * +10 +10 +10 +10 * |-|-----------|-|-----------|-|-----------|-| * r:0 0 1 1 0 0 1 1 0 * w:0 1 1 0 0 1 1 0 0 * * This ensures we'll fold the old NO_HZ contribution in this window while * accumlating the new one. * * - When we wake up from NO_HZ during the window, we push up our * contribution, since we effectively move our sample point to a known * busy state. * * This is solved by pushing the window forward, and thus skipping the * sample, for this CPU (effectively using the NO_HZ-delta for this CPU which * was in effect at the time the window opened). This also solves the issue * of having to deal with a CPU having been in NO_HZ for multiple LOAD_FREQ * intervals. * * When making the ILB scale, we should try to pull this in as well. */ static atomic_long_t calc_load_nohz[2]; static int calc_load_idx;
(3)计算1分钟的系统负载。5分钟和15分钟的负载也是类似计算方法
/* * a1 = a0 * e + a * (1 - e) */ static inline unsigned long calc_load(unsigned long load, unsigned long exp, unsigned long active) { unsigned long newload; newload = load * exp + active * (FIXED_1 - exp); if (active >= load) newload += FIXED_1-1; return newload / FIXED_1; } #define FSHIFT 11 /* nr of bits of precision */ #define FIXED_1 (1<<FSHIFT) /* 1.0 as fixed-point */ #define LOAD_FREQ (5*HZ+1) /* 5 sec intervals */ #define EXP_1 1884 /* 1/exp(5sec/1min) as fixed-point */ #define EXP_5 2014 /* 1/exp(5sec/5min) */ #define EXP_15 2037 /* 1/exp(5sec/15min) */
核心算法calc_load()的思想是:old_load * 老化系数 + new_load *(1 - 老化系数)
1分钟负载计算公式:
old_load * (EXP_1/FIXED_1) + new_load * (1 - EXP_1/FIXED_1)
即:
其中,
FIXED_1 = 2^11 = 2048
EXP_1 = 1884
EXP_5 = 2014
EXP_15 = 2037
5分钟和15分钟的计算,只需要将公式中的EXP_1换成EXP_5/EXP_15即可。
从计算来看,系统负载本质实际就是统计nr_running + uninterruptible task数量。
(4)由于NO_HZ可能导致已经错过了多个tick,所以需要将错过的这些tick也考虑在内。根据实际错过的具体tick数,重新计算出准确的负载load
/* * NO_HZ can leave us missing all per-CPU ticks calling * calc_load_fold_active(), but since a NO_HZ CPU folds its delta into * calc_load_nohz per calc_load_nohz_start(), all we need to do is fold * in the pending NO_HZ delta if our NO_HZ period crossed a load cycle boundary. * * Once we've updated the global active value, we need to apply the exponential * weights adjusted to the number of cycles missed. */ static void calc_global_nohz(void) { unsigned long sample_window; long delta, active, n; sample_window = READ_ONCE(calc_load_update); if (!time_before(jiffies, sample_window + 10)) { /* * Catch-up, fold however many we are behind still */ delta = jiffies - sample_window - 10; n = 1 + (delta / LOAD_FREQ); active = atomic_long_read(&calc_load_tasks); active = active > 0 ? active * FIXED_1 : 0; avenrun[0] = calc_load_n(avenrun[0], EXP_1, active, n); avenrun[1] = calc_load_n(avenrun[1], EXP_5, active, n); avenrun[2] = calc_load_n(avenrun[2], EXP_15, active, n); WRITE_ONCE(calc_load_update, sample_window + n * LOAD_FREQ); } /* * Flip the NO_HZ index... * * Make sure we first write the new time then flip the index, so that * calc_load_write_idx() will see the new time when it reads the new * index, this avoids a double flip messing things up. */ smp_wmb(); calc_load_idx++;
最后通过cat proc/loadavg,可以查看系统负载结果:
代码如下:
static int loadavg_proc_show(struct seq_file *m, void *v) { unsigned long avnrun[3]; get_avenrun(avnrun, FIXED_1/200, 0); seq_printf(m, "%lu.%02lu %lu.%02lu %lu.%02lu %ld/%d %d\n", LOAD_INT(avnrun[0]), LOAD_FRAC(avnrun[0]), LOAD_INT(avnrun[1]), LOAD_FRAC(avnrun[1]), LOAD_INT(avnrun[2]), LOAD_FRAC(avnrun[2]), nr_running(), nr_threads, idr_get_cursor(&task_active_pid_ns(current)->idr) - 1); return 0; }
总结
1、cpu负载计算,在每个scheduler_tick中触发。
统计的数据是cpu rq的runnable_load_avg,使用的公式是:当前load = 更新系数 * 旧的load + (1-更新系数)* 新的load ---更新系数为 (2^idx - 1) / 2^idx,idx = 0,1,2,3,4。
同时会统计出5条不同周期的CPU负载均线,来用于不同场景下的cpu负载体现和比较基准,并能体现其变化趋势。
2、系统负载计算,是在scheduler_tick中统计runnable + uninterruptible的task数量(统计间隔5s)。在sched tick timer触发时(统计间隔5s + 10 tick),计算系统负载。
统计的数据是runnable + uninterruptible的task数量,使用的公式是:旧load * 老化系数+新load * (1 - 老化系数)。
同时会统计1分钟、5分钟、15分钟的系统负载数据。可以通过节点查看得知。
PS:乍一看2个负载的计算公式都差不多,其实统计的load天差地别,需要注意区别!
参考:https://blog.csdn.net/pwl999/article/details/78817902
https://blog.csdn.net/wukongmingjing/article/details/82531950