Linux 中 CPU 利用率是如何算出來的?
大家好,我是飛哥!
線上上伺服器觀察線上服務執行狀態的時候,絕大多數人都是喜歡先用 top 命令看看當前系統的整體 cpu 利用率。例如,隨手拿來的一臺機器,top 命令顯示的利用率資訊如下
這個輸出結果說簡單也簡單,說複雜也不是那麼容易就能全部搞明白的。例如:
問題 1:top 輸出的利用率資訊是如何計算出來的,它精確嗎?
問題 2:ni 這一列是 nice,它輸出的是 cpu 在處理啥時的開銷?
問題 3:wa 代表的是 io wait,那麼這段時間中 cpu 到底是忙碌還是空閒?
今天我們對 cpu 利用率統計進行深入的學習。透過今天的學習,你不但能瞭解 cpu 利用率統計實現細節,還能 nice、io wait 等指標有更深入的理解。
區別於以往的文章,今天我們不直接進入 Linux 實現,而是先從自己的思考開始!
一、先思考一下
拋開 Linux 的實現先不談,如果有如下需求,有一個四核伺服器,上面跑了四個程式。
讓你來設計計算整個系統 cpu 利用率的這個需求,支援像 top 命令這樣的輸出,滿足以下要求:
cpu 使用率要儘可能地準確 要能地體現秒級瞬時 cpu 狀態
可以先停下來閱讀思考幾分鐘。
好,思考結束。經過思考你會發現,這個看起來很簡單的需求,實際還是有點小複雜的。
其中一個思路是把所有程式的執行時間都加起來,然後再除以系統執行總時間*4。
這個思路是沒問題的,用這種方法統計很長一段時間內的 cpu 利用率是可以的,統計也足夠的準確。
但只要用過 top 你就知道 top 輸出的 cpu 利用率並不是長時間不變的,而是預設 3 秒為單位會動態更新一下(這個時間間隔可以使用 -d 設定)。我們的這個方案體現總利用率可以,體現這種瞬時的狀態就難辦了。你可能會想到那我也 3 秒算一次不就行了?但這個 3 秒的時間從哪個點開始呢。粒度很不好控制。
上一個思路問題核心就是如何解決瞬時問題。提到瞬時狀態,你可能就又來思路了。那我就用瞬時取樣去看,看看當前有幾個核在忙。四個核中如果有兩個核在忙,那利用率就是 50%。
這個思路思考的方向也是正確的,但是問題有兩個:
你算出的數字都是 25% 的整數倍 這個瞬時值會導致 cpu 使用率顯示的劇烈震盪。
比如下圖:
在 t1 的瞬時狀態看來,系統的 cpu 利用率毫無疑問就是 100%,但在 t2 時間看來,使用率又變成 0% 了。思路方向是對的,但顯然這種粗暴的計算無法像 top 命令一樣優雅地工作。
我們再改進一下它,把上面兩個思路結合起來,可能就能解決我們的問題了。在取樣上,我們把週期定的細一些,但在計算上我們把週期定的粗一些。
我們引入採用週期的概念,定時比如每 1 毫秒取樣一次。如果取樣的瞬時,cpu 在執行,就將這 1 ms 記錄為使用。這時會得出一個瞬時的 cpu 使用率,把它都存起來。
在統計 3 秒內的 cpu 使用率的時候,比如上圖中的 t1 和 t2 這段時間範圍。那就把這段時間內的所有瞬時值全加一下,取個平均值。這樣就能解決上面的問題了,統計相對準確,避免了瞬時值劇烈震盪且粒度過粗(只能以 25 %為單位變化)的問題了。
可能有同學會問了,假如 cpu 在兩次取樣中間發生變化了呢,如下圖這種情況。
在當前取樣點到來的時候,程式 A 其實剛執行完,有一點點時間沒有既沒被上一個取樣點統計到,本次也統計不到。對於程式 B,其實只開始了一小段時間,把 1 ms 全記上似乎有點多記了。
確實會存在這個問題,但因為我們的取樣是 1 ms 一次,而我們實際檢視使用的時候最少也有是秒級別地用,會包括有成千上萬個取樣點的資訊,所以這種誤差並不會影響我們對全域性的把握。
事實上,Linux 也就是這樣來統計系統 cpu 利用率的。雖然可能會有誤差,但作為一項統計資料使用已經是足夠了的。在實現上,Linux 是將所有的瞬時值都累加到某一個資料上的,而不是真的存了很多份的瞬時資料。
接下來就讓我們進入 Linux 來檢視它對系統 cpu 利用率統計的具體實現。
二、top 命令使用資料在哪兒
上一節我們說的 Linux 在實現上是將瞬時值都累加到某一個資料上的,這個值是核心透過 /proc/stat 偽檔案來對使用者態暴露。Linux 在計算系統 cpu 利用率的時候用的就是它。
整體上看,top 命令工作的內部細節如下圖所示。
top 命令訪問 /proc/stat 獲取各項 cpu 利用率使用值 核心呼叫 stat_open 函式來處理對 /proc/stat 的訪問 核心訪問的資料來源於 kernel_cpustat 陣列,並彙總 列印輸出給使用者態
接下來我們把每一步都展開來詳細看看。
透過使用 strace 跟蹤 top 命令的各種系統呼叫,可以看的到它對該檔案的呼叫。
# strace top
...
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 外,還有各個程式細分的 /proc/{pid}/stat,是用來計算各個程式的 cpu 利用率時使用的。
核心為各個偽檔案都定義了處理函式,/proc/stat 檔案的處理方法是 proc_stat_operations。
//file:fs/proc/stat.c
static int __init proc_stat_init(void)
{
proc_create("stat", 0, NULL, &proc_stat_operations);
return 0;
}
static const struct file_operations proc_stat_operations = {
.open = stat_open,
...
};
proc_stat_operations 中包含了該檔案時對應的操作方法。當開啟 /proc/stat 檔案的時候,stat_open 就會被呼叫到。stat_open 依次呼叫 single_open_size,show_stat 來輸出資料內容。我們來看看它的程式碼:
//file:fs/proc/stat.c
static int show_stat(struct seq_file *p, void *v)
{
u64 user, nice, system, idle, iowait, irq, softirq, steal;
for_each_possible_cpu(i) {
struct kernel_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];
...
}
//轉換成節拍數並列印出來
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 是在遍歷儲存著 cpu 使用率資料的 kcpustat_cpu 變數。該變數是一個 percpu 變數,它為每一個邏輯核都準備了一個陣列元素。裡面儲存著當前核所對應各種事件,包括 user、nice、system、idel、iowait、irq、softirq 等。
在這個迴圈中,將每一個核的每種使用率都加起來。最後透過 seq_put_decimal_ull 將這些資料輸出出來。
注意,在核心中實際每個時間記錄的是納秒數,但是在輸出的時候統一都轉化成了節拍單位。至於節拍單位多長,下一節我們介紹。總之, /proc/stat 的輸出是從 kernel_cpustat 這個 percpu 變數中讀取出來的。
我們接著再看看這個變數中的資料是何時加進來的。
三、統計資料怎麼來的
前面我們提到核心是以取樣的方式來統計 cpu 使用率的。這個取樣週期依賴的是 Linux 時間子系統中的定時器。
Linux 核心每隔固定週期會發出 timer interrupt (IRQ 0),這有點像樂譜中的節拍的概念。每隔一段時間,就打出一個拍子,Linux 就響應之並處理一些事情。
一個節拍的長度是多長時間,是透過 CONFIG_HZ 來定義的。它定義的方式是每一秒有幾次 timer interrupts。不同的系統中這個節拍的大小可能不同,通常在 1 ms 到 10 ms 之間。可以在自己的 Linux config 檔案中找到它的配置。
# grep ^CONFIG_HZ /boot/config-5.4.56.bsk.10-amd64
CONFIG_HZ=1000
從上述結果中可以看出,我的機器的每秒要打出 1000 次節拍。也就是每 1 ms 一次。
每次當時間中斷到來的時候,都會呼叫 update_process_times 來更新系統時間。更新後的時間都儲存在我們前面提到的 percpu 變數 kcpustat_cpu 中。
我們來詳細看下彙總過程 update_process_times 的原始碼,它位於 kernel/time/timer.c 檔案中。
//file:kernel/time/timer.c
void update_process_times(int user_tick)
{
struct task_struct *p = current;
//進行時間累積處理
account_process_tick(p, user_tick);
...
}
這個函式的引數 user_tick 值得是取樣的瞬間是處於核心態還是使用者態。接下來呼叫 account_process_tick。
//file:kernel/sched/cputime.c
void account_process_tick(struct task_struct *p, int user_tick)
{
cputime = TICK_NSEC;
...
if (user_tick)
//3.1 統計使用者態時間
account_user_time(p, cputime);
else if ((p != rq->idle) || (irq_count() != HARDIRQ_OFFSET))
//3.2 統計核心態時間
account_system_time(p, HARDIRQ_OFFSET, cputime);
else
//3.3 統計空閒時間
account_idle_time(cputime);
}
在這個函式中,首先設定 cputime = TICK_NSEC
, 一個 TICK_NSEC 的定義是一個節拍所佔的納秒數。接下來根據判斷結果分別執行 account_user_time、account_system_time 和 account_idle_time 來統計使用者態、核心態和空閒時間。
3.1 使用者態時間統計
//file:kernel/sched/cputime.c
void account_user_time(struct task_struct *p, u64 cputime)
{
//分兩種種情況統計使用者態 CPU 的使用情況
int index;
index = (task_nice(p) > 0) ? CPUTIME_NICE : CPUTIME_USER;
//將時間累積到 /proc/stat 中
task_group_account_field(p, index, cputime);
......
}
account_user_time 函式主要分兩種情況統計:
如果程式的 nice 值大於 0,那麼將會增加到 CPU 統計結構的 nice 欄位中。 如果程式的 nice 值小於等於 0,那麼增加到 CPU 統計結構的 user 欄位中。
看到這裡,開篇的問題 2 就有答案了,其實使用者態的時間不只是 user 欄位,nice 也是。之所以要把 nice 分出來,是為了讓 Linux 使用者更一目瞭然地看到調過 nice 的程式所佔的 cpu 週期有多少。
我們平時如果想要觀察系統的使用者態消耗的時間的話,應該是將 top 中輸出的 user 和 nice 加起來一併考慮,而不是隻看 user!
接著呼叫 task_group_account_field 來把時間加到前面我們用到的 kernel_cpustat 核心變數中。
//file:kernel/sched/cputime.c
static inline void task_group_account_field(struct task_struct *p, int index,
u64 tmp)
{
__this_cpu_add(kernel_cpustat.cpustat[index], tmp);
...
}
3.2 核心態時間統計
我們再來看核心態時間是如何統計的,找到 account_system_time 的程式碼。
//file:kernel/sched/cputime.c
void account_system_time(struct task_struct *p, int hardirq_offset, u64 cputime)
{
if (hardirq_count() - hardirq_offset)
index = CPUTIME_IRQ;
else if (in_serving_softirq())
index = CPUTIME_SOFTIRQ;
else
index = CPUTIME_SYSTEM;
account_system_index_time(p, cputime, index);
}
核心態的時間主要分 3 種情況進行統計。
如果當前處於硬中斷執行上下文, 那麼統計到 irq 欄位中 如果當前處於軟中斷執行上下文, 那麼統計到 softirq 欄位中 否則統計到 system 欄位中
判斷好要加到哪個統計項中後,依次呼叫 account_system_index_time、task_group_account_field 來將這段時間加到核心變數 kernel_cpustat 中
//file:kernel/sched/cputime.c
static inline void task_group_account_field(struct task_struct *p, int index,
u64 tmp)
{
__this_cpu_add(kernel_cpustat.cpustat[index], tmp);
}
3.3 空閒時間的累積
沒錯,在核心變數 kernel_cpustat 中不僅僅是統計了各種使用者態、核心態的使用統計,空閒也一併統計起來了。
如果在取樣的瞬間,cpu 既不在核心態也不在使用者態的話,就將當前節拍的時間都累加到 idle 中。
//file:kernel/sched/cputime.c
void account_idle_time(u64 cputime)
{
u64 *cpustat = kcpustat_this_cpu->cpustat;
struct rq *rq = this_rq();
if (atomic_read(&rq->nr_iowait) > 0)
cpustat[CPUTIME_IOWAIT] += cputime;
else
cpustat[CPUTIME_IDLE] += cputime;
}
在 cpu 空閒的情況下,進一步判斷當前是不是在等待 IO(例如磁碟 IO),如果是的話這段空閒時間會加到 iowait 中,否則就加到 idle 中。從這裡,我們可以看到 iowait 其實是 cpu 的空閒時間,只不過是在等待 IO 完成而已。
看到這裡,開篇問題 3 也有非常明確的答案了,io wait 其實是 cpu 在空閒狀態的一項統計,只不過這種狀態和 idle 的區別是 cpu 是因為等待 io 而空閒。
四、總結
本文深入分析了 Linux 統計系統 CPU 利用率的內部原理。全文的內容可以用如下一張圖來彙總:
Linux 中的定時器會以某個固定節拍,比如 1 ms 一次取樣各個 cpu 核的使用情況,然後將當前節拍的所有時間都累加到 user/nice/system/irq/softirq/io_wait/idle 中的某一項上。
top 命令是讀取的 /proc/stat 中輸出的 cpu 各項利用率資料,而這個資料在核心中的是根據 kernel_cpustat 來彙總並輸出的。
回到開篇問題 1,top 輸出的利用率資訊是如何計算出來的,它精確嗎?
/proc/stat 檔案輸出的是某個時間點的各個指標所佔用的節拍數。如果想像 top 那樣輸出一個百分比,計算過程是分兩個時間點 t1, t2 分別獲取一下 stat 檔案中的相關輸出,然後經過個簡單的算術運算便可以算出當前的 cpu 利用率。
我也提供了一個簡單的 shell 程式碼,你可以把它下載下來,用它來實際檢視一下你伺服器的 cpu 利用率,我放到我的 github 上了。
Github 地址:
再說是否精確。這個統計方法是取樣的,只要是取樣,肯定就不是百分之百精確。但由於我們檢視 cpu 使用率的時候往往都是計算 1 秒甚至更長一段時間的使用情況,這其中會包含很多采樣點,所以檢視整體情況是問題不大的。
另外從本文,我們也學到了 top 中輸出的 cpu 時間專案其實大致可以分為三類:
第一類:使用者態消耗時間,包括 user 和 nice。如果想看使用者態的消耗,要將 user 和 nice 加起來看才對。
第二類:核心態消耗時間,包括 irq、softirq 和 system。
第三類:空閒時間,包括 io_wait 和 idle。其中 io_wait 也是 cpu 的空閒狀態,只不過是在等 io 完成而已。如果只是想看 cpu 到底有多閒,應該把 io_wait 和 idle 加起來才對。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024923/viewspace-2936620/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- CPU利用率過高的原因
- 如何進行Linux CPU中的Kernel space分析Linux
- 提升CPU等資源的利用率,減少CPU的空轉
- 在Linux中,如何獲取CPU的總核心數?Linux
- 如何在 Linux 中找出 CPU 佔用高的程序Linux
- 用CPU來加速你的Linux命令Linux
- Linux中如何檢視CPU使用率?Linux
- mysql expire_logs_days是怎麼生效和計算出來的MySql
- 在Linux中,如何檢視佔用CPU最多的程序?Linux
- mysql中CPU或記憶體利用率過高問題MySql記憶體
- Linux 從頭學 01:CPU 是如何執行一條指令的?Linux
- 如何在Ubuntu Linux中獲取CPU溫度UbuntuLinux
- One Order行專案裡Item Category是怎麼計算出來的Go
- 核心是如何給容器中的程式分配CPU資源的?
- 在Linux中,如何查詢系統中佔用CPU最高的程序?Linux
- Windows 平臺計算 CPU 總利用率Windows
- REDHAT中如何判斷主機的CPU是32位還是64位的Redhat
- SQL Server CPU 利用率毛刺的分析定位與解決SQLServer
- 在 Linux 中查詢 CPU 的核數Linux
- grafana展示的CPU利用率與實際不符的問題探究Grafana
- Linux下如何使用X86 CPU的GPIOLinux
- mysql資料庫Cpu利用率100%問題排查MySql資料庫
- 在Linux中,發現CPU負載過大,接下來怎麼辦?Linux負載
- 監控Ubuntu Linux中的CPU/GPU 溫度UbuntuLinuxGPU
- Linux 中CPU 和 GPU 的行為監控LinuxGPU
- 7種Linux中獲取CPU速度的方法Linux
- 遊戲中的角色是如何“動”起來的?遊戲
- 微軟將在9月中旬修復CPU利用率飆高的問題微軟
- 在Linux中,什麼是SSH?它是如何工作的?Linux
- 在Linux中,什麼是管道?它是如何工作的?Linux
- 你來寫,“Calca”來快速算出計算結果
- 在Linux中,如何檢查系統的CPU和記憶體使用情況?Linux記憶體
- Linux 效能優化之 CPU 篇 ----- Linux 軟中斷Linux優化
- 在K8s中調整JVM提高CPU和記憶體利用率 - AnuragK8SJVM記憶體
- CPU 電源管理器:Linux 系統中 CPU 主頻的控制和管理Linux
- 在Linux中,什麼是守護程序,它們是如何工作的?Linux
- 深入 Nodejs 原始碼探究 CPU 資訊的獲取與利用率計算NodeJS原始碼
- MVC 框架中的路由器(Router)是如何跑起來的MVC框架路由器