本文來(lái)自微信公眾:開(kāi)發(fā)內(nèi)功修煉 (ID:kfngxl),作者:張彥飛 allen大家好,我是飛哥!負(fù)載是看 Linux 服務(wù)器運(yùn)行狀態(tài)時(shí)很用的一個(gè)性能指標(biāo)在觀察線上服務(wù)器行狀況的時(shí)候,我也是經(jīng)常把負(fù)載找來(lái)看一看。在線上求壓力過(guò)大的時(shí)候經(jīng)常是也伴隨著負(fù)的飆高。但是負(fù)載原理你真的理解了?我來(lái)列舉幾個(gè)問(wèn),看看你對(duì)負(fù)載的解是否足夠的深刻負(fù)載是如何計(jì)算出的?負(fù)載高低和 CPU 消耗正相關(guān)嗎??jī)?nèi)核是如何暴露載數(shù)據(jù)給應(yīng)用層的如果你對(duì)以上問(wèn)題理解還拿捏不是很,那么飛哥今天就你來(lái)深入地了解一 Linux 中的負(fù)載!一、理解負(fù)查看過(guò)程我們經(jīng)常 top 命令查看 Linux 系統(tǒng)的負(fù)載情況。一個(gè)型的 top 命令輸出的負(fù)載如下所。#?topLoad?Avg:?1.25,?1.30,?1.95??...........輸出中的 Load Avg 就是我們常說(shuō)的負(fù)載,也叫統(tǒng)平均負(fù)載。因?yàn)?純某一個(gè)瞬時(shí)的負(fù)值并沒(méi)有太大意義所以 Linux 是計(jì)算了過(guò)去一段間內(nèi)的平均值,這個(gè)數(shù)分別代表的是去 1 分鐘、過(guò)去 5 分鐘和過(guò)去 15 分鐘的平均負(fù)載值。那么 top 命令展示的數(shù)據(jù)數(shù)是如何來(lái)的呢?事上,top 命令里的負(fù)載值是從 /proc/ loadavg 這個(gè)偽文件里來(lái)的。通過(guò) strace 命令跟蹤 top 命令的系統(tǒng)調(diào)用可以看的到個(gè)過(guò)程。#?strace?topopenat(AT_FDCWD,?"/proc/loadavg",?O_RDONLY)?=?7內(nèi)核中定義了 loadavg 這個(gè)偽文件的 open 函數(shù)。當(dāng)用戶(hù)態(tài)訪 /proc/ loadavg 會(huì)觸發(fā)內(nèi)核定義的函數(shù)在這里會(huì)讀取內(nèi)核的平均負(fù)載變量,單計(jì)算后便可展示來(lái)。整體流程如下所示。我們根據(jù)上流程圖再展開(kāi)了看。偽文件 /proc/ loadavg 在 kernel 中定義是在 /fs/ proc / loadavg.c 中。在該文件中會(huì)創(chuàng)建 /proc/ loadavg,并為其指定操作方法 loadavg_proc_fops。//file:?fs/proc/loadavg.cstatic?int?__init?proc_loadavg_init(void){?proc_create("loadavg",?0,?NULL,?&loadavg_proc_fops);?return?0;}在 loadavg_proc_fops 中包含了打開(kāi)該文件時(shí)對(duì)應(yīng)的操作方。//file:?fs/proc/loadavg.cstatic?const?struct?file_operations?loadavg_proc_fops?=?{?.open??=?loadavg_proc_open,?};當(dāng)在用戶(hù)態(tài)打開(kāi) /proc/ loadavg 文件時(shí),都會(huì)調(diào)用 loadavg_proc_fops 中的 open 函數(shù)指針 - loadavg_proc_open。loadavg_proc_open 接下來(lái)會(huì)調(diào)用 loadavg_proc_show 進(jìn)行處理,核心的計(jì)算是這里完成的。//file:?fs/proc/loadavg.cstatic?int?loadavg_proc_show(struct?seq_file?*m,?void?*v){?unsigned?long?avnrun[3];?//獲取平均負(fù)載值?get_avenrun(avnrun,?FIXED_1/200,?0);?//打印輸出平均負(fù)載?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 讀取當(dāng)前負(fù)載值將平負(fù)載值按照一定的式打印輸出在上面源碼中,大家看到 FIXED_1/200、LOAD_INT、LOAD_FRAC 等奇奇怪怪的定義,代碼寫(xiě)這么猥瑣是因?yàn)閮?nèi)中并沒(méi)有 float、double 等浮點(diǎn)數(shù)類(lèi)型,而用整數(shù)來(lái)模擬的。些代碼都是為了在數(shù)和小數(shù)之間轉(zhuǎn)化的。知道這個(gè)背景行了,不用過(guò)度展剖析。這樣用戶(hù)通訪問(wèn) /proc/ loadavg 文件就可以讀取到核計(jì)算的負(fù)載數(shù)據(jù)。其中獲取 get_avenrun 只是在訪問(wèn) avenrun 這個(gè)全局?jǐn)?shù)組而已。//file:kernel/sched/core.cvoid?get_avenrun(unsigned?long?*loads,?unsigned?long?offset,?int?shift){?loads[0]?=?(avenrun[0]?+?offset)? update_process_times => scheduler_tick。最終在 scheduler_tick 中會(huì)刷新當(dāng)前 CPU 上的負(fù)載值到 calc_load_tasks 上。因?yàn)槊總€(gè) CPU 都在定時(shí)刷,所以 calc_load_tasks 上記錄的就是整個(gè)系統(tǒng)的瞬時(shí)負(fù)值。我們來(lái)看下負(fù)刷新的 scheduler_tick 這個(gè)核心函數(shù)://file:kernel/sched/core.cvoid?scheduler_tick(void){?int?cpu?=?smp_processor_id();?struct?rq?*rq?=?cpu_rq(cpu);?update_cpu_load_active(rq);?}在這個(gè)函數(shù)中,獲取當(dāng)前 cpu 以及其對(duì)應(yīng)的運(yùn)行隊(duì)列 rq(run queue),調(diào)用 update_cpu_load_active 刷新當(dāng)前 CPU 的負(fù)載數(shù)據(jù)到全局組中。//file:kernel/sched/core.cstatic?void?update_cpu_load_active(struct?rq?*this_rq){??calc_load_account_active(this_rq);}//file:kernel/sched/core.cstatic?void?calc_load_account_active(struct?rq?*this_rq){?//獲取當(dāng)前運(yùn)行隊(duì)列負(fù)載相對(duì)值?delta??=?calc_load_fold_active(this_rq);?if?(delta)??//添加到全局瞬時(shí)負(fù)載??atomic_long_add(delta,?&calc_load_tasks);?}在 calc_load_account_active 中看到,通過(guò) calc_load_fold_active 獲取當(dāng)前運(yùn)行隊(duì)列的負(fù)載相對(duì)值,并它加到全局瞬時(shí)負(fù)值 calc_load_tasks 上。至此,calc_load_tasks 上就有了當(dāng)前系統(tǒng)當(dāng)前時(shí)間下的體瞬時(shí)負(fù)載總數(shù)了我們?cè)僬归_(kāi)看看是何根據(jù)運(yùn)行隊(duì)列計(jì)負(fù)載值的://file:kernel/sched/core.cstatic?long?calc_load_fold_active(struct?rq?*this_rq){?long?nr_active,?delta?=?0;?//?R?和?D?狀態(tài)的用戶(hù)?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;}哦,原來(lái)是同時(shí)計(jì)算了 nr_running 和 nr_uninterruptible 兩種狀態(tài)的進(jìn)程的量。對(duì)應(yīng)于用戶(hù)空中的 R 和 D 兩種狀態(tài)的 task 數(shù)(進(jìn)程 OR 線程)。由于 calc_load_tasks 是一個(gè)長(zhǎng)期存在的數(shù)據(jù)。以在刷新 rq 里的進(jìn)程數(shù)到其上的候,只需要刷變化量就行,不用全部算。因此上述函數(shù)回的是一個(gè) delta。2.2 定時(shí)計(jì)算系統(tǒng)平均負(fù)載一小節(jié)中我們找到系統(tǒng)當(dāng)前瞬時(shí)負(fù)載 calc_load_tasks 變量的更新過(guò)程?,F(xiàn)在們還缺一個(gè)計(jì)算過(guò) 1 分鐘、過(guò)去 5 分鐘、過(guò)去 15 分鐘平均負(fù)載的機(jī)制。傳統(tǒng)意義上我們?cè)谟?jì)算平均數(shù)時(shí)候采取的方法都把過(guò)去一段時(shí)間的字都加起來(lái)然后平一下。把過(guò)去 N 個(gè)時(shí)間點(diǎn)的所有瞬負(fù)載都加起來(lái)取一平均數(shù)不完事了。其實(shí)是我們傳統(tǒng)意上理解的平均數(shù),如有 n 個(gè)數(shù)字,分別是 x1, x2, ..., xn。那么這個(gè)數(shù)據(jù)集合的平均數(shù)就是 (x1 + x2 + ... + xn) / N。但是如果用這種簡(jiǎn)單的算來(lái)計(jì)算平均負(fù)載的,存在以下幾個(gè)問(wèn):1.需要存儲(chǔ)過(guò)去每一個(gè)采樣周期的據(jù)假設(shè)我們每 10 毫秒都采集一次,那么就需要使用一比較大的數(shù)組將每次采樣的數(shù)據(jù)全部存起來(lái),那么統(tǒng)計(jì)去 15 分鐘的平均數(shù)就得存 1500 個(gè)數(shù)據(jù) (15 分鐘 * 每分鐘 100 次) 。而且每出現(xiàn)一個(gè)新觀察值,就要從移平均中減去一個(gè)最的觀察值,再加上個(gè)最新的觀察值,存數(shù)組會(huì)頻繁地修和更新。2.計(jì)算過(guò)程較為復(fù)雜計(jì)算的候再把整個(gè)數(shù)組全起來(lái),再除以樣本數(shù)。雖然加法很簡(jiǎn),但是成百上千個(gè)字的累加仍然很是瑣。3.不能準(zhǔn)確表示當(dāng)前變化趨勢(shì)傳的平均數(shù)計(jì)算過(guò)程,所有數(shù)字的權(quán)重一樣的。但對(duì)于平負(fù)載這種實(shí)時(shí)應(yīng)用說(shuō),其實(shí)越靠近當(dāng)時(shí)刻的數(shù)值權(quán)重應(yīng)越要大一些才好。為這樣能更好反應(yīng)期變化的趨勢(shì)。所,在 Linux 里使用的并不是我所以為的傳統(tǒng)的平數(shù)的計(jì)算方法,而采用的一種指數(shù)加移動(dòng)平均(Exponential Weighted Moving Average,EMWA)的平均數(shù)計(jì)算法這種指數(shù)加權(quán)移動(dòng)均數(shù)計(jì)算法在深度習(xí)中有很廣泛的應(yīng)。另外股票市場(chǎng)里 EMA 均線也是使用的是類(lèi)似的方求均值的方法。該法的數(shù)學(xué)表達(dá)式是a1 = a0 * factor + a * (1 - factor)。這個(gè)算法想理解起有點(diǎn)小復(fù)雜,感興的同學(xué)可以 Google 自行搜索。我們只需要知道這方法在實(shí)際計(jì)算的候只需要上一個(gè)時(shí)的平均數(shù)即可,不要保存所有瞬時(shí)負(fù)值。另外就是越靠現(xiàn)在的時(shí)間點(diǎn)權(quán)重高,能夠很好地表近期變化趨勢(shì)。這實(shí)也是在時(shí)間子系中定時(shí)完成的,通一種叫做指數(shù)加權(quán)動(dòng)平均計(jì)算的方法計(jì)算這三個(gè)平均數(shù)我們來(lái)詳細(xì)看下上中的執(zhí)行過(guò)程。時(shí)子系統(tǒng)將在時(shí)鐘中中會(huì)注冊(cè)時(shí)鐘中斷處理函數(shù)為 timer_interrupt 。//file:arch/ia64/kernel/time.cvoid?__inittime_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"};當(dāng)每次時(shí)鐘節(jié)拍到來(lái)時(shí)調(diào)用到 timer_interrupt,依次會(huì)調(diào)用到 do_timer 函數(shù)。//file:kernel/time/timekeeping.cvoid?do_timer(unsigned?long?ticks){???calc_global_load(ticks);}其中 calc_global_load 是平均負(fù)載計(jì)算的核心。它會(huì)獲取系當(dāng)前瞬時(shí)負(fù)載值 calc_load_tasks,然后來(lái)計(jì)算過(guò)去 1 分鐘、過(guò)去 5 分鐘、過(guò)去 15 分鐘的平均負(fù)載,并保存 avenrun 中,供用戶(hù)進(jìn)程讀。//file:kernel/sched/core.cvoid?calc_global_load(unsigned?long?ticks){??//?1獲取當(dāng)前瞬時(shí)負(fù)載值?active?=?atomic_long_read(&calc_load_tasks);?//?2平均負(fù)載的計(jì)算?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);?}獲取瞬時(shí)負(fù)載比較簡(jiǎn)單,就是讀取玄鳥(niǎo)內(nèi)存變量而已。在 calc_load 中就是采用了我們前面說(shuō)的指數(shù)加權(quán)動(dòng)平均法來(lái)計(jì)算過(guò) 1 分鐘、過(guò)去 5 分鐘、過(guò)去 15 分鐘的平均負(fù)載的。具體實(shí)現(xiàn)的代如下://file:kernel/sched/core.c/*?*?a1?=?a0?*?e?+?a?*?(1?-?e)?*/static?unsigned?longcalc_load(unsigned?long?load,?unsigned?long?exp,?unsigned?long?active){?load?*=?exp;?load?+=?active?*?(FIXED_1?-?exp);?load?+=?1UL?<(FSHIFT?-?1);?return?load?>>?FSHIFT;}雖然這個(gè)算法理解起來(lái)挺復(fù)雜,但是碼看起來(lái)確實(shí)要簡(jiǎn)不少,計(jì)算量看起很少。而且看不懂沒(méi)有關(guān)系,只需要道內(nèi)核并不是采用原始的平均數(shù)計(jì)算法,而是采用了一計(jì)算快,且能更好達(dá)變化趨勢(shì)的算法行。至此,我們開(kāi)提到的“負(fù)載是如計(jì)算出來(lái)的?”這個(gè)問(wèn)題也有結(jié)論了。Linux 定時(shí)將每個(gè) CPU 上的運(yùn)行隊(duì)列中 running 和 uninterruptible 的狀態(tài)的進(jìn)程數(shù)量匯總到一個(gè)局系統(tǒng)瞬時(shí)負(fù)載值,然后再定時(shí)使用數(shù)加權(quán)移動(dòng)平均法統(tǒng)計(jì)過(guò)去 1 分鐘、過(guò)去 5 分鐘、過(guò)去 15 分鐘的平均負(fù)載。三、平負(fù)載和 CPU 消耗的關(guān)系現(xiàn)在很多學(xué)都將平均負(fù)載和 CPU 給聯(lián)系到了一起。認(rèn)為負(fù)載高CPU 消耗就會(huì)高,負(fù)載低,CPU 消耗就會(huì)低。在很的 Linux 的版本里,統(tǒng)計(jì)負(fù)載時(shí)候確實(shí)是只計(jì)算 runnable 的任務(wù)數(shù)量,這些進(jìn)程只對(duì) CPU 有需求。在那個(gè)年里,負(fù)載和 CPU 消耗量確實(shí)是正相關(guān)的。負(fù)載越高葴山示正在 CPU 上運(yùn)行,或等待 CPU 執(zhí)行的進(jìn)程越多,CPU 消耗量也會(huì)越高。但是前面們看到了,本文使的 3.10 版本的 Linux 負(fù)載平均數(shù)不僅跟蹤 runnable 的任務(wù),而且還跟處于 uninterruptible sleep 狀態(tài)的任務(wù)。而 uninterruptible 狀態(tài)的進(jìn)程其實(shí)是不占 CPU 的。所以說(shuō),負(fù)載高并一定是 CPU 處理不過(guò)來(lái),也有可能會(huì)是因?yàn)榇疟P(pán)其他資源調(diào)度不過(guò)而使得進(jìn)程進(jìn)入 uninterruptible 狀態(tài)的進(jìn)程導(dǎo)致的!為什要這么修改。我從上搜到了遠(yuǎn)在 1993 年的一封郵件里找到了原因,以是郵件原文。From:?Matthias?Urlichs?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?swing?orwaiting?on?"fast",?i.e.?noninterruptible,?I/O,?also?consume?resources.?It?seems?somewhat?nonintuitive?that?the?load?average?goes?down?when?youreplace?your?fast?swap?disk?with?a?slow?swap?disk...?Anyway,?the?following?patch?seems?to?make?the?load?average?much?moreconsistent?WRT?the?subjective?speed?of?the?system.?And,?most?important,?theload?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_SWING))????????????nr?+=?FIXED_1;????return?nr;?}可見(jiàn)這個(gè)修改是在 1993 年就引入了。在這封郵件所示 Linux 源碼變化中可以看到,載正式把 TASK_UNINTERRUPTIBLE 和 TASK_SWAPPING 狀態(tài)(交換狀態(tài)后來(lái)從 Linux 中刪除)的進(jìn)程也給添加了來(lái)。在這封郵件中正文中,作者也清地表達(dá)了為什么要 TASK_UNINTERRUPTIBLE 狀態(tài)的進(jìn)程添加進(jìn)來(lái)的原因。把他的說(shuō)明翻譯一,如下:“內(nèi)核在算平均負(fù)載時(shí)只計(jì)“可運(yùn)行”進(jìn)程。不喜歡那樣;問(wèn)題正在“快速”交換等待的進(jìn)程,即不中斷的 I / O,也會(huì)消耗資源。您用慢速交換磁盤(pán)換快速交換磁盤(pán)時(shí)平均負(fù)載下降似乎點(diǎn)不直觀...... 無(wú)論如何,下面的補(bǔ)丁似乎使負(fù)載均值更加一致 WRT 系統(tǒng)的主觀速度。而且,最重要的,當(dāng)沒(méi)有人做任何情時(shí),負(fù)載仍然為。;-)”這一補(bǔ)丁提交者的主要思想平均負(fù)載應(yīng)該表現(xiàn)系統(tǒng)所有資源的需情況,而不應(yīng)該只現(xiàn)對(duì) CPU 資源的需求。假設(shè)某個(gè) TASK_UNINTERRUPTIBLE 狀態(tài)的進(jìn)程因?yàn)榈却疟P(pán) IO 而排隊(duì)的話,此時(shí)并不消耗 CPU,但是正在等磁盤(pán)等件資源。那么它是該體現(xiàn)在平均負(fù)載計(jì)算里的。所以作把 TASK_UNINTERRUPTIBLE 狀態(tài)的進(jìn)程都表現(xiàn)到平均負(fù)里了。所以,負(fù)載低表明的是當(dāng)前系上對(duì)系統(tǒng)資源整體求更情況。如果負(fù)變高,可能是 CPU 資源不夠了,也可能是磁盤(pán) IO 資源不夠了,所以需要配合其它觀測(cè)令具體分情況分析四、總結(jié)今天我?guī)?家深入地學(xué)習(xí)了一 Linux 中的負(fù)載。我們根據(jù)一圖來(lái)總結(jié)一下今天到的內(nèi)容。我把負(fù)工作原理分成了如三步。1.內(nèi)核定時(shí)匯總每 CPU 負(fù)載到系統(tǒng)瞬時(shí)負(fù)載2.內(nèi)核使用指數(shù)加權(quán)移動(dòng)平均快速歸山算去 1、5、15 分鐘的平均數(shù)3.用戶(hù)進(jìn)程通過(guò)打開(kāi) loadavg 讀取內(nèi)核中的平均負(fù)載們?cè)倩仡^來(lái)總結(jié)一開(kāi)篇提到的幾個(gè)問(wèn)。1.負(fù)載是如何計(jì)算出來(lái)的?是定時(shí)將每個(gè) CPU 上的運(yùn)行隊(duì)列中 running 和 uninterruptible 的狀態(tài)的進(jìn)程數(shù)量匯總到一全局系統(tǒng)瞬時(shí)負(fù)載中,然后再定時(shí)使指數(shù)加權(quán)移動(dòng)平均來(lái)統(tǒng)計(jì)過(guò)去 1 分鐘、過(guò)去 5 分鐘、過(guò)去 15 分鐘的平均負(fù)載。2.負(fù)載高低和 CPU 消耗正相關(guān)嗎?負(fù)高低表明的是當(dāng)前統(tǒng)上對(duì)系統(tǒng)資源整需求更情況。如果載變高,可能是 CPU 資源不夠了,也可能是磁盤(pán) IO 資源不夠了。所以不能說(shuō)看著負(fù)載變,就覺(jué)得是 CPU 資源不夠用了。3.內(nèi)核是如何暴露負(fù)載數(shù)據(jù)給應(yīng)用鸞鳥(niǎo)的內(nèi)核定義了一個(gè)偽件 /proc/ loadavg,每當(dāng)用戶(hù)打開(kāi)這個(gè)文的時(shí)候,內(nèi)核中的 loadavg_proc_show 函數(shù)就會(huì)被調(diào)用到該函數(shù)中訪問(wèn) avenrun 全局?jǐn)?shù)組變量,并將平均載從整數(shù)轉(zhuǎn)化為小,然后打印出來(lái)?