在 Linux 下咱們經過 top 或者 htop 命令能夠看到當前的 CPU 資源利用率,另外在一些監控工具中你可能也碰見過,那麼它是如何計算的呢?在 Nodejs 中咱們該如何實現?html
帶着這些疑問,本節會先從 Linux 下的 CPU 利用率進行一個簡單講解作一下前置知識鋪墊,以後會深刻 Nodejs 源碼,去探討如何獲取 CPU 信息及計算 CPU 某時間段的利用率。node
開始以前,能夠先看一張圖,它展現了 Nodejs OS 模塊讀取系統 CPU 信息的整個過程調用,在下文中也會詳細講解,你會再次看到它。linux
Linux 下 CPU 的利用率分爲用戶態(用戶模式下執行時間)、系統態(系統內核執行)、空閒態(空閒系統進程執行時間),三者相加爲 CPU 執行總時間,關於 CPU 的活動信息咱們能夠在 /proc/stat 文件查看。c++
CPU 利用率是指非系統空閒進程 / CPU 總執行時間。git
> 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 總的核心數),咱們看下第一行的含義。github
關於 /proc/stat 的介紹,參考這裏 www.linuxhowtos.org/System/proc…shell
/proc/stat 文件下展現的是系統從啓動到當下所累加的總的 CPU 時間,若是要計算 CPU 在某個時間段的利用率,則須要取 t一、t2 兩個時間點進行運算。c#
t1~t2 時間段的 CPU 執行時間:api
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 os 模塊 cpus() 方法返回一個對象數組,包含每一個邏輯 CPU 內核信息。
提個疑問,這些數據具體是怎麼獲取的?和上面 Linuv 下的 /proc/stat 有關聯嗎?帶着這些疑問只能從源碼中一探究竟。
const os = require('os');
os.cpus();
複製代碼
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.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 實現:
// 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()));
}
複製代碼
通過上面 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.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 目錄下的源文件到目標程序中」,因此這塊是在編譯時而非運行時來肯定的。
經過對 OS 模塊讀取 CPU 信息流程梳理,再次展示 Nodejs 的經典架構:
JavaScript -> internalBinding -> C++ -> Libuv -> OS
瞭解了上面的原理以後在來 Nodejs 中實現,已經再簡單不過了,系統層爲咱們提供了完美的 API 調用。
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
}
}
...
複製代碼
定義方法 _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 利用率的計算進行了實踐。