在線上伺服器觀察線上服務運作狀態的時候,絕大多數人都是喜歡先用 top 指令看看目前系統的整體 cpu 使用率。例如,隨手拿來的一台機器,top 指令顯示的使用率資訊如下:

這個輸出結果說簡單也簡單,說複雜也不是那麼容易就能全部搞懂的。例如:
問題 1:top 輸出的使用率資訊是如何計算出來的,它精確嗎?
問題 2:ni 這一列是 nice,它輸出的是 cpu 在處理啥時的開銷?
問題 3:wa 代表的是 io wait,那麼這段時間中 cpu 到底是忙碌還是空閒?
今天我們對 cpu 使用率統計進行深入的學習。透過現今的學習,你不僅能了解 cpu 使用率統計實現細節,還能對 nice、io wait 等指標有更深入的理解。
今天我們先從自己的思考開始!
一、先思考一下
#拋開 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 利用率。
再说是否精确。这个统计方法是采样的,只要是采样,肯定就不是百分之百精确。但由于我们查看 cpu 使用率的时候往往都是计算 1 秒甚至更长一段时间的使用情况,这其中会包含很多采样点,所以查看整体情况是问题不大的。
另外从本文,我们也学到了 top 中输出的 cpu 时间项目其实大致可以分为三类:
第****一类:用户态消耗时间,包括 user 和 nice。如果想看用户态的消耗,要将 user 和 nice 加起来看才对。
第二类:内核态消耗时间,包括 irq、softirq 和 system。
第三类:空闲时间,包括 io_wait 和 idle。其中 io_wait 也是 cpu 的空闲状态,只不过是在等 io 完成而已。如果只是想看 cpu 到底有多闲,应该把 io_wait 和 idle 加起来才对。
以上是Linux 中 CPU 使用率是如何算出來的?的詳細內容。更多資訊請關注PHP中文網其他相關文章!

linux设备节点是应用程序和设备驱动程序沟通的一个桥梁;设备节点被创建在“/dev”,是连接内核与用户层的枢纽,相当于硬盘的inode一样的东西,记录了硬件设备的位置和信息。设备节点使用户可以与内核进行硬件的沟通,读写设备以及其他的操作。

区别:1、open是UNIX系统调用函数,而fopen是ANSIC标准中的C语言库函数;2、open的移植性没fopen好;3、fopen只能操纵普通正规文件,而open可以操作普通文件、网络套接字等;4、open无缓冲,fopen有缓冲。

在linux中,可以利用“rpm -qa pcre”命令判断pcre是否安装;rpm命令专门用于管理各项套件,使用该命令后,若结果中出现pcre的版本信息,则表示pcre已经安装,若没有出现版本信息,则表示没有安装pcre。

在linux中,eof是自定义终止符,是“END Of File”的缩写;因为是自定义的终止符,所以eof就不是固定的,可以随意的设置别名,linux中按“ctrl+d”就代表eof,eof一般会配合cat命令用于多行文本输出,指文件末尾。

端口映射又称端口转发,是指将外部主机的IP地址的端口映射到Intranet中的一台计算机,当用户访问外网IP的这个端口时,服务器自动将请求映射到对应局域网内部的机器上;可以通过使用动态或固定的公共网络IP路由ADSL宽带路由器来实现。

linux查询mac地址的方法:1、打开系统,在桌面中点击鼠标右键,选择“打开终端”;2、在终端中,执行“ifconfig”命令,查看输出结果,在输出信息第四行中紧跟“ether”单词后的字符串就是mac地址。

手机远程linux工具有:1、JuiceSSH,是一款功能强大的安卓SSH客户端应用,可直接对linux服务进行管理;2、Termius,可以利用手机来连接Linux服务器;3、Termux,一个强大的远程终端工具;4、向日葵远程控制等等。

linux中,lsb是linux标准基础的意思,是“Linux Standards Base”的缩写,是linux标准化领域中的标准;lsb制定了应用程序与运行环境之间的二进制接口,保证了linux发行版与linux应用程序之间的良好结合。


熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

AI Hentai Generator
免費產生 AI 無盡。

熱門文章

熱工具

SublimeText3 英文版
推薦:為Win版本,支援程式碼提示!

SublimeText3漢化版
中文版,非常好用

WebStorm Mac版
好用的JavaScript開發工具

SublimeText3 Mac版
神級程式碼編輯軟體(SublimeText3)

SublimeText3 Linux新版
SublimeText3 Linux最新版