这几天和小伙伴在研究怎么用nodejs来监控机器的硬件信息,其中有项是要计算CPU的剩余idle信息,第一时间想到用 top
命令, 可以直接获取当前机器的硬件信息。本着好奇查了下top命令计算CPU idle的原理,具体可以 参见这里 。简单总结就是通过查询 /proc/stat
文件获取每个核的信息,然后通过计算得出总的剩余idle。既然是通过查询 /proc/stat
来获取的信息,那我是不是可以手动执行下 cat /proc/stat
命令一探究竟,然并卵,item提示没有此文件(mac os系统)。索性我登录线上又执行了一遍。于是看到了如下信息:
os
模块有个 os.cpus()
api也可以获取同样的信息,在mac上具体输出如下: 可以看到node输出的信息可读性高许多。那么有几个问题来了:
1. nodejs 是怎么提供跨平台api获取到包括mac os & linux等系统机器的CPU信息呢
2. 命令 cat /proc/stat
输出的第一行CPU总的信息为什么在nodejs输出中没有体现呢
其中第一个问题可以详细的描述为: 虽然我们知道nodejs是通过libuv这个库(用C实现)实现跨平台的,那么我们还是想看看js是怎么C通信的和在不同的平台是怎么获取CPU信息的。带着这些问题我打开了之前下好的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
文件。
有了上面的基本的知识之后我们可以首先打开lib目录找到 os.js
文件。果然在代码里面找到了这样的代码:
```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模块,观察文件最后发现有个初始化的函数具体如下:
c++
void Initialize(Local target,
Local unused, Local 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函数看看了。
c++
static void GetCPUInfo(const FunctionCallbackInfo& 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 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文件:
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`函数如下:
c++
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)的形式来实现的。
c++
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/ >