关于负载的计算,它的结果是包含有小数的一个浮点数,内核中是不能使用float变量的,那么这里就采用了一个整型变量的低11位来表示小数部分。那么对于数值1来说,它就是FIXED_1,也就是需要对1进行左移11bit。实际上此时这个整型变量保存的值是1024。
cat /proc/loadavg
0.43 0.58 0.65 5/7010 45102
我们通过cat命令查看负载值如上所示,它显示的是带有两个小数表示的一个浮点数,所以最后在输出这个数值时还需要做一个转换,如果从1024个值中得出这100小数部分,实际上也很简单,小学生都会计算,公式如下:
小数部分 = 低11位的值 / 1024 * 100
内核中为了实现这个功能定义了一些宏如下所示:
#define FSHIFT 11 /* nr of bits of precision */
#define FIXED_1 (1<
前面介绍了单位换算的问题,后面就开始真正的主题,对于平均负载,它是如何计算的呢?
首先要先搞清楚这个概念意味着什么,实际上系统负载这个指标表示的是系统中当前正在运行的进程数量,它等于running状态的进程数 + uninterrupt状态的进程数:
load = runing tasks num + uninterrupt tasks num
那么问题来了,这个值一直都是动态变化的每秒钟都不一样,如果我们仅仅是要求平均值,那么能够想到的比较容易算的方式,假如以5秒为采样单位:
…
第5秒 active nr
第10秒 active nr
…
然后每次计算都累加在一起,最后除以采样次数,这样就获取到了一个平均负载值。
这样计算有一个缺点,就是我们获取到的负载值实际上并不能反应当下系统中的负载情况,因为它计算了从系统启动开始以来的平均值,无法反应当下系统的运行情况,因此系统中实际并不是这样计算的,会求最近1min,5min和15min之内的平均值,那么计算方法是怎样的呢?
对于平均算法来说有很多种实现,比如:
(1)可以使用所有数据相加后处于数据个数,缺点是实时性不够好;
(2)也可以去除过时数据,只保存最近的多个数据做加权平均。
前面已经介绍了第一种方式的实现缺点,那么根据平均负载的需求来看,应该要使用第2种方法才行,每次计算时需要丢弃掉1min、5min、和15min之前的数据,记录最近的数据来计算平均值,但是这种算法依然不够好,它维护的数据太多了。
因此内核采用了另外一种维护数据量更少的算法,一次指数平滑法,感兴趣的可以去网上搜索了解更多的信息。
内核在实现时引入了一个衰减系数(小于1的值),利用这个衰减系数,来达到丢弃旧数据的目的。只需要知道衰减因子、上一次计算的平均值、本次采样的值,这三个就可以计算出最新的平均值了。主要原理就是使用一个小数作为衰减系数e,从开始计算的时刻a0开始,不做衰减,那么存在如下公式:
a1 = a0 * e + a * (1 - e)
a2 = a1 * e + a * (1 - e)
a3 = a2 * e + a * (1 - e)
an = an-1 * e + a * (1 - e)
我们来看如何做到的,举个例子,如果衰减系数为0.3,那么每次在计算平均负载时,都会对旧数据乘以衰减系数,也就是上一时刻的数据占比30%,当前数据占比70%,这样就相当于是更能反映当下的系统运行情况了,每次计算周期都进行这个衰减计算,可以想象的到,距离当前2个周期的数据衰减了两次,相当于乘以30%的2次方,反复如此计算下去,那么很久远的采样数据就在当前的计算结果中无限趋近于0了。
内核中的代码实现在:
void calc_global_load(unsigned long ticks)
{
long active, delta;
if (time_before(jiffies, calc_load_update + 10))
return;
/*
* Fold the 'old' idle-delta to include all NO_HZ cpus.
*/
delta = calc_load_fold_idle();
if (delta)
atomic_long_add(delta, &calc_load_tasks);
active = atomic_long_read(&calc_load_tasks);
active = active > 0 ? active * FIXED_1 : 0;
avenrun[0] = calc_load(avenrun[0], EXP_1, active);
avenrun[1] = calc_load(avenrun[1], EXP_5, active);
avenrun[2] = calc_load(avenrun[2], EXP_15, active);
calc_load_update += LOAD_FREQ;
/*
* In case we idled for multiple LOAD_FREQ intervals, catch up in bulk.
*/
calc_global_nohz();
}
其中的:
在calc_load中进行一次指数平滑算法的计算:
static unsigned long
calc_load(unsigned long load, unsigned long exp, unsigned long active)
{
load *= exp;
load += active * (FIXED_1 - exp);
load += 1UL << (FSHIFT - 1);
return load >> FSHIFT;
}
而更新平均负载是在一个系统周期timer中实现的:
void do_timer(unsigned long ticks)
1576 {
1577 jiffies_64 += ticks;
1578 update_wall_time();
1579 calc_global_load(ticks);
1580 }
计算 calc_load_tasks
的地方:
calc_load_fold_idle() //CPU进入nohz之前会把数据进行保存
calc_load_migrate()-->calc_load_fold_active() //CPU hotplug时,如果下线CPU需要做记录保存,加入最后更新时计算
calc_load_account_active()-->calc_load_fold_active() //CPU没有idle,那么会执行定期更新,在每个CPU都会执行这个计算
在计算负载时:
1.每个CPU都需要定时更新 calc_load_tasks的数值,该值记录的是所有CPU上可运行和uninterruptable数量的总和(calc_load_account_active)
2.处理idle的情况,进入idle前需要保存(calc_load_fold_idle)
3.处理CPUhotplug情况,下线前需要保存值(calc_load_migrate)
最后根据calc_load_tasks执行一次global平均值计算:
1.timer中触发5HZ周期的平均值计算(calc_global_load)