在 Linux 下我們通過 top 或者 htop 命令可以看到當前的 CPU 資源利用率,另外在一些監控工具中你可能也遇見過,那麼它是如何計算的呢?在 Nodejs 中我們該如何實現?
帶著這些疑問,本節會先從 Linux 下的 CPU 利用率進行一個簡單講解做一下前置知識鋪墊,之後會深入 Nodejs 原始碼,去探討如何獲取 CPU 資訊及計算 CPU 某時間段的利用率。
開始之前,可以先看一張圖,它展示了 Nodejs OS 模組讀取系統 CPU 資訊的整個過程呼叫,在下文中也會詳細講解,你會再次看到它。
Linux 下 CPU 利用率
Linux 下 CPU 的利用率分為使用者態(使用者模式下執行時間)、系統態(系統核心執行)、空閒態(空閒系統程式執行時間),三者相加為 CPU 執行總時間,關於 CPU 的活動資訊我們可以在 /proc/stat 檔案檢視。
CPU 利用率是指非系統空閒程式 / CPU 總執行時間。
> cat /proc/stat
cpu 2255 34 2290 22625563 6290 127 456
cpu0 1132 34 1441 11311718 3675 127 438
cpu1 1123 0 849 11313845 2614 0 18
intr 114930548 113199788 3 0 5 263 0 4 [... lots more numbers ...]
ctxt 1990473 # 自系統啟動以來 CPU 發生的上下文交換次數
btime 1062191376 # 啟動到現在為止的時間,單位為秒
processes 2915 # 系統啟動以來所建立的任務數目
procs_running 1 # 當前執行佇列的任務數目
procs_blocked 0 # 當前被阻塞的任務數目
複製程式碼
上面第一行 cpu 表示總的 CPU 使用情況,下面的cpu0、cpu1 是指系統的每個 CPU 核心數執行情況(cpu0 + cpu1 + cpuN = cpu 總的核心數),我們看下第一行的含義。
- user:系統啟動開始累計到當前時刻,使用者態的 CPU 時間(單位:jiffies),不包含 nice 值為負的程式。
- nice:系統啟動開始累計到當前時刻,nice 值為負的程式所佔用的 CPU 時間。
- system:系統啟動開始累計到當前時刻,核心時間
- idle:從系統啟動開始累計到當前時刻,除硬碟IO等待時間以外其它等待時間
- iowait:從系統啟動開始累計到當前時刻,硬碟IO等待時間
- irq:從系統啟動開始累計到當前時刻,硬中斷時間
- softirq:從系統啟動開始累計到當前時刻,軟中斷時間
關於 /proc/stat 的介紹,參考這裡 www.linuxhowtos.org/System/proc…
CPU 某時間段利用率公式
/proc/stat 檔案下展示的是系統從啟動到當下所累加的總的 CPU 時間,如果要計算 CPU 在某個時間段的利用率,則需要取 t1、t2 兩個時間點進行運算。
t1~t2 時間段的 CPU 執行時間:
t1 = (user1 + nice1 + system1 + idle1 + iowait1 + irq1 + softirq1)
t2 = (user2 + nice2 + system2 + idle2 + iowait2 + irq2 + softirq2)
t = t2 - t1
複製程式碼
t1~t2 時間段的 CPU 空閒使用時間:
idle = (idle2 - idle1)
複製程式碼
t1~t2 時間段的 CPU 空閒率:
idleRate = idle / t;
複製程式碼
t1~t2 時間段的 CPU 利用率:
usageRate = 1 - idleRate;
複製程式碼
上面我們對 Linux 下 CPU 利用率做一個簡單的瞭解,計算某時間段的 CPU 利用率公式可以先理解下,在下文最後會使用 Nodejs 進行實踐。
這塊可以擴充套件下,感興趣的可以嘗試下使用 shell 指令碼實現 CPU 利用率的計算。
在 Nodejs 中是如何獲取 cpu 資訊的?
Nodejs os 模組 cpus() 方法返回一個物件陣列,包含每個邏輯 CPU 核心資訊。
提個疑問,這些資料具體是怎麼獲取的?和上面 Linuv 下的 /proc/stat 有關聯嗎?帶著這些疑問只能從原始碼中一探究竟。
const os = require('os');
os.cpus();
複製程式碼
1. JS 層
lib 模組是 Node.js 對外暴露的 js 層模組程式碼,找到 os.js 檔案,以下只保留 cpus 相關核心程式碼,其中 getCPUs 是通過 internalBinding('os') 匯入。
internalBinding 就是連結 JS 層與 C++ 層的橋樑。
// https://github.com/Q-Angelo/node/blob/master/lib/os.js#L41
const {
getCPUs,
getFreeMem,
getLoadAvg,
...
} = internalBinding('os');
// https://github.com/Q-Angelo/node/blob/master/lib/os.js#L92
function cpus() {
// [] is a bugfix for a regression introduced in 51cea61
const data = getCPUs() || [];
const result = [];
let i = 0;
while (i < data.length) {
result.push({
model: data[i++],
speed: data[i++],
times: {
user: data[i++],
nice: data[i++],
sys: data[i++],
idle: data[i++],
irq: data[i++]
}
});
}
return result;
}
// https://github.com/Q-Angelo/node/blob/master/lib/os.js#L266
module.exports = {
cpus,
...
};
複製程式碼
2. C++ 層
2.1 Initialize:
C++ 層程式碼位於 src 目錄下,這一塊屬於內建模組,是給 JS 層(lib 目錄下)提供的 API,在 src/node_os.cc 檔案中有一個 Initialize 初始化操作,getCPUs 對應的則是 GetCPUInfo 方法,接下來我們就要看這個方法的實現。
// https://github.com/Q-Angelo/node/blob/master/src/node_os.cc#L390
void Initialize(Local<Object> target,
Local<Value> unused,
Local<Context> context,
void* priv) {
Environment* env = Environment::GetCurrent(context);
env->SetMethod(target, "getCPUs", GetCPUInfo);
...
target->Set(env->context(),
FIXED_ONE_BYTE_STRING(env->isolate(), "isBigEndian"),
Boolean::New(env->isolate(), IsBigEndian())).Check();
}
複製程式碼
2.2 GetCPUInfo 實現:
- 核心是在 uv_cpu_info 方法通過指標的形式傳入 &cpu_infos、&count 兩個引數拿到 cpu 的資訊和個數 count
- for 迴圈遍歷每個 CPU 核心資料,賦值給變數 ci,遍歷過程中 user、nice、sys... 這些資料就很熟悉了,正是我們在 Nodejs 中通過 os.cpus() 拿到的,這些資料都會儲存在 result 物件中
- 遍歷結束,通過 uv_free_cpu_info 對 cpu_infos、count 進行回收
- 最後,設定引數 Array::New(isolate, result.data(), result.size()) 以陣列形式返回。
// https://github.com/Q-Angelo/node/blob/master/src/node_os.cc#L113
static void GetCPUInfo(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Isolate* isolate = env->isolate();
uv_cpu_info_t* cpu_infos;
int count;
int err = uv_cpu_info(&cpu_infos, &count);
if (err)
return;
// It's faster to create an array packed with all the data and
// assemble them into objects in JS than to call Object::Set() repeatedly
// The array is in the format
// [model, speed, (5 entries of cpu_times), model2, speed2, ...]
std::vector<Local<Value>> result(count * 7);
for (int i = 0, j = 0; i < count; i++) {
uv_cpu_info_t* ci = cpu_infos + i;
result[j++] = OneByteString(isolate, ci->model);
result[j++] = Number::New(isolate, ci->speed);
result[j++] = Number::New(isolate, ci->cpu_times.user);
result[j++] = Number::New(isolate, ci->cpu_times.nice);
result[j++] = Number::New(isolate, ci->cpu_times.sys);
result[j++] = Number::New(isolate, ci->cpu_times.idle);
result[j++] = Number::New(isolate, ci->cpu_times.irq);
}
uv_free_cpu_info(cpu_infos, count);
args.GetReturnValue().Set(Array::New(isolate, result.data(), result.size()));
}
複製程式碼
3. Libuv 層
經過上面 C++ 內建模組的分析,其中一個重要的方法 uv_cpu_info 是用來獲取資料來源,現在就要找它啦
3.1 node_os.cc:
內建模組 node_os.cc 引用了標頭檔案 env-inl.h
// https://github.com/Q-Angelo/node/blob/master/src/node_os.cc#L22
#include "env-inl.h"
...
複製程式碼
3.2 env-inl.h:
env-inl.h 處又引用了 uv.h
// https://github.com/Q-Angelo/node/blob/master/src/env-inl.h#L31
#include "uv.h"
複製程式碼
3.3 uv.h:
.h(標頭檔案)包含了類裡面成員和方法的宣告,它不包含具體的實現,宣告找到了,下面找下它的具體實現。
除了我們要找的 uv_cpu_info,此處還宣告瞭 uv_free_cpu_info 方法,與之對應主要用來做回收,上文 C++ 層在資料遍歷結束就使用的這個方法對引數 cpu_infos、count 進行了回收。
/* https://github.com/Q-Angelo/node/blob/master/deps/uv/include/uv.h#L1190 */
UV_EXTERN int uv_cpu_info(uv_cpu_info_t** cpu_infos, int* count);
UV_EXTERN void uv_free_cpu_info(uv_cpu_info_t* cpu_infos, int count);
複製程式碼
Libuv 層只是對下層作業系統的一種封裝,下面來看作業系統層的實現。
4. OS 作業系統層
4.1 linux-core.c:
在 deps/uv/ 下搜尋 uv_cpu_info,會發現它的實現有很多 aix、cygwin.c、darwin.c、freebsd.c、linux-core.c 等等各種系統的,按照名字也可以看出 linux-core.c 似乎就是 Linux 下的實現了,重點也來看下這個的實現。
uv__open_file("/proc/stat") 引數 /proc/stat 這個正是 Linux 下 CPU 資訊的位置。
// https://github.com/Q-Angelo/node/blob/master/deps/uv/src/unix/linux-core.c#L610
int uv_cpu_info(uv_cpu_info_t** cpu_infos, int* count) {
unsigned int numcpus;
uv_cpu_info_t* ci;
int err;
FILE* statfile_fp;
*cpu_infos = NULL;
*count = 0;
statfile_fp = uv__open_file("/proc/stat");
...
}
複製程式碼
4.2 core.c:
最終找到 uv__open_file() 方法的實現是在 /deps/uv/src/unix/core.c 檔案,它以只讀和執行後關閉模式獲取一個檔案的指標。
到這裡也就該明白了,Linux 平臺下我們使用 Nodejs os 模組的 cpus() 方法最終也是讀取的 /proc/stat 檔案獲取的 CPU 資訊。
// https://github.com/Q-Angelo/node/blob/master/deps/uv/src/unix/core.c#L455
/* get a file pointer to a file in read-only and close-on-exec mode */
FILE* uv__open_file(const char* path) {
int fd;
FILE* fp;
fd = uv__open_cloexec(path, O_RDONLY);
if (fd < 0)
return NULL;
fp = fdopen(fd, "r");
if (fp == NULL)
uv__close(fd);
return fp;
}
複製程式碼
什麼時候該定位到 win 目錄下?什麼時候定位到 unix 目錄下?
這取決於 Libuv 層,在“深入淺出 Nodejs” 一書中有這樣一段話:“Node 在編譯期間會判斷平臺條件,選擇性編譯 unix 目錄或是 win 目錄下的原始檔到目標程式中”,所以這塊是在編譯時而非執行時來確定的。
5. 一圖勝千言
通過對 OS 模組讀取 CPU 資訊流程梳理,再次展現 Nodejs 的經典架構:
JavaScript -> internalBinding -> C++ -> Libuv -> OS
在 Nodejs 中實踐
瞭解了上面的原理之後在來 Nodejs 中實現,已經再簡單不過了,系統層為我們提供了完美的 API 呼叫。
os.cpus() 資料指標
Nodejs os.cpus() 返回的物件陣列中有一個 times 欄位,包含了 user、nice、sys、idle、irq 幾個指標資料,分別代表 CPU 在使用者模式、良好模式、系統模式、空閒模式、中斷模式下花費的毫秒數。相比 linux 下,直接通過 cat /proc/stat 檢視更直觀了。
[
{
model: 'Intel(R) Core(TM) i7 CPU 860 @ 2.80GHz',
speed: 2926,
times: {
user: 252020,
nice: 0,
sys: 30340,
idle: 1070356870,
irq: 0
}
}
...
複製程式碼
Nodejs 中編碼實踐
定義方法 _getCPUInfo 用來獲取系統 CPU 資訊。
方法 getCPUUsage 提供了 CPU 利用率的 “實時” 監控,這個 “實時” 不是絕對的實時,總會有時差的,我們下面實現中預設設定的 1 秒鐘,可通過 Options.ms 進行調整。
const os = require('os');
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
class OSUtils {
constructor() {
this.cpuUsageMSDefault = 1000; // CPU 利用率預設時間段
}
/**
* 獲取某時間段 CPU 利用率
* @param { Number } Options.ms [時間段,預設是 1000ms,即 1 秒鐘]
* @param { Boolean } Options.percentage [true(以百分比結果返回)|false]
* @returns { Promise }
*/
async getCPUUsage(options={}) {
const that = this;
let { cpuUsageMS, percentage } = options;
cpuUsageMS = cpuUsageMS || that.cpuUsageMSDefault;
const t1 = that._getCPUInfo(); // t1 時間點 CPU 資訊
await sleep(cpuUsageMS);
const t2 = that._getCPUInfo(); // t2 時間點 CPU 資訊
const idle = t2.idle - t1.idle;
const total = t2.total - t1.total;
let usage = 1 - idle / total;
if (percentage) usage = (usage * 100.0).toFixed(2) + "%";
return usage;
}
/**
* 獲取 CPU 資訊
* @returns { Object } CPU 資訊
*/
_getCPUInfo() {
const cpus = os.cpus();
let user = 0, nice = 0, sys = 0, idle = 0, irq = 0, total = 0;
for (let cpu in cpus) {
const times = cpus[cpu].times;
user += times.user;
nice += times.nice;
sys += times.sys;
idle += times.idle;
irq += times.irq;
}
total += user + nice + sys + idle + irq;
return {
user,
sys,
idle,
total,
}
}
}
複製程式碼
使用方式如下所示:
const cpuUsage = await osUtils.getCPUUsage({ percentage: true });
console.log('CPU 利用率:', cpuUsage) // CPU 利用率: 13.72%
複製程式碼
總結
本文先從 Linux 下 CPU 利用率的概念做一個簡單的講解,之後深入 Nodejs OS 模組原始碼對獲取系統 CPU 資訊進行了梳理,另一方面也再次呈現了 Nodejs 經典的架構 JavaScript -> internalBinding -> C++ -> Libuv -> OS 這對於梳理其它 API 是通用的,可以做為一定的參考,最後使用 Nodejs 對 CPU 利用率的計算進行了實踐。