深入 Nodejs 原始碼探究 CPU 資訊的獲取與利用率計算

五月君發表於2020-04-06

在 Linux 下我們通過 top 或者 htop 命令可以看到當前的 CPU 資源利用率,另外在一些監控工具中你可能也遇見過,那麼它是如何計算的呢?在 Nodejs 中我們該如何實現?

帶著這些疑問,本節會先從 Linux 下的 CPU 利用率進行一個簡單講解做一下前置知識鋪墊,之後會深入 Nodejs 原始碼,去探討如何獲取 CPU 資訊及計算 CPU 某時間段的利用率。

開始之前,可以先看一張圖,它展示了 Nodejs OS 模組讀取系統 CPU 資訊的整個過程呼叫,在下文中也會詳細講解,你會再次看到它。

深入 Nodejs 原始碼探究 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 原始碼探究 CPU 資訊的獲取與利用率計算

在 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 利用率的計算進行了實踐。

Reference

相關文章