Linux系统平均负载是如何计算的?

关于负载的计算,它的结果是包含有小数的一个浮点数,内核中是不能使用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();
}

其中的:

  1. calc_load_tasks就是上面介绍时所说的runnable进程数量和uninterruptable进程数量之和。因为是SMP系统可能涉及到同步问题,因此采用atomic原子变量来保存。
  2. calc_load_update为下次采样时间,每次都需要加5*HZ,因此系统每5秒进行一次更新计算
  3. avenrun数组中保存的是1min,5min,15min时间所计算的平均值,实际上就是通过调整衰减因子来达到目的的,衰减因子越小,那么衰减越快。

在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)

你可能感兴趣的:(内核故障调试特性)