深刻 Nodejs 源碼探究 CPU 信息的獲取與利用率計算

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

帶着這些疑問,本節會先從 Linux 下的 CPU 利用率進行一個簡單講解作一下前置知識鋪墊,以後會深刻 Nodejs 源碼,去探討如何獲取 CPU 信息及計算 CPU 某時間段的利用率。node

開始以前,能夠先看一張圖,它展現了 Nodejs OS 模塊讀取系統 CPU 信息的整個過程調用,在下文中也會詳細講解,你會再次看到它。linux

Linux 下 CPU 利用率

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

  • user:系統啓動開始累計到當前時刻,用戶態的 CPU 時間(單位:jiffies),不包含 nice 值爲負的進程。
  • nice:系統啓動開始累計到當前時刻,nice 值爲負的進程所佔用的 CPU 時間。
  • system:系統啓動開始累計到當前時刻,核心時間
  • idle:從系統啓動開始累計到當前時刻,除硬盤IO等待時間之外其它等待時間
  • iowait:從系統啓動開始累計到當前時刻,硬盤IO等待時間
  • irq:從系統啓動開始累計到當前時刻,硬中斷時間
  • softirq:從系統啓動開始累計到當前時刻,軟中斷時間

關於 /proc/stat 的介紹,參考這裏 www.linuxhowtos.org/System/proc…shell

CPU 某時間段利用率公式

/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 中是如何獲取 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 利用率的計算進行了實踐。

Reference

相關文章
相關標籤/搜索