从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系统)。索性我登录线上又执行了一遍。于是看到了如下信息:
果然获取到了每个CPU核心的信息。我们知道nodejs中os
模块有个os.cpus()
api也可以获取同样的信息,在mac上具体输出如下:
可以看到node输出的信息可读性高许多。那么有几个问题来了:
- nodejs 是怎么提供跨平台api获取到包括mac os & linux等系统机器的CPU信息呢
- 命令
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实现的一个典型的例子。典型在哪里呢,可以看下面图:
可以看到我们业务代码通过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/