自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Linux 中的負載高低和 CPU 開銷并不完全對應(yīng)

系統(tǒng) Linux
負載是查看 Linux 服務(wù)器運行狀態(tài)時很常用的一個性能指標。在觀察線上服務(wù)器運行狀況的時候,我們也是經(jīng)常把負載找出來看一看。在線上請求壓力過大的時候,經(jīng)常是也伴隨著負載的飆高。

大家好,我是飛哥!

負載是查看 Linux 服務(wù)器運行狀態(tài)時很常用的一個性能指標。在觀察線上服務(wù)器運行狀況的時候,我們也是經(jīng)常把負載找出來看一看。在線上請求壓力過大的時候,經(jīng)常是也伴隨著負載的飆高。

但是負載的原理你真的理解了嗎?我來列舉幾個問題,看看你對負載的理解是否足夠的深刻。

  • 負載是如何計算出來的?
  • 負載高低和 CPU 消耗正相關(guān)嗎?
  • 內(nèi)核是如何暴露負載數(shù)據(jù)給應(yīng)用層的?

如果你對以上問題的理解還拿捏不是很準,那么飛哥今天就帶你來深入地了解一下 Linux 中的負載!

一、理解負載查看過程

我們經(jīng)常用 top 命令查看 Linux 系統(tǒng)的負載情況。一個典型的 top 命令輸出的負載如下所示。

# top
Load Avg: 1.25, 1.30, 1.95 .....
......

輸出中的 Load Avg 就是我們常說的負載,也叫系統(tǒng)平均負載。因為單純某一個瞬時的負載值并沒有太大意義。所以 Linux 是計算了過去一段時間內(nèi)的平均值,這三個數(shù)分別代表的是過去 1 分鐘、過去 5 分鐘和過去 15 分鐘的平均負載值。

那么 top 命令展示的數(shù)據(jù)數(shù)是如何來的呢?事實上,top 命令里的負載值是從 /proc/loadavg 這個偽文件里來的。通過 strace 命令跟蹤 top 命令的系統(tǒng)調(diào)用可以看的到這個過程。

# strace top
...
openat(AT_FDCWD, "/proc/loadavg", O_RDONLY) = 7

內(nèi)核中定義了 loadavg 這個偽文件的 open 函數(shù)。當用戶態(tài)訪問 /proc/loadavg 會觸發(fā)內(nèi)核定義的函數(shù),在這里會讀取內(nèi)核中的平均負載變量,簡單計算后便可展示出來。整體流程如下圖所示。

圖片

我們根據(jù)上述流程圖再展開了看下。偽文件 /proc/loadavg 在 kernel 中定義是在 /fs/proc/loadavg.c 中。在該文件中會創(chuàng)建 /proc/loadavg,并為其指定操作方法 loadavg_proc_fops。

//file: fs/proc/loadavg.c
static int __init proc_loadavg_init(void)
{
proc_create("loadavg", 0, NULL, &loadavg_proc_fops);
return 0;
}

在 loadavg_proc_fops 中包含了打開該文件時對應(yīng)的操作方法。

//file: fs/proc/loadavg.c
static const struct file_operations loadavg_proc_fops = {
.open = loadavg_proc_open,
......
};

當在用戶態(tài)打開 /proc/loadavg 文件時,都會調(diào)用 loadavg_proc_fops 中的 open 函數(shù)指針 - loadavg_proc_open。loadavg_proc_open 接下來會調(diào)用 loadavg_proc_show 進行處理,核心的計算是在這里完成的。

//file: fs/proc/loadavg.c
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,
task_active_pid_ns(current)->last_pid);
return 0;
}

在 loadavg_proc_show 函數(shù)中做了兩件事。

  • 調(diào)用 get_avenrun 讀取當前負載值
  • 將平均負載值按照一定的格式打印輸出

在上面的源碼中,大家看到了 FIXED_1/200、LOAD_INT、LOAD_FRAC 等奇奇怪怪的定義,代碼寫的這么猥瑣是因為內(nèi)核中并沒有 float、double 等浮點數(shù)類型,而是用整數(shù)來模擬的。這些代碼都是為了在整數(shù)和小數(shù)之間轉(zhuǎn)化使的。知道這個背景就行了,不用過度展開剖析。

這樣用戶通過訪問 /proc/loadavg 文件就可以讀取到內(nèi)核計算的負載數(shù)據(jù)了。其中獲取 get_avenrun 只是在訪問 avenrun 這個全局數(shù)組而已。

//file:kernel/sched/core.c
void get_avenrun(unsigned long *loads, unsigned long offset, int shift)
{
loads[0] = (avenrun[0] + offset) << shift;
loads[1] = (avenrun[1] + offset) << shift;
loads[2] = (avenrun[2] + offset) << shift;
}

現(xiàn)在可以總結(jié)一下我們開篇中的一個問題: 內(nèi)核是如何暴露負載數(shù)據(jù)給應(yīng)用層的?

內(nèi)核定義了一個偽文件 /proc/loadavg,每當用戶打開這個文件的時候,內(nèi)核中的 loadavg_proc_show 函數(shù)就會被調(diào)用到,接著訪問 avenrun 全局數(shù)組變量 并將平均負載從整數(shù)轉(zhuǎn)化為小數(shù),并打印出來。

好了,另外一個新問題又來了,avenrun 全局數(shù)組變量中存儲的數(shù)據(jù)是何時,又是被如何計算出來的呢?

二、內(nèi)核中負載的計算過程

接上小節(jié),我們繼續(xù)查看 avenrun 全局數(shù)組變量的數(shù)據(jù)來源。這個數(shù)組的計算過程分為如下兩步:

1.PerCPU 定期匯總瞬時負載:定時刷新每個 CPU 當前任務(wù)數(shù)到 calc_load_tasks,將每個 CPU 的負載數(shù)據(jù)匯總起來,得到系統(tǒng)當前的瞬時負載。
2.定時計算系統(tǒng)平均負載:定時器根據(jù)當前系統(tǒng)整體瞬時負載,使用指數(shù)加權(quán)移動平均法(一種高效計算平均數(shù)的算法)計算過去 1 分鐘、過去 5 分鐘、過去 15 分鐘的平均負載。

接下來我們分成兩個小節(jié)來分別介紹。

2.1 PerCPU 定期匯總負載

在 Linux 內(nèi)核中,有一個子系統(tǒng)叫做時間子系統(tǒng)。在時間子系統(tǒng)里,初始化了一個叫高分辨率的定時器。在該定時器中會定時將每個 CPU 上的負載數(shù)據(jù)(running 進程數(shù) + uninterruptible 進程數(shù))匯總到系統(tǒng)全局的瞬時負載變量 calc_load_tasks 中。整體流程如下圖所示。

圖片

我們把上述流程圖展開看一下,我們找到了高分辨率定時器的源碼如下:

//file:kernel/time/tick-sched.c
void tick_setup_sched_timer(void)
{
//初始化高分辨率定時器 sched_timer
hrtimer_init(&ts->sched_timer, CLOCK_MONOTONIC, HRTIMER_MODE_ABS);

//將定時器的到期函數(shù)設(shè)置成 tick_sched_timer
ts->sched_timer.function = tick_sched_timer;
...
}

在高分辨率初始化的時候,將到期函數(shù)設(shè)置成了 tick_sched_timer。通過這個函數(shù)讓每個 CPU 都會周期性地執(zhí)行一些任務(wù)。其中刷新當前系統(tǒng)負載就是在這個時機進行的。這里有一點要注意一個前提是每個 CPU 都有自己獨立的運行隊列,。

我們根據(jù) tick_sched_timer 的源碼進行追蹤,它依次通過調(diào)用 tick_sched_handle => update_process_times => scheduler_tick。最終在 scheduler_tick 中會刷新當前 CPU 上的負載值到 calc_load_tasks 上。因為每個 CPU 都在定時刷,所以 calc_load_tasks 上記錄的就是整個系統(tǒng)的瞬時負載值。

我們來看下負責(zé)刷新的 scheduler_tick 這個核心函數(shù):

//file:kernel/sched/core.c
void scheduler_tick(void)
{
int cpu = smp_processor_id();
struct rq *rq = cpu_rq(cpu);
update_cpu_load_active(rq);
...
}

在這個函數(shù)中,獲取當前 cpu 以及其對應(yīng)的運行隊列 rq(run queue),調(diào)用 update_cpu_load_active 刷新當前 CPU 的負載數(shù)據(jù)到全局數(shù)組中。

//file:kernel/sched/core.c
static void update_cpu_load_active(struct rq *this_rq)
{
...
calc_load_account_active(this_rq);
}

//file:kernel/sched/core.c
static void calc_load_account_active(struct rq *this_rq)
{
//獲取當前運行隊列的負載相對值
delta = calc_load_fold_active(this_rq);
if (delta)
//添加到全局瞬時負載值
atomic_long_add(delta, &calc_load_tasks);
...
}

在 calc_load_account_active 中看到,通過 calc_load_fold_active 獲取當前運行隊列的負載相對值,并把它加到全局瞬時負載值 calc_load_tasks 上。至此,calc_load_tasks 上就有了當前系統(tǒng)當前時間下的整體瞬時負載總數(shù)了。

我們再展開看看是如何根據(jù)運行隊列計算負載值的:

//file:kernel/sched/core.c
static long calc_load_fold_active(struct rq *this_rq)
{
long nr_active, delta = 0;

// R 和 D 狀態(tài)的用戶 task
nr_active = this_rq->nr_running;
nr_active += (long) this_rq->nr_uninterruptible;

// 只返回變化的量
if (nr_active != this_rq->calc_load_active) {
delta = nr_active - this_rq->calc_load_active;
this_rq->calc_load_active = nr_active;
}

return delta;
}

哦,原來是同時計算了 nr_running 和 nr_uninterruptible 兩種狀態(tài)的進程的數(shù)量。對應(yīng)于用戶空間中的 R 和 D 兩種狀態(tài)的 task 數(shù)(進程 OR 線程)。

由于 calc_load_tasks 是一個長期存在的數(shù)據(jù)。所以在刷新 rq 里的進程數(shù)到其上的時候,只需要刷變化的量就行,不用全部重算。因此上述函數(shù)返回的是一個 delta。

2.2 定時計算系統(tǒng)平均負載

上一小節(jié)中我們找到了系統(tǒng)當前瞬時負載 calc_load_tasks 變量的更新過程?,F(xiàn)在我們還缺一個計算過去 1 分鐘、過去 5 分鐘、過去 15 分鐘平均負載的機制。

傳統(tǒng)意義上,我們在計算平均數(shù)的時候采取的方法都是把過去一段時間的數(shù)字都加起來然后平均一下。把過去 N 個時間點的所有瞬時負載都加起來取一個平均數(shù)不完事了。這其實是我們傳統(tǒng)意義上理解的平均數(shù),假如有 n 個數(shù)字,分別是 x1, x2, ..., xn。那么這個數(shù)據(jù)集合的平均數(shù)就是 (x1 + x2 + ... + xn) / N。

但是如果用這種簡單的算法來計算平均負載的話,存在以下幾個問題:

1.需要存儲過去每一個采樣周期的數(shù)據(jù)
假設(shè)我們每 10 毫秒都采集一次,那么就需要使用一個比較大的數(shù)組將每一次采樣的數(shù)據(jù)全部都存起來,那么統(tǒng)計過去 15 分鐘的平均數(shù)就得存 1500 個數(shù)據(jù)(15 分鐘 * 每分鐘 100 次) 。而且每出現(xiàn)一個新的觀察值,就要從移動平均中減去一個最早的觀察值,再加上一個最新的觀察值,內(nèi)存數(shù)組會頻繁地修改和更新。

2.計算過程較為復(fù)雜
計算的時候再把整個數(shù)組全加起來,再除以樣本總數(shù)。雖然加法很簡單,但是成百上千個數(shù)字的累加仍然很是繁瑣。

3.不能準確表示當前變化趨勢傳統(tǒng)的平均數(shù)計算過程中,所有數(shù)字的權(quán)重是一樣的。但對于平均負載這種實時應(yīng)用來說,其實越靠近當前時刻的數(shù)值權(quán)重應(yīng)該越要大一些才好。因為這樣能更好反應(yīng)近期變化的趨勢。

所以,在 Linux 里使用的并不是我們所以為的傳統(tǒng)的平均數(shù)的計算方法,而是采用的一種指數(shù)加權(quán)移動平均(Exponential Weighted Moving Average,EMWA)的平均數(shù)計算法。

這種指數(shù)加權(quán)移動平均數(shù)計算法在深度學(xué)習(xí)中有很廣泛的應(yīng)用。另外股票市場里的 EMA 均線也是使用的是類似的方法求均值的方法。該算法的數(shù)學(xué)表達式是:a1 = a0 * factor + a * (1 - factor)。這個算法想理解起來有點小復(fù)雜,感興趣的同學(xué)可以 Google 自行搜索。

我們只需要知道這種方法在實際計算的時候只需要上一個時間的平均數(shù)即可,不需要保存所有瞬時負載值。另外就是越靠近現(xiàn)在的時間點權(quán)重越高,能夠很好地表示近期變化趨勢。

這其實也是在時間子系統(tǒng)中定時完成的,通過一種叫做指數(shù)加權(quán)移動平均計算的方法,計算這三個平均數(shù)。

圖片

我們來詳細看下上圖中的執(zhí)行過程。時間子系統(tǒng)將在時鐘中斷中會注冊時鐘中斷的處理函數(shù)為 timer_interrupt 。

//file:arch/ia64/kernel/time.c
void __init
time_init (void)
{
register_percpu_irq(IA64_TIMER_VECTOR, &timer_irqaction);
ia64_init_itm();
}

static struct irqaction timer_irqaction = {
.handler = timer_interrupt,
.flags = IRQF_DISABLED | IRQF_IRQPOLL,
.name = "timer"
};

當每次時鐘節(jié)拍到來時會調(diào)用到 timer_interrupt,依次會調(diào)用到 do_timer 函數(shù)。

//file:kernel/time/timekeeping.c
void do_timer(unsigned long ticks)
{
...
calc_global_load(ticks);
}

其中 calc_global_load 是平均負載計算的核心。它會獲取系統(tǒng)當前瞬時負載值 calc_load_tasks,然后來計算過去 1 分鐘、過去 5 分鐘、過去 15 分鐘的平均負載,并保存到 avenrun 中,供用戶進程讀取。

//file:kernel/sched/core.c
void calc_global_load(unsigned long ticks)
{
...
// 1.獲取當前瞬時負載值
active = atomic_long_read(&calc_load_tasks);

// 2.平均負載的計算
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);
...
}

獲取瞬時負載比較簡單,就是讀取一個內(nèi)存變量而已。在 calc_load 中就是采用了我們前面說的指數(shù)加權(quán)移動平均法來計算過去 1 分鐘、過去 5 分鐘、過去 15 分鐘的平均負載的。具體實現(xiàn)的代碼如下:

//file:kernel/sched/core.c
/*
* 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;
}

雖然這個算法理解起來挺復(fù)雜,但是代碼看起來確實要簡單不少,計算量看起來很少。而且看不懂也沒有關(guān)系,只需要知道內(nèi)核并不是采用的原始的平均數(shù)計算方法,而是采用了一種計算快,且能更好表達變化趨勢的算法就行。

至此,我們開篇提到的“負載是如何計算出來的?”這個問題也有結(jié)論了。

Linux 定時將每個 CPU 上的運行隊列中 running 和 uninterruptible 的狀態(tài)的進程數(shù)量匯總到一個全局系統(tǒng)瞬時負載值中,然后再定時使用指數(shù)加權(quán)移動平均法來統(tǒng)計過去 1 分鐘、過去 5 分鐘、過去 15 分鐘的平均負載。

三、平均負載和 CPU 消耗的關(guān)系

現(xiàn)在很多同學(xué)都將平均負載和 CPU 給聯(lián)系到了一起。認為負載高、CPU 消耗就會高,負載低,CPU 消耗就會低。

在很老的 Linux 的版本里,統(tǒng)計負載的時候確實是只計算了 runnable 的任務(wù)數(shù)量,這些進程只對 CPU 有需求。在那個年代里,負載和 CPU 消耗量確實是正相關(guān)的。負載越高就表示正在 CPU 上運行,或等待 CPU 執(zhí)行的進程越多,CPU 消耗量也會越高。

但是前面我們看到了,本文使用的 3.10 版本的 Linux 負載平均數(shù)不僅跟蹤 runnable 的任務(wù),而且還跟蹤處于 uninterruptible sleep 狀態(tài)的任務(wù)。而 uninterruptible 狀態(tài)的進程其實是不占 CPU 的。

圖片

?所以說,負載高并一定是 CPU 處理不過來,也有可能會是因為磁盤等其他資源調(diào)度不過來而使得進程進入 uninterruptible 狀態(tài)的進程導(dǎo)致的!

為什么要這么修改。我從網(wǎng)上搜到了遠在 1993 年的一封郵件里找到了原因,以下是郵件原文。

From: Matthias Urlichs <urlichs@smurf.sub.org>
Subject: Load average broken ?
Date: Fri, 29 Oct 1993 11:37:23 +0200


The kernel only counts "runnable" processes when computing the load average.
I don't like that; the problem is that processes which are swapping or
waiting on "fast", i.e. noninterruptible, I/O, also consume resources.

It seems somewhat nonintuitive that the load average goes down when you
replace your fast swap disk with a slow swap disk...

Anyway, the following patch seems to make the load average much more
consistent WRT the subjective speed of the system. And, most important, the
load is still zero when nobody is doing anything. ;-)


--- kernel/sched.c.orig Fri Oct 29 10:31:11 1993
+++ kernel/sched.c Fri Oct 29 10:32:51 1993
@@ -414,7 +414,9 @@
unsigned long nr = 0;

for(p = &LAST_TASK; p > &FIRST_TASK; --p)
- if (*p && (*p)->state == TASK_RUNNING)
+ if (*p && ((*p)->state == TASK_RUNNING) ||
+ (*p)->state == TASK_UNINTERRUPTIBLE) ||
+ (*p)->state == TASK_SWAPPING))
nr += FIXED_1;
return nr;
}

可見這個修改是在 1993 年就引入了。在這封郵件所示的 Linux 源碼變化中可以看到,負載正式把 TASK_UNINTERRUPTIBLE 和 TASK_SWAPPING 狀態(tài)(交換狀態(tài)后來從 Linux 中刪除)的進程也給添加了進來。在這封郵件中的正文中,作者也清楚地表達了為什么要把 TASK_UNINTERRUPTIBLE 狀態(tài)的進程添加進來的原因。我把他的說明翻譯一下,如下:

“內(nèi)核在計算平均負載時只計算“可運行”進程。我不喜歡那樣;問題是正在“快速”交換或等待的進程,即不可中斷的 I/O,也會消耗資源。當您用慢速交換磁盤替換快速交換磁盤時,平均負載下降似乎有點不直觀...... 無論如何,下面的補丁似乎使負載平均值更加一致 WRT 系統(tǒng)的主觀速度。而且,最重要的是,當沒有人做任何事情時,負載仍然為零。;-)”

這一補丁提交者的主要思想是平均負載應(yīng)該表現(xiàn)對系統(tǒng)所有資源的需求情況,而不應(yīng)該只表現(xiàn)對 CPU 資源的需求。

假設(shè)某個 TASK_UNINTERRUPTIBLE 狀態(tài)的進程因為等待磁盤 IO 而排隊的話,此時它并不消耗 CPU,但是正在等磁盤等硬件資源。那么它是應(yīng)該體現(xiàn)在平均負載的計算里的。所以作者把 TASK_UNINTERRUPTIBLE 狀態(tài)的進程都表現(xiàn)到平均負載里了。

所以,負載高低表明的是當前系統(tǒng)上對系統(tǒng)資源整體需求更情況。如果負載變高,可能是 CPU 資源不夠了,也可能是磁盤 IO 資源不夠了,所以還需要配合其它觀測命令具體分情況分析。

四、總結(jié)

今天我?guī)Т蠹疑钊氲貙W(xué)習(xí)了一下 Linux 中的負載。我們根據(jù)一幅圖來總結(jié)一下今天學(xué)到的內(nèi)容。

圖片

我把負載工作原理分成了如下三步。

  • 1.內(nèi)核定時匯總每 CPU 負載到系統(tǒng)瞬時負載
  • 2.內(nèi)核使用指數(shù)加權(quán)移動平均快速計算過去1、5、15分鐘的平均數(shù)
  • 3.用戶進程通過打開 loadavg 讀取內(nèi)核中的平均負載

我們再回頭來總結(jié)一下開篇提到的幾個問題。

1.負載是如何計算出來的?
是定時將每個 CPU 上的運行隊列中 running 和 uninterruptible 的狀態(tài)的進程數(shù)量匯總到一個全局系統(tǒng)瞬時負載值中,然后再定時使用指數(shù)加權(quán)移動平均法來統(tǒng)計過去 1 分鐘、過去 5 分鐘、過去 15 分鐘的平均負載。

2.負載高低和 CPU 消耗正相關(guān)嗎?
負載高低表明的是當前系統(tǒng)上對系統(tǒng)資源整體需求更情況。如果負載變高,可能是 CPU 資源不夠了,也可能是磁盤 IO 資源不夠了。所以不能說看著負載變高,就覺得是 CPU 資源不夠用了。

3.內(nèi)核是如何暴露負載數(shù)據(jù)給應(yīng)用層的?
內(nèi)核定義了一個偽文件 /proc/loadavg,每當用戶打開這個文件的時候,內(nèi)核中的 loadavg_proc_show 函數(shù)就會被調(diào)用到,該函數(shù)中訪問 avenrun 全局數(shù)組變量,并將平均負載從整數(shù)轉(zhuǎn)化為小數(shù),然后打印出來。


責(zé)任編輯:武曉燕 來源: 開發(fā)內(nèi)功修煉
相關(guān)推薦

2024-04-22 08:49:29

CIO人工智能云計算

2015-09-01 10:42:15

編程規(guī)范完全指南

2017-04-13 08:46:41

oracle數(shù)據(jù)挖掘

2015-10-10 10:02:44

程序員面試指南

2014-09-19 10:03:18

Chrome

2021-06-01 06:03:28

Css前端CSS 特效

2024-01-02 16:43:58

2011-06-29 10:03:33

Qt Configure

2024-01-12 10:16:53

2019-07-04 08:47:57

JavaScript瀏覽器技術(shù)

2021-05-06 07:26:55

CSS 文字動畫技巧

2021-02-09 00:46:45

區(qū)塊鏈加密貨幣工具

2021-08-18 10:28:09

MySQL SQL 語句數(shù)據(jù)庫

2017-04-19 20:10:20

SQLJOIN

2019-04-12 15:00:11

2019-08-18 22:45:22

編程Rust語言

2025-04-30 06:55:30

AI人工智能直播

2019-08-19 16:01:59

Rust編程語言JavaScript

2020-07-22 08:01:13

Nginx服務(wù)器網(wǎng)絡(luò)

2011-12-28 13:28:05

云計算服務(wù)器
點贊
收藏

51CTO技術(shù)棧公眾號