從os.cpus()來分析nodejs源碼結構

這幾天和小夥伴在研究怎麼用nodejs來監控機器的硬件信息,其中有項是要計算CPU的剩餘idle信息,第一時間想到用top命令, 能夠直接獲取當前機器的硬件信息。本着好奇查了下top命令計算CPU idle的原理,具體能夠參見這裏。簡單總結就是經過查詢/proc/stat文件獲取每一個核的信息,而後經過計算得出總的剩餘idle。既然是經過查詢/proc/stat來獲取的信息,那我是否是能夠手動執行下cat /proc/stat命令一探究竟,然並卵,item提示沒有此文件(mac os系統)。索性我登陸線上又執行了一遍。因而看到了以下信息:node

圖片描述

果真獲取到了每一個CPU核心的信息。咱們知道nodejs中os模塊有個os.cpus()api也能夠獲取一樣的信息,在mac上具體輸出以下:linux

圖片描述

能夠看到node輸出的信息可讀性高許多。那麼有幾個問題來了:c++

  1. nodejs 是怎麼提供跨平臺api獲取到包括mac os & linux等系統機器的CPU信息呢macos

  2. 命令cat /proc/stat輸出的第一行CPU總的信息爲何在nodejs輸出中沒有體現呢windows

其中第一個問題能夠詳細的描述爲: 雖然咱們知道nodejs是經過libuv這個庫(用C實現)實現跨平臺的,那麼咱們仍是想看看js是怎麼C通訊的和在不一樣的平臺是怎麼獲取CPU信息的。帶着這些問題我打開了以前下好的nodejs源碼,打算一探究竟。api

nodejs目錄簡介

在進入具體的os.cpus()api閱讀以前,咱們先簡單介紹下nodejs的幾個重要的目錄:數組

.
├── AUTHORS
├── BSDmakefile    //bsd平臺makefile文件
├── LICENSE
├── Makefile       //linux平臺makefile文件
├── common.gypi
├── config.gypi
├── config.mk
├── configure
├── deps            //nodejs的依賴
├── lib             //nodejs的js核心模塊
├── node -> out/Release/node
├── node.gyp        //node-gyp構建編譯任務的配置文件
├── src             //nodejs的c++內建模塊
├── test
├── tools
└── vcbuild.bat   //win平臺makefile文件

其中lib目錄是咱們nodejs對外暴露的js模塊源碼,這部分熟悉nodejs同窗應該很親切。 咱們知道有些模塊好比http & OS模塊是經過js封裝了C++的實現方式對外提供的api。而這部分的C++的代碼就放在src目錄下。咱們還知道nodejs實際上是基於V8引擎運行和libuv實現跨平臺的,對於這部分的依賴是放在deps目錄中的,而其餘帶makefile字樣爲名字的文件大都是針對不一樣的平臺的編譯文件,而組織這些編譯任務的是node-gyp工具,其配置文件對應就是node.gyp文件。函數

os.js核心模塊

有了上面的基本的知識以後咱們能夠首先打開lib目錄找到os.js文件。果真在代碼裏面找到了這樣的代碼:工具

'use strict';

const binding = process.binding('os');
const internalUtil = require('internal/util');
const isWindows = process.platform === 'win32';

exports.hostname = binding.getHostname;
exports.loadavg = binding.getLoadAvg;
exports.uptime = binding.getUptime;
exports.freemem = binding.getFreeMem;
exports.totalmem = binding.getTotalMem;
exports.cpus = binding.getCPUs;
exports.type = binding.getOSType;
exports.release = binding.getOSRelease;
exports.networkInterfaces = binding.getInterfaceAddresses;
exports.homedir = binding.getHomeDirectory;

能夠很清楚的知道os.cpus()api只經過binding對象獲取的,而binding對象又是經過上面的process.binding('os')導入的。通過一番查證,這個process.binding就是js調用C++代碼的關鍵所在。結合這兩句能夠明確的知道這個方法就是直接經過C++內建模塊直接導出的一個api。學習

node_os.cc內建模塊

經過src目錄找到node_os.cc模塊,觀察文件最後發現有個初始化的函數具體以下:

void Initialize(Local<Object> target,
                Local<Value> unused,
                Local<Context> context) {
      Environment* env = Environment::GetCurrent(context);
      env->SetMethod(target, "getHostname", GetHostname);
      env->SetMethod(target, "getLoadAvg", GetLoadAvg);
      env->SetMethod(target, "getUptime", GetUptime);
      env->SetMethod(target, "getTotalMem", GetTotalMemory);
      env->SetMethod(target, "getFreeMem", GetFreeMemory);
      env->SetMethod(target, "getCPUs", GetCPUInfo);
      env->SetMethod(target, "getOSType", GetOSType);
      env->SetMethod(target, "getOSRelease", GetOSRelease);
      env->SetMethod(target, "getInterfaceAddresses", GetInterfaceAddresses);
      env->SetMethod(target, "getHomeDirectory", GetHomeDirectory);
      target->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "isBigEndian"),
                  Boolean::New(env->isolate(), IsBigEndian()));
}

說明getCPUs函數其實就是GetCPUInfo函數,因而就能夠愉快的找到GetCPUInfo函數看看了。

static void GetCPUInfo(const FunctionCallbackInfo<Value>& args) {
  Environment* env = Environment::GetCurrent(args);
  uv_cpu_info_t* cpu_infos;
  int count, i;

  int err = uv_cpu_info(&cpu_infos, &count);
  if (err)
    return;

  Local<Array> cpus = Array::New(env->isolate());
  for (i = 0; i < count; i++) {
    uv_cpu_info_t* ci = cpu_infos + i;

    Local<Object> times_info = Object::New(env->isolate());
    times_info->Set(env->user_string(),
                    Number::New(env->isolate(), ci->cpu_times.user));
    times_info->Set(env->nice_string(),
                    Number::New(env->isolate(), ci->cpu_times.nice));
    times_info->Set(env->sys_string(),
                    Number::New(env->isolate(), ci->cpu_times.sys));
    times_info->Set(env->idle_string(),
                    Number::New(env->isolate(), ci->cpu_times.idle));
    times_info->Set(env->irq_string(),
                    Number::New(env->isolate(), ci->cpu_times.irq));

    Local<Object> cpu_info = Object::New(env->isolate());
    cpu_info->Set(env->model_string(),
                  OneByteString(env->isolate(), ci->model));
    cpu_info->Set(env->speed_string(),
                  Number::New(env->isolate(), ci->speed));
    cpu_info->Set(env->times_string(), times_info);

    (*cpus)->Set(i, cpu_info);
  }

  uv_free_cpu_info(cpu_infos, count);
  args.GetReturnValue().Set(cpus);
}

能夠看到這個函數的大概意思是

  • 先經過調用uv_cpu_info以指針的形式傳入參數,獲取到全部的cpu的信息,並判斷錯誤碼,有錯誤直接退出

  • 建立一個新的數組cpus。

  • 經過遍歷循環(count應該就是第一步獲取到的cpu的核數)每一個核心的信息,存儲在ci對象中。

  • 循環中建立了一個times_info對象存儲每一個cpu核心的times信息(包括user, sys, nice, idle等)。

  • 而且還建立一個cpu_info對象來存儲ci的model信息,speed信息和上一步中的times_info信息。

  • 而後把cpu_info放入到數組cpus中。

  • 最後釋放cpu_infos 和count對象。而且把數組經過設置到參數的形式返回出去。

其實看到這裏os.cpu()這個api的面目已經差很少了,而且對應到開頭在node RLPE環境中執行輸出的結果也能夠跟這裏一一對應上了。最後問題就落在第一步中的 uv_cpu_info函數上了,這個函數是全部cpu信息的來源。那麼這個函數在哪裏呢?

libuv模塊

經過搜索能夠查到這個uv_cpu_info函數來自deps/uv/目錄中,而且存在多份定義,在sunos.c, netbsd.c linux-core.c, freebsd.c,darwin.c, utils.c中都存在定義,想必這就是libuv的真實面目了吧,針對不一樣的平臺實現了統一的api。而後被nodejs的C++內建模塊調用,最後經過js模塊暴露一個簡單的os.cpu()api。看到這裏應該對剛開始的第一個問題有一個答案了。那麼這麼多xxxbsd又是啥呢?查了下原來是unix的不一樣的發行版本,而sunos應該是原來SUN公司搞的那個系統,而linux系分支應該是沒有疑問的是linux-core了。而utils.c發現是windows的實現, 而darwin.c應該mac os的實現,其實跟xxxbsd實現很類似。那咱們先看下最好理解的linux-core.c文件:

int uv_cpu_info(uv_cpu_info_t** cpu_infos, int* count) {
      
      // *** some code....****
      err = read_models(numcpus, ci);
      if (err == 0)
        err = read_times(numcpus, ci);  

      // *** some code....****
}

能夠看到read_times函數獲取的cpu times信息。因而找到read_times函數以下:

static int read_times(unsigned int numcpus, uv_cpu_info_t* ci) {        
          //  *** some code....****
          fp = fopen("/proc/stat", "r");
          if (fp == NULL)
            return -errno;
        
          if (!fgets(buf, sizeof(buf), fp))
            abort();
        
          num = 0;
        
          while (fgets(buf, sizeof(buf), fp)) {
            if (num >= numcpus)
              break;
        
            if (strncmp(buf, "cpu", 3))
              break;
        
          //  *** some code....****
            ts.user = clock_ticks * user;
            ts.nice = clock_ticks * nice;
            ts.sys  = clock_ticks * sys;
            ts.idle = clock_ticks * idle;
            ts.irq  = clock_ticks * irq;
            ci[num++].cpu_times = ts;
          }
          fclose(fp);
          assert(num == numcpus);
          return 0;
}

看到了fp = fopen("/proc/stat", "r");這句是否是豁然開朗,這不就是咱們在開頭說的top命令實現原理查看的文件麼,nodejs在linux平臺的實現也是經過讀這個文件獲取的cpu信息的呢。經過while循環逐行獲取文件信息,注意到if (strncmp(buf, "cpu", 3))這句代碼,經過函數名能夠猜出這個一個字符串比較的函數,通過查證果真是吧當前在buf中的字符串的前三個字符跟字符串'cpu'比較若是相等救你直接break跳過這同樣,這就回答了咱們前面提到的第二個問題。因此跳過了/proc/stat文件的第一行,而直接獲取了每一個核的單獨信息。至此就是完整的os.cpus()api的linux實現了。

再看darwin.c中的uv_cpu_info函數實現,能夠看到並無經過/proc/stat文件來實現(macos也不存在這個文件)。而是經過系統調用(sysctlbyname, host_processor_info)的形式來實現的。

int uv_cpu_info(uv_cpu_info_t** cpu_infos, int* count) {
 //  *** some code....****
  size = sizeof(model);
  if (sysctlbyname("machdep.cpu.brand_string", &model, &size, NULL, 0) &&
      sysctlbyname("hw.model", &model, &size, NULL, 0)) {
    return -errno;
  }

  size = sizeof(cpuspeed);
  if (sysctlbyname("hw.cpufrequency", &cpuspeed, &size, NULL, 0))
    return -errno;

  if (host_processor_info(mach_host_self(), PROCESSOR_CPU_LOAD_INFO, &numcpus,
                          (processor_info_array_t*)&info,
                          &msg_type) != KERN_SUCCESS) {
    return -EINVAL;  /* FIXME(bnoordhuis) Translate error. */
  }
   //  *** some code....****
}

到這裏其實就大概能夠看到 os.cpus()實現的全貌了。

總結

雖然這個 os.cpus()api很簡單,可是它的實現確是nodejs實現的一個典型的例子。典型在哪裏呢,能夠看下面圖:

圖片描述

能夠看到咱們業務代碼經過require導入nodejs核心的js模塊,核心js模塊經過process.binding的方式導入C++內建模塊。而C++內建模塊在處理有平臺的兼容性的功能時又是經過libuv來實現的。libuv其實就是針對不一樣平臺實現功能後提供的統一的api封裝給上層調用,具體調用哪一個平臺的api這個應該是在編譯nodejs的時候就決定的,不是在運行時判斷的,這個流程適用於nodejs中不少地方。初次探索nodejs源碼,有疏漏之處懇請指出,記錄於此,也以便以後更深刻的學習。

原文地址:http://blog.fexnotes.com/2016/01/18/nodejs-source-intro/

相關文章
相關標籤/搜索