从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/

相关文章
相关标签/搜索