在線上服務(wù)器觀察線上服務(wù)運(yùn)行狀態(tài)的時(shí)候,絕大多數(shù)人都是喜歡先用 top 命令看看當(dāng)前系統(tǒng)的整體 cpu 利用率。例如,隨手拿來(lái)的一臺(tái)機(jī)器,top 命令顯示的利用率信息如下:
這個(gè)輸出結(jié)果說(shuō)簡(jiǎn)單也簡(jiǎn)單,說(shuō)復(fù)雜也不是那么容易就能全部搞明白的。例如:
問(wèn)題 1:top 輸出的利用率信息是如何計(jì)算出來(lái)的,它精確嗎?
問(wèn)題 2:ni 這一列是 nice,它輸出的是 cpu 在處理啥時(shí)的開(kāi)銷?
問(wèn)題 3:wa 代表的是 io wait,那么這段時(shí)間中 cpu 到底是忙碌還是空閑?
今天我們對(duì) cpu 利用率統(tǒng)計(jì)進(jìn)行深入的學(xué)習(xí)。通過(guò)今天的學(xué)習(xí),你不但能了解 cpu 利用率統(tǒng)計(jì)實(shí)現(xiàn)細(xì)節(jié),還能對(duì) nice、io wait 等指標(biāo)有更深入的理解。
今天我們先從自己的思考開(kāi)始!
一、先思考一下
拋開(kāi) Linux 的實(shí)現(xiàn)先不談,如果有如下需求,有一個(gè)四核服務(wù)器,上面跑了四個(gè)進(jìn)程。
讓你來(lái)設(shè)計(jì)計(jì)算整個(gè)系統(tǒng) cpu 利用率的這個(gè)需求,支持像 top 命令這樣的輸出,滿足以下要求:
cpu 使用率要盡可能地準(zhǔn)確;
要盡可能地體現(xiàn)秒級(jí)瞬時(shí) cpu 狀態(tài)。
可以先停下來(lái)思考幾分鐘。
好,思考結(jié)束。經(jīng)過(guò)思考你會(huì)發(fā)現(xiàn),這個(gè)看起來(lái)很簡(jiǎn)單的需求,實(shí)際還是有點(diǎn)小復(fù)雜的。
其中一個(gè)思路是把所有進(jìn)程的執(zhí)行時(shí)間都加起來(lái),然后再除以系統(tǒng)執(zhí)行總時(shí)間*4。
這個(gè)思路是沒(méi)問(wèn)題的,用這種方法統(tǒng)計(jì)很長(zhǎng)一段時(shí)間內(nèi)的 cpu 利用率是可以的,統(tǒng)計(jì)也足夠的準(zhǔn)確。
但只要用過(guò) top 你就知道 top 輸出的 cpu 利用率并不是長(zhǎng)時(shí)間不變的,而是默認(rèn) 3 秒為單位會(huì)動(dòng)態(tài)更新一下(這個(gè)時(shí)間間隔可以使用 -d 設(shè)置)。我們的這個(gè)方案體現(xiàn)總利用率可以,體現(xiàn)這種瞬時(shí)的狀態(tài)就難辦了。你可能會(huì)想到那我也 3 秒算一次不就行了?但這個(gè) 3 秒的時(shí)間從哪個(gè)點(diǎn)開(kāi)始呢。粒度很不好控制。
上一個(gè)思路問(wèn)題核心就是如何解決瞬時(shí)問(wèn)題。提到瞬時(shí)狀態(tài),你可能就又來(lái)思路了。那我就用瞬時(shí)采樣去看,看看當(dāng)前有幾個(gè)核在忙。四個(gè)核中如果有兩個(gè)核在忙,那利用率就是 50%。
這個(gè)思路思考的方向也是正確的,但是問(wèn)題有兩個(gè):
你算出的數(shù)字都是 25% 的整數(shù)倍;
這個(gè)瞬時(shí)值會(huì)導(dǎo)致 cpu 使用率顯示的劇烈震蕩。
比如下圖:
在 t1 的瞬時(shí)狀態(tài)看來(lái),系統(tǒng)的 cpu 利用率毫無(wú)疑問(wèn)就是 100%,但在 t2 時(shí)間看來(lái),使用率又變成 0% 了。思路方向是對(duì)的,但顯然這種粗暴的計(jì)算無(wú)法像 top 命令一樣優(yōu)雅地工作。
我們?cè)俑倪M(jìn)一下它,把上面兩個(gè)思路結(jié)合起來(lái),可能就能解決我們的問(wèn)題了。在采樣上,我們把周期定得細(xì)一些,但在計(jì)算上我們把周期定得粗一些。
我們引入采用周期的概念,定時(shí)比如每 1 毫秒采樣一次。如果采樣的瞬時(shí),cpu 在運(yùn)行,就將這 1 ms 記錄為使用。這時(shí)會(huì)得出一個(gè)瞬時(shí)的 cpu 使用率,把它都存起來(lái)。
在統(tǒng)計(jì) 3 秒內(nèi)的 cpu 使用率的時(shí)候,比如上圖中的 t1 和 t2 這段時(shí)間范圍。那就把這段時(shí)間內(nèi)的所有瞬時(shí)值全加一下,取個(gè)平均值。這樣就能解決上面的問(wèn)題了,統(tǒng)計(jì)相對(duì)準(zhǔn)確,避免了瞬時(shí)值劇烈震蕩且粒度過(guò)粗(只能以 25% 為單位變化)的問(wèn)題了。
可能有同學(xué)會(huì)問(wèn)了,假如 cpu 在兩次采樣中間發(fā)生變化了呢,如下圖這種情況。
在當(dāng)前采樣點(diǎn)到來(lái)的時(shí)候,進(jìn)程 A 其實(shí)剛執(zhí)行完,有一點(diǎn)點(diǎn)時(shí)間既沒(méi)被上一個(gè)采樣點(diǎn)統(tǒng)計(jì)到,本次也統(tǒng)計(jì)不到。對(duì)于進(jìn)程 B,其實(shí)只開(kāi)始了一小段時(shí)間,把 1 ms 全記上似乎有點(diǎn)多記了。
確實(shí)會(huì)存在這個(gè)問(wèn)題,但因?yàn)槲覀兊牟蓸邮?1 ms 一次,而我們實(shí)際查看使用的時(shí)候最少也是秒級(jí)別地用,會(huì)包括有成千上萬(wàn)個(gè)采樣點(diǎn)的信息,所以這種誤差并不會(huì)影響我們對(duì)全局的把握。
事實(shí)上,Linux 也就是這樣來(lái)統(tǒng)計(jì)系統(tǒng) cpu 利用率的。雖然可能會(huì)有誤差,但作為一項(xiàng)統(tǒng)計(jì)數(shù)據(jù)使用已經(jīng)是足夠了的。在實(shí)現(xiàn)上,Linux 是將所有的瞬時(shí)值都累加到某一個(gè)數(shù)據(jù)上的,而不是真的存了很多份的瞬時(shí)數(shù)據(jù)。
接下來(lái)就讓我們進(jìn)入 Linux 來(lái)查看它對(duì)系統(tǒng) cpu 利用率統(tǒng)計(jì)的具體實(shí)現(xiàn)。
二、top 命令使用數(shù)據(jù)在哪兒
上一節(jié)我們說(shuō)的 Linux 在實(shí)現(xiàn)上是將瞬時(shí)值都累加到某一個(gè)數(shù)據(jù)上的,這個(gè)值是內(nèi)核通過(guò) /proc/stat 偽文件來(lái)對(duì)用戶態(tài)暴露。Linux 在計(jì)算系統(tǒng) cpu 利用率的時(shí)候用的就是它。
整體上看,top 命令工作的內(nèi)部細(xì)節(jié)如下圖所示。
top 命令訪問(wèn) /proc/stat 獲取各項(xiàng) cpu 利用率使用值;
內(nèi)核調(diào)用 stat_open 函數(shù)來(lái)處理對(duì) /proc/stat 的訪問(wèn);
內(nèi)核訪問(wèn)的數(shù)據(jù)來(lái)源于 kernel_cpustat 數(shù)組,并匯總;
打印輸出給用戶態(tài)。
接下來(lái)我們把每一步都展開(kāi)來(lái)詳細(xì)看看。
通過(guò)使用 strace 跟蹤 top 命令的各種系統(tǒng)調(diào)用,可以看到它對(duì)該文件的調(diào)用。
#stracetop ... openat(AT_FDCWD,"/proc/stat",O_RDONLY)=4 openat(AT_FDCWD,"/proc/2351514/stat",O_RDONLY)=8 openat(AT_FDCWD,"/proc/2393539/stat",O_RDONLY)=8 ...
除了 /proc/stat 外,還有各個(gè)進(jìn)程細(xì)分的 /proc/{pid}/stat,是用來(lái)計(jì)算各個(gè)進(jìn)程的 cpu 利用率時(shí)使用的。
內(nèi)核為各個(gè)偽文件都定義了處理函數(shù),/proc/stat 文件的處理方法是 proc_stat_operations。
//file:fs/proc/stat.c staticint__initproc_stat_init(void) { proc_create("stat",0,NULL,&proc_stat_operations); return0; } staticconststructfile_operationsproc_stat_operations={ .open=stat_open, ... };
proc_stat_operations 中包含了該文件對(duì)應(yīng)的操作方法。當(dāng)打開(kāi) /proc/stat 文件的時(shí)候,stat_open 就會(huì)被調(diào)用到。stat_open 依次調(diào)用 single_open_size,show_stat 來(lái)輸出數(shù)據(jù)內(nèi)容。我們來(lái)看看它的代碼:
//file:fs/proc/stat.c staticintshow_stat(structseq_file*p,void*v) { u64user,nice,system,idle,iowait,irq,softirq,steal; for_each_possible_cpu(i){ structkernel_cpustat*kcs=&kcpustat_cpu(i); user+=kcs->cpustat[CPUTIME_USER]; nice+=kcs->cpustat[CPUTIME_NICE]; system+=kcs->cpustat[CPUTIME_SYSTEM]; idle+=get_idle_time(kcs,i); iowait+=get_iowait_time(kcs,i); irq+=kcs->cpustat[CPUTIME_IRQ]; softirq+=kcs->cpustat[CPUTIME_SOFTIRQ]; ... } //轉(zhuǎn)換成節(jié)拍數(shù)并打印出來(lái) seq_put_decimal_ull(p,"cpu",nsec_to_clock_t(user)); seq_put_decimal_ull(p,"",nsec_to_clock_t(nice)); seq_put_decimal_ull(p,"",nsec_to_clock_t(system)); seq_put_decimal_ull(p,"",nsec_to_clock_t(idle)); seq_put_decimal_ull(p,"",nsec_to_clock_t(iowait)); seq_put_decimal_ull(p,"",nsec_to_clock_t(irq)); seq_put_decimal_ull(p,"",nsec_to_clock_t(softirq)); ... }
在上面的代碼中,for_each_possible_cpu 是在遍歷存儲(chǔ)著 cpu 使用率數(shù)據(jù)的 kcpustat_cpu 變量。該變量是一個(gè) percpu 變量,它為每一個(gè)邏輯核都準(zhǔn)備了一個(gè)數(shù)組元素。里面存儲(chǔ)著當(dāng)前核所對(duì)應(yīng)各種事件,包括 user、nice、system、idel、iowait、irq、softirq 等。
在這個(gè)循環(huán)中,將每一個(gè)核的每種使用率都加起來(lái)。最后通過(guò) seq_put_decimal_ull 將這些數(shù)據(jù)輸出出來(lái)。
注意,在內(nèi)核中實(shí)際每個(gè)時(shí)間記錄的是納秒數(shù),但是在輸出的時(shí)候統(tǒng)一都轉(zhuǎn)化成了節(jié)拍單位。至于節(jié)拍單位多長(zhǎng),下一節(jié)我們介紹。總之, /proc/stat 的輸出是從 kernel_cpustat 這個(gè) percpu 變量中讀取出來(lái)的。
我們接著再看看這個(gè)變量中的數(shù)據(jù)是何時(shí)加進(jìn)來(lái)的。
三、統(tǒng)計(jì)數(shù)據(jù)怎么來(lái)的
前面我們提到內(nèi)核是以采樣的方式來(lái)統(tǒng)計(jì) cpu 使用率的。這個(gè)采樣周期依賴的是 Linux 時(shí)間子系統(tǒng)中的定時(shí)器。
Linux 內(nèi)核每隔固定周期會(huì)發(fā)出 timer interrupt (IRQ 0),這有點(diǎn)像樂(lè)譜中的節(jié)拍的概念。每隔一段時(shí)間,就打出一個(gè)拍子,Linux 就響應(yīng)之并處理一些事情。
一個(gè)節(jié)拍的長(zhǎng)度是多長(zhǎng)時(shí)間,是通過(guò) CONFIG_HZ 來(lái)定義的。它定義的方式是每一秒有幾次 timer interrupts。不同的系統(tǒng)中這個(gè)節(jié)拍的大小可能不同,通常在 1 ms 到 10 ms 之間??梢栽谧约旱?Linux config 文件中找到它的配置。
#grep^CONFIG_HZ/boot/config-5.4.56.bsk.10-amd64 CONFIG_HZ=1000
從上述結(jié)果中可以看出,我的機(jī)器每秒要打出 1000 次節(jié)拍。也就是每 1 ms 一次。
每次當(dāng)時(shí)間中斷到來(lái)的時(shí)候,都會(huì)調(diào)用 update_process_times 來(lái)更新系統(tǒng)時(shí)間。更新后的時(shí)間都存儲(chǔ)在我們前面提到的 percpu 變量 kcpustat_cpu 中。
我們來(lái)詳細(xì)看下匯總過(guò)程 update_process_times 的源碼,它位于 kernel/time/timer.c 文件中。
//file:kernel/time/timer.c voidupdate_process_times(intuser_tick) { structtask_struct*p=current; //進(jìn)行時(shí)間累積處理 account_process_tick(p,user_tick); ... }
這個(gè)函數(shù)的參數(shù) user_tick 指的是采樣的瞬間是處于內(nèi)核態(tài)還是用戶態(tài)。接下來(lái)調(diào)用 account_process_tick。
//file:kernel/sched/cputime.c voidaccount_process_tick(structtask_struct*p,intuser_tick) { cputime=TICK_NSEC; ... if(user_tick) //3.1統(tǒng)計(jì)用戶態(tài)時(shí)間 account_user_time(p,cputime); elseif((p!=rq->idle)||(irq_count()!=HARDIRQ_OFFSET)) //3.2統(tǒng)計(jì)內(nèi)核態(tài)時(shí)間 account_system_time(p,HARDIRQ_OFFSET,cputime); else //3.3統(tǒng)計(jì)空閑時(shí)間 account_idle_time(cputime); }
在這個(gè)函數(shù)中,首先設(shè)置 cputime = TICK_NSEC, 一個(gè) TICK_NSEC 的定義是一個(gè)節(jié)拍所占的納秒數(shù)。接下來(lái)根據(jù)判斷結(jié)果分別執(zhí)行 account_user_time、account_system_time 和 account_idle_time 來(lái)統(tǒng)計(jì)用戶態(tài)、內(nèi)核態(tài)和空閑時(shí)間。
3.1 用戶態(tài)時(shí)間統(tǒng)計(jì)
//file:kernel/sched/cputime.c voidaccount_user_time(structtask_struct*p,u64cputime) { //分兩種種情況統(tǒng)計(jì)用戶態(tài)CPU的使用情況 intindex; index=(task_nice(p)>0)?CPUTIME_NICE:CPUTIME_USER; //將時(shí)間累積到/proc/stat中 task_group_account_field(p,index,cputime); ...... }
account_user_time 函數(shù)主要分兩種情況統(tǒng)計(jì):
如果進(jìn)程的 nice 值大于 0,那么將會(huì)增加到 CPU 統(tǒng)計(jì)結(jié)構(gòu)的 nice 字段中。
如果進(jìn)程的 nice 值小于等于 0,那么增加到 CPU 統(tǒng)計(jì)結(jié)構(gòu)的 user 字段中。
看到這里,開(kāi)篇的問(wèn)題 2 就有答案了,其實(shí)用戶態(tài)的時(shí)間不只是 user 字段,nice 也是。之所以要把 nice 分出來(lái),是為了讓 Linux 用戶更一目了然地看到調(diào)過(guò) nice 的進(jìn)程所占的 cpu 周期有多少。
我們平時(shí)如果想要觀察系統(tǒng)的用戶態(tài)消耗的時(shí)間的話,應(yīng)該是將 top 中輸出的 user 和 nice 加起來(lái)一并考慮,而不是只看 user!
接著調(diào)用 task_group_account_field 來(lái)把時(shí)間加到前面我們用到的 kernel_cpustat 內(nèi)核變量中。
//file:kernel/sched/cputime.c staticinlinevoidtask_group_account_field(structtask_struct*p,intindex, u64tmp) { __this_cpu_add(kernel_cpustat.cpustat[index],tmp); ... }
3.2 內(nèi)核態(tài)時(shí)間統(tǒng)計(jì)
我們?cè)賮?lái)看內(nèi)核態(tài)時(shí)間是如何統(tǒng)計(jì)的,找到 account_system_time 的代碼。
//file:kernel/sched/cputime.c voidaccount_system_time(structtask_struct*p,inthardirq_offset,u64cputime) { if(hardirq_count()-hardirq_offset) index=CPUTIME_IRQ; elseif(in_serving_softirq()) index=CPUTIME_SOFTIRQ; else index=CPUTIME_SYSTEM; account_system_index_time(p,cputime,index); }
內(nèi)核態(tài)的時(shí)間主要分 3 種情況進(jìn)行統(tǒng)計(jì)。
如果當(dāng)前處于硬中斷執(zhí)行上下文, 那么統(tǒng)計(jì)到 irq 字段中;
如果當(dāng)前處于軟中斷執(zhí)行上下文, 那么統(tǒng)計(jì)到 softirq 字段中;
否則統(tǒng)計(jì)到 system 字段中。
判斷好要加到哪個(gè)統(tǒng)計(jì)項(xiàng)中后,依次調(diào)用 account_system_index_time、task_group_account_field 來(lái)將這段時(shí)間加到內(nèi)核變量 kernel_cpustat 中。
//file:kernel/sched/cputime.c staticinlinevoidtask_group_account_field(structtask_struct*p,intindex, u64tmp) { __this_cpu_add(kernel_cpustat.cpustat[index],tmp); }
3.3 空閑時(shí)間的累積
沒(méi)錯(cuò),在內(nèi)核變量 kernel_cpustat 中不僅僅是統(tǒng)計(jì)了各種用戶態(tài)、內(nèi)核態(tài)的使用時(shí)間,空閑也一并統(tǒng)計(jì)起來(lái)了。
如果在采樣的瞬間,cpu 既不在內(nèi)核態(tài)也不在用戶態(tài)的話,就將當(dāng)前節(jié)拍的時(shí)間都累加到 idle 中。
//file:kernel/sched/cputime.c voidaccount_idle_time(u64cputime) { u64*cpustat=kcpustat_this_cpu->cpustat; structrq*rq=this_rq(); if(atomic_read(&rq->nr_iowait)>0) cpustat[CPUTIME_IOWAIT]+=cputime; else cpustat[CPUTIME_IDLE]+=cputime; }
在 cpu 空閑的情況下,進(jìn)一步判斷當(dāng)前是不是在等待 IO(例如磁盤(pán) IO),如果是的話這段空閑時(shí)間會(huì)加到 iowait 中,否則就加到 idle 中。從這里,我們可以看到 iowait 其實(shí)是 cpu 的空閑時(shí)間,只不過(guò)是在等待 IO 完成而已。
看到這里,開(kāi)篇問(wèn)題 3 也有非常明確的答案了,io wait 其實(shí)是 cpu 在空閑狀態(tài)的一項(xiàng)統(tǒng)計(jì),只不過(guò)這種狀態(tài)和 idle 的區(qū)別是 cpu 是因?yàn)榈却?io 而空閑。
四、總結(jié)
本文深入分析了 Linux 統(tǒng)計(jì)系統(tǒng) CPU 利用率的內(nèi)部原理。全文的內(nèi)容可以用如下一張圖來(lái)匯總:
Linux 中的定時(shí)器會(huì)以某個(gè)固定節(jié)拍,比如 1 ms 一次采樣各個(gè) cpu 核的使用情況,然后將當(dāng)前節(jié)拍的所有時(shí)間都累加到 user/nice/system/irq/softirq/io_wait/idle 中的某一項(xiàng)上。
top 命令是讀取的 /proc/stat 中輸出的 cpu 各項(xiàng)利用率數(shù)據(jù),而這個(gè)數(shù)據(jù)在內(nèi)核中是根據(jù) kernel_cpustat 來(lái)匯總并輸出的。
回到開(kāi)篇問(wèn)題 1,top 輸出的利用率信息是如何計(jì)算出來(lái)的,它精確嗎?
/proc/stat 文件輸出的是某個(gè)時(shí)間點(diǎn)的各個(gè)指標(biāo)所占用的節(jié)拍數(shù)。如果想像 top 那樣輸出一個(gè)百分比,計(jì)算過(guò)程是分兩個(gè)時(shí)間點(diǎn) t1, t2 分別獲取一下 stat 文件中的相關(guān)輸出,然后經(jīng)過(guò)個(gè)簡(jiǎn)單的算術(shù)運(yùn)算便可以算出當(dāng)前的 cpu 利用率。
再說(shuō)是否精確。這個(gè)統(tǒng)計(jì)方法是采樣的,只要是采樣,肯定就不是百分之百精確。但由于我們查看 cpu 使用率的時(shí)候往往都是計(jì)算 1 秒甚至更長(zhǎng)一段時(shí)間的使用情況,這其中會(huì)包含很多采樣點(diǎn),所以查看整體情況是問(wèn)題不大的。
另外從本文,我們也學(xué)到了 top 中輸出的 cpu 時(shí)間項(xiàng)目其實(shí)大致可以分為三類:
第一類:用戶態(tài)消耗時(shí)間,包括 user 和 nice。如果想看用戶態(tài)的消耗,要將 user 和 nice 加起來(lái)看才對(duì)。
第二類:內(nèi)核態(tài)消耗時(shí)間,包括 irq、softirq 和 system。
第三類:空閑時(shí)間,包括 io_wait 和 idle。其中 io_wait 也是 cpu 的空閑狀態(tài),只不過(guò)是在等 io 完成而已。如果只是想看 cpu 到底有多閑,應(yīng)該把 io_wait 和 idle 加起來(lái)才對(duì)。
審核編輯:劉清
-
服務(wù)器
+關(guān)注
關(guān)注
12文章
9204瀏覽量
85547 -
定時(shí)器
+關(guān)注
關(guān)注
23文章
3250瀏覽量
114921 -
LINUX內(nèi)核
+關(guān)注
關(guān)注
1文章
316瀏覽量
21660
原文標(biāo)題:Linux 中 CPU 利用率是如何算出來(lái)的?
文章出處:【微信號(hào):良許Linux,微信公眾號(hào):良許Linux】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論