一文讀懂 Linux 系統平均負載

我們經常會使用 top 命令來查看系統的性能情況,在 top 命令的第一行可以看到 load average 這個數據,如下圖所示:

load average 包含 3 列,分別表示 1 分鐘、5 分鐘和 15 分鐘的 系統平均負載

對於系統平均負載這個數值,可能很多同學並不完全理解其意義,並不知道數值達到多少時才表示系統負載過高。本文將會以簡單的語言來介紹系統平均負載這個概念,並且會介紹 Linux 內核是怎麼計算這個數值。

系統平均負載

《Understanding Linux CPU Load(鏈接在文章最後)》這篇文章已經非常通俗的解釋了什麼是 系統平均負載,這裏借用一下此文中的例子。

如果將 CPU 比作是橋樑,對於單核的 CPU 就好比是單車道的橋樑。每次橋樑只能讓一輛汽車通過,並且要以規定的速度通過。那麼:

系統的平均負載與上面的例子一樣,在單核 CPU 的環境下:

對於單核 CPU 來說,平均負載 1.0 表示使用率最高。但對於多核 CPU 來說,平均負載要乘以核心數。比如在 4 核 CPU 的系統中,當平均負載爲 4.0 時,才表示 CPU 的使用率最高。

Linux 平均負載計算原理

在介紹系統平均負載的計算原理前,先要介紹一下什麼是系統負載。在 Linux 系統中,系統負載表示 系統中當前正在運行的進程數量,其包括 可運行狀態 的進程數和 不可中斷休眠狀態 的進程數的和。注意:不可中斷休眠狀態的進程一般是在等待 I/O 完成的進程。

系統負載 = 可運行狀態進程數 + 不可中斷休眠狀態進程數

知道了什麼是 系統負載,那麼 系統平均負載 就容易理解了。比如每 5 秒統計一次系統負載,1 分鐘內會統計 12 次。如下所示:

第5秒  -> 系統負載
第10秒 -> 系統負載
第15秒 -> 系統負載
...
第60秒 -> 系統負載

然後把每次統計到的系統負載加起來,再除以統計次數,即可得出 系統平均負載。如下圖所示:

但這種計算方式有些缺陷,就是預測系統負載的準確性不夠高,因爲越老的數據越不能反映現在的情況。打個比方,要預測某條公路今天的車流量,使用昨天的數據作爲預測依據,會比使用一個月之前的數據作爲依據要準確得多。

所以,時間越近的數據,對未來的預測準確性越高。

Linux 內核使用一種名爲 指數平滑法 的算法來解決這個問題,指數平滑法的核心思想是對新老數據進行加權,越老的數據權重越低。

指數平滑法:是由 Robert G..Brown 提出的一種加權移動平均法,有興趣瞭解其數學原理的可以搜索相關資料,本文不作詳細介紹。

其計算公式如下(來源於 Linux 內核代碼 kernel/sched/core.c):

load1 = load0 * e + active * (1 - e)

解釋一下上面公式的意思:

所以,我們就可以使用上面的公式來預測任何時間的系統平均負載了。比如,我們要預測時間點 n 的系統平均負載,那麼可以這樣來計算:

load1 = load0 * e + active * (1 - e)
load2 = load1 * e + active * (1 - e)
load3 = load2 * e + active * (1 - e)
...
loadn = loadn-1 * e + active * (1 - e)

現在就只剩下 衰減係數 該如何計算了。

從 Linux 內核的註釋可以瞭解到,計算 1 分鐘內系統平均負載的 衰減係數 的計算方式如下:

1 / exp(5sec / 1min)

其中:

也就是說,要計算一分鐘的系統平均負載時,需要使用上面的 衰減係數。對於 5 分鐘和 15 分鐘的 衰減係數 的計算方式分別爲:

1 / exp(5sec / 5min)
1 / exp(5sec / 15min)

Linux 內核已經把 1 分鐘、5 分鐘和 15 分鐘的 衰減係數 結果計算出來,並且定義在 include/linux/sched.h 文件中,如下所示:

#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) */

通過上述公式計算出來的 衰減係數 是個浮點數,而在內核中是不能進行浮點數運行的。解決方法是先對 衰減係數 進行擴大,然後在展示時最縮小。所以,上面的 衰減係數 數值是經過擴大 2048 倍後的結果。

Linux 平均負載計算實現

萬事俱備,只欠東風。上面我們已經把所有的知識點介紹了,現在來分析一下 Linux 內核代碼是怎樣實現的。

1. 數據存儲

在 Linux 內核中,使用了 avenrun 數組來存儲 1 分鐘、5 分鐘和 15 分鐘的系統平均負載,如下代碼所示:

unsigned long avenrun[3];

如元素 avenrun[0] 用於存儲 1 分鐘內的系統平均負載,而元素 avenrun[1] 用於存儲 5 分鐘的系統平均負載,如此類推。

2. 統計過程

由於統計需要定時進行,所以內核把統計過程放置到 時鐘中斷 中進行。當 時鐘中斷 觸發時,將會調用 do_timer() 函數,而 do_timer() 函數將會調用 calc_global_load() 來統計系統平均負載。

我們來看看 calc_global_load() 函數的實現:

void calc_global_load(unsigned long ticks)
{
    long active, delta;

    // 1. 如果還沒到統計的時間間隔,那麼將不進行統計(5秒統計一次)
    if (time_before(jiffies, calc_load_update + 10))
        return;

    // 2. 獲取活躍進程數
    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;

    // 3. 統計各個時間段系統平均負載
    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);

    // 4. 更新下次統計的時間(增加5秒)
    calc_load_update += LOAD_FREQ;

    ...
}

calc_global_load() 函數主要完成 4 件事情:

  1. 判斷當前時間是否需要進行統計,如果還沒到統計的時間間隔,那麼將不進行統計(5 秒統計一次)。

  2. 獲取活躍進程數(可運行狀態進程數 + 不可中斷休眠狀態進程數)。

  3. 統計各個時間段系統平均負載(1 分鐘、5 分鐘和 15 分鐘)。

  4. 更新下次統計的時間(增加 5 秒)。

從上面的分析可知,calc_global_load() 函數將會調用 calc_load() 來計算系統平均負載。其代碼如下:

/*
 * a1 = a0 * e + a * (1 - e)
 */
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;
}

calc_load() 函數的各個參數意義如下:

可以看出,calc_load() 函數的實現就是按照 指數平滑法 來計算的。

參考文獻:

  • 《Understanding Linux CPU Load》

    https://scoutapm.com/blog/unders> tanding-load-averages

  • 《Linux 系統平均負載是如何計算的》

    https://blog.csdn.net/rikeyone/article/details/108309665

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/aVCQvxAA8itQ3PheEUFBcg