精华 从os.cpus()来分析nodejs源码结构
发布于 2 年前 作者 rockcoder23 2971 次浏览 最后一次编辑是 1 年前 来自 分享

从os.cpus()来分析nodejs源码结构

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

这几天和小伙伴在研究怎么用nodejs来监控机器的硬件信息,其中有项是要计算CPU的剩余idle信息,第一时间想到用top命令, 可以直接获取当前机器的硬件信息。本着好奇查了下top命令计算CPU idle的原理,具体可以参见这里。简单总结就是通过查询/proc/stat文件获取每个核的信息,然后通过计算得出总的剩余idle。既然是通过查询/proc/stat来获取的信息,那我是不是可以手动执行下cat /proc/stat命令一探究竟,然并卵,item提示没有此文件(mac os系统)。索性我登录线上又执行了一遍。于是看到了如下信息: cat-proc-stat.png 果然获取到了每个CPU核心的信息。我们知道nodejs中os模块有个os.cpus()api也可以获取同样的信息,在mac上具体输出如下: os-cpus.png 可以看到node输出的信息可读性高许多。那么有几个问题来了:

  1. nodejs 是怎么提供跨平台api获取到包括mac os & linux等系统机器的CPU信息呢
  2. 命令cat /proc/stat输出的第一行CPU总的信息为什么在nodejs输出中没有体现呢

其中第一个问题可以详细的描述为: 虽然我们知道nodejs是通过libuv这个库(用C实现)实现跨平台的,那么我们还是想看看js是怎么C通信的和在不同的平台是怎么获取CPU信息的。带着这些问题我打开了之前下好的nodejs源码,打算一探究竟。

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实现的一个典型的例子。典型在哪里呢,可以看下面图: nodejs-struct.png 可以看到我们业务代码通过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/

13 回复

有兴致 ^_^ +1

这图片竟然是用的印象笔记的地址。。挂了

thks, @alsotang 已更换成本站地址,之前偷懒从印象笔记复制过来的 : )

赞。Node.js就是这么搞的。JavaScript -> V8 binding -> libuv API -> (OS API)

@JacksonTian 哈哈,谢谢。也是受《深入浅出nodejs》启蒙的,虽然没有找大大开过光。

@rockcoder23 开光,看来朴大技多不压身,还兼职这个- -||

赞,写得很清晰明了

相当之有逻辑,不错

回到顶部