深入 Nodejs 源码探究 CPU 信息的获取与利用率计算 - 图1
在 Linux 下我们通过 top 或者 htop 命令可以看到当前的 CPU 资源利用率,另外在一些监控工具中你可能也遇见过,那么它是如何计算的呢?在 Nodejs 中我们该如何实现?

带着这些疑问,本节会先从 Linux 下的 CPU 利用率进行一个简单讲解做一下前置知识铺垫,之后会深入 Nodejs 源码,去探讨如何获取 CPU 信息及计算 CPU 某时间段的利用率。

开始之前,可以先看一张图,它展示了 Nodejs OS 模块读取系统 CPU 信息的整个过程调用,在下文中也会详细讲解,你会再次看到它。

深入 Nodejs 源码探究 CPU 信息的获取与利用率计算 - 图2

Linux 下 CPU 利用率

Linux 下 CPU 的利用率分为用户态(用户模式下执行时间)、系统态(系统内核执行)、空闲态(空闲系统进程执行时间),三者相加为 CPU 执行总时间,关于 CPU 的活动信息我们可以在 /proc/stat 文件查看。

CPU 利用率是指非系统空闲进程 / CPU 总执行时间

  1. > cat /proc/stat
  2. cpu 2255 34 2290 22625563 6290 127 456
  3. cpu0 1132 34 1441 11311718 3675 127 438
  4. cpu1 1123 0 849 11313845 2614 0 18
  5. intr 114930548 113199788 3 0 5 263 0 4 [... lots more numbers ...]
  6. ctxt 1990473 # 自系统启动以来 CPU 发生的上下文交换次数
  7. btime 1062191376 # 启动到现在为止的时间,单位为秒
  8. processes 2915 # 系统启动以来所创建的任务数目
  9. procs_running 1 # 当前运行队列的任务数目
  10. procs_blocked 0 # 当前被阻塞的任务数目

上面第一行 cpu 表示总的 CPU 使用情况,下面的cpu0、cpu1 是指系统的每个 CPU 核心数运行情况(cpu0 + cpu1 + cpuN = cpu 总的核心数),我们看下第一行的含义。

  • user:系统启动开始累计到当前时刻,用户态的 CPU 时间(单位:jiffies),不包含 nice 值为负的进程。
  • nice:系统启动开始累计到当前时刻,nice 值为负的进程所占用的 CPU 时间。
  • system:系统启动开始累计到当前时刻,核心时间
  • idle:从系统启动开始累计到当前时刻,除硬盘IO等待时间以外其它等待时间
  • iowait:从系统启动开始累计到当前时刻,硬盘IO等待时间
  • irq:从系统启动开始累计到当前时刻,硬中断时间
  • softirq:从系统启动开始累计到当前时刻,软中断时间

关于 /proc/stat 的介绍,参考这里 http://www.linuxhowtos.org/System/procstat.htm

CPU 某时间段利用率公式

/proc/stat 文件下展示的是系统从启动到当下所累加的总的 CPU 时间,如果要计算 CPU 在某个时间段的利用率,则需要取 t1、t2 两个时间点进行运算

t1~t2 时间段的 CPU 执行时间:

  1. t1 = (user1 + nice1 + system1 + idle1 + iowait1 + irq1 + softirq1)
  2. t2 = (user2 + nice2 + system2 + idle2 + iowait2 + irq2 + softirq2)
  3. t = t2 - t1

t1~t2 时间段的 CPU 空闲使用时间:

  1. idle = (idle2 - idle1)

t1~t2 时间段的 CPU 空闲率:

  1. idleRate = idle / t;

t1~t2 时间段的 CPU 利用率:

  1. usageRate = 1 - idleRate;

上面我们对 Linux 下 CPU 利用率做一个简单的了解,计算某时间段的 CPU 利用率公式可以先理解下,在下文最后会使用 Nodejs 进行实践。

这块可以扩展下,感兴趣的可以尝试下使用 shell 脚本实现 CPU 利用率的计算。

在 Nodejs 中是如何获取 cpu 信息的?

Nodejs os 模块 cpus() 方法返回一个对象数组,包含每个逻辑 CPU 内核信息。

提个疑问,这些数据具体是怎么获取的?和上面 Linuv 下的 /proc/stat 有关联吗?带着这些疑问只能从源码中一探究竟。

  1. const os = require('os');
  2. os.cpus();

1. JS 层

lib 模块是 Node.js 对外暴露的 js 层模块代码,找到 os.js 文件,以下只保留 cpus 相关核心代码,其中 getCPUs 是通过 internalBinding(‘os’) 导入。

internalBinding 就是链接 JS 层与 C++ 层的桥梁。

  1. // https://github.com/Q-Angelo/node/blob/master/lib/os.js#L41
  2. const {
  3. getCPUs,
  4. getFreeMem,
  5. getLoadAvg,
  6. ...
  7. } = internalBinding('os');
  8. // https://github.com/Q-Angelo/node/blob/master/lib/os.js#L92
  9. function cpus() {
  10. // [] is a bugfix for a regression introduced in 51cea61
  11. const data = getCPUs() || [];
  12. const result = [];
  13. let i = 0;
  14. while (i < data.length) {
  15. result.push({
  16. model: data[i++],
  17. speed: data[i++],
  18. times: {
  19. user: data[i++],
  20. nice: data[i++],
  21. sys: data[i++],
  22. idle: data[i++],
  23. irq: data[i++]
  24. }
  25. });
  26. }
  27. return result;
  28. }
  29. // https://github.com/Q-Angelo/node/blob/master/lib/os.js#L266
  30. module.exports = {
  31. cpus,
  32. ...
  33. };

2. C++ 层

2.1 Initialize:

C++ 层代码位于 src 目录下,这一块属于内建模块,是给 JS 层(lib 目录下)提供的 API,在 src/node_os.cc 文件中有一个 Initialize 初始化操作,getCPUs 对应的则是 GetCPUInfo 方法,接下来我们就要看这个方法的实现。

  1. // https://github.com/Q-Angelo/node/blob/master/src/node_os.cc#L390
  2. void Initialize(Local<Object> target,
  3. Local<Value> unused,
  4. Local<Context> context,
  5. void* priv) {
  6. Environment* env = Environment::GetCurrent(context);
  7. env->SetMethod(target, "getCPUs", GetCPUInfo);
  8. ...
  9. target->Set(env->context(),
  10. FIXED_ONE_BYTE_STRING(env->isolate(), "isBigEndian"),
  11. Boolean::New(env->isolate(), IsBigEndian())).Check();
  12. }

2.2 GetCPUInfo 实现:

  • 核心是在 uv_cpu_info 方法通过指针的形式传入 &cpu_infos、&count 两个参数拿到 cpu 的信息和个数 count
  • for 循环遍历每个 CPU 核心数据,赋值给变量 ci,遍历过程中 user、nice、sys… 这些数据就很熟悉了,正是我们在 Nodejs 中通过 os.cpus() 拿到的,这些数据都会保存在 result 对象中
  • 遍历结束,通过 uv_free_cpu_info 对 cpu_infos、count 进行回收
  • 最后,设置参数 Array::New(isolate, result.data(), result.size()) 以数组形式返回。
  1. // https://github.com/Q-Angelo/node/blob/master/src/node_os.cc#L113
  2. static void GetCPUInfo(const FunctionCallbackInfo<Value>& args) {
  3. Environment* env = Environment::GetCurrent(args);
  4. Isolate* isolate = env->isolate();
  5. uv_cpu_info_t* cpu_infos;
  6. int count;
  7. int err = uv_cpu_info(&cpu_infos, &count);
  8. if (err)
  9. return;
  10. // It's faster to create an array packed with all the data and
  11. // assemble them into objects in JS than to call Object::Set() repeatedly
  12. // The array is in the format
  13. // [model, speed, (5 entries of cpu_times), model2, speed2, ...]
  14. std::vector<Local<Value>> result(count * 7);
  15. for (int i = 0, j = 0; i < count; i++) {
  16. uv_cpu_info_t* ci = cpu_infos + i;
  17. result[j++] = OneByteString(isolate, ci->model);
  18. result[j++] = Number::New(isolate, ci->speed);
  19. result[j++] = Number::New(isolate, ci->cpu_times.user);
  20. result[j++] = Number::New(isolate, ci->cpu_times.nice);
  21. result[j++] = Number::New(isolate, ci->cpu_times.sys);
  22. result[j++] = Number::New(isolate, ci->cpu_times.idle);
  23. result[j++] = Number::New(isolate, ci->cpu_times.irq);
  24. }
  25. uv_free_cpu_info(cpu_infos, count);
  26. args.GetReturnValue().Set(Array::New(isolate, result.data(), result.size()));
  27. }

3. Libuv 层

经过上面 C++ 内建模块的分析,其中一个重要的方法 uv_cpu_info 是用来获取数据源,现在就要找它啦

3.1 node_os.cc:

内建模块 node_os.cc 引用了头文件 env-inl.h

  1. // https://github.com/Q-Angelo/node/blob/master/src/node_os.cc#L22
  2. #include "env-inl.h"
  3. ...

3.2 env-inl.h:

env-inl.h 处又引用了 uv.h

  1. // https://github.com/Q-Angelo/node/blob/master/src/env-inl.h#L31
  2. #include "uv.h"

3.3 uv.h:

.h(头文件)包含了类里面成员和方法的声明,它不包含具体的实现,声明找到了,下面找下它的具体实现。

除了我们要找的 uv_cpu_info,此处还声明了 uv_free_cpu_info 方法,与之对应主要用来做回收,上文 C++ 层在数据遍历结束就使用的这个方法对参数 cpu_infos、count 进行了回收。

  1. /* https://github.com/Q-Angelo/node/blob/master/deps/uv/include/uv.h#L1190 */
  2. UV_EXTERN int uv_cpu_info(uv_cpu_info_t** cpu_infos, int* count);
  3. UV_EXTERN void uv_free_cpu_info(uv_cpu_info_t* cpu_infos, int count);

Libuv 层只是对下层操作系统的一种封装,下面来看操作系统层的实现。

4. OS 操作系统层

4.1 linux-core.c:

在 deps/uv/ 下搜索 uv_cpu_info,会发现它的实现有很多 aix、cygwin.c、darwin.c、freebsd.c、linux-core.c 等等各种系统的,按照名字也可以看出 linux-core.c 似乎就是 Linux 下的实现了,重点也来看下这个的实现。

uv__open_file(“/proc/stat”) 参数 /proc/stat 这个正是 Linux 下 CPU 信息的位置

  1. // https://github.com/Q-Angelo/node/blob/master/deps/uv/src/unix/linux-core.c#L610
  2. int uv_cpu_info(uv_cpu_info_t** cpu_infos, int* count) {
  3. unsigned int numcpus;
  4. uv_cpu_info_t* ci;
  5. int err;
  6. FILE* statfile_fp;
  7. *cpu_infos = NULL;
  8. *count = 0;
  9. statfile_fp = uv__open_file("/proc/stat");
  10. ...
  11. }

4.2 core.c:

最终找到 uv__open_file() 方法的实现是在 /deps/uv/src/unix/core.c 文件,它以只读和执行后关闭模式获取一个文件的指针。

到这里也就该明白了,Linux 平台下我们使用 Nodejs os 模块的 cpus() 方法最终也是读取的 /proc/stat 文件获取的 CPU 信息。

  1. // https://github.com/Q-Angelo/node/blob/master/deps/uv/src/unix/core.c#L455
  2. /* get a file pointer to a file in read-only and close-on-exec mode */
  3. FILE* uv__open_file(const char* path) {
  4. int fd;
  5. FILE* fp;
  6. fd = uv__open_cloexec(path, O_RDONLY);
  7. if (fd < 0)
  8. return NULL;
  9. fp = fdopen(fd, "r");
  10. if (fp == NULL)
  11. uv__close(fd);
  12. return fp;
  13. }

什么时候该定位到 win 目录下?什么时候定位到 unix 目录下?

这取决于 Libuv 层,在“深入浅出 Nodejs” 一书中有这样一段话:“Node 在编译期间会判断平台条件,选择性编译 unix 目录或是 win 目录下的源文件到目标程序中”,所以这块是在编译时而非运行时来确定的

5. 一图胜千言

通过对 OS 模块读取 CPU 信息流程梳理,再次展现 Nodejs 的经典架构:

JavaScript -> internalBinding -> C++ -> Libuv -> OS

深入 Nodejs 源码探究 CPU 信息的获取与利用率计算 - 图3

在 Nodejs 中实践

了解了上面的原理之后在来 Nodejs 中实现,已经再简单不过了,系统层为我们提供了完美的 API 调用。

os.cpus() 数据指标

Nodejs os.cpus() 返回的对象数组中有一个 times 字段,包含了 user、nice、sys、idle、irq 几个指标数据,分别代表 CPU 在用户模式、良好模式、系统模式、空闲模式、中断模式下花费的毫秒数。相比 linux 下,直接通过 cat /proc/stat 查看更直观了。

  1. [
  2. {
  3. model: 'Intel(R) Core(TM) i7 CPU 860 @ 2.80GHz',
  4. speed: 2926,
  5. times: {
  6. user: 252020,
  7. nice: 0,
  8. sys: 30340,
  9. idle: 1070356870,
  10. irq: 0
  11. }
  12. }
  13. ...

Nodejs 中编码实践

定义方法 _getCPUInfo 用来获取系统 CPU 信息。

方法 getCPUUsage 提供了 CPU 利用率的 “实时” 监控,这个 “实时” 不是绝对的实时,总会有时差的,我们下面实现中默认设置的 1 秒钟,可通过 Options.ms 进行调整。

  1. const os = require('os');
  2. const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
  3. class OSUtils {
  4. constructor() {
  5. this.cpuUsageMSDefault = 1000; // CPU 利用率默认时间段
  6. }
  7. /**
  8. * 获取某时间段 CPU 利用率
  9. * @param { Number } Options.ms [时间段,默认是 1000ms,即 1 秒钟]
  10. * @param { Boolean } Options.percentage [true(以百分比结果返回)|false]
  11. * @returns { Promise }
  12. */
  13. async getCPUUsage(options={}) {
  14. const that = this;
  15. let { cpuUsageMS, percentage } = options;
  16. cpuUsageMS = cpuUsageMS || that.cpuUsageMSDefault;
  17. const t1 = that._getCPUInfo(); // t1 时间点 CPU 信息
  18. await sleep(cpuUsageMS);
  19. const t2 = that._getCPUInfo(); // t2 时间点 CPU 信息
  20. const idle = t2.idle - t1.idle;
  21. const total = t2.total - t1.total;
  22. let usage = 1 - idle / total;
  23. if (percentage) usage = (usage * 100.0).toFixed(2) + "%";
  24. return usage;
  25. }
  26. /**
  27. * 获取 CPU 信息
  28. * @returns { Object } CPU 信息
  29. */
  30. _getCPUInfo() {
  31. const cpus = os.cpus();
  32. let user = 0, nice = 0, sys = 0, idle = 0, irq = 0, total = 0;
  33. for (let cpu in cpus) {
  34. const times = cpus[cpu].times;
  35. user += times.user;
  36. nice += times.nice;
  37. sys += times.sys;
  38. idle += times.idle;
  39. irq += times.irq;
  40. }
  41. total += user + nice + sys + idle + irq;
  42. return {
  43. user,
  44. sys,
  45. idle,
  46. total,
  47. }
  48. }
  49. }

使用方式如下所示:

  1. const cpuUsage = await osUtils.getCPUUsage({ percentage: true });
  2. console.log('CPU 利用率:', cpuUsage) // CPU 利用率: 13.72%

总结

本文先从 Linux 下 CPU 利用率的概念做一个简单的讲解,之后深入 Nodejs OS 模块源码对获取系统 CPU 信息进行了梳理,另一方面也再次呈现了 Nodejs 经典的架构 JavaScript -> internalBinding -> C++ -> Libuv -> OS 这对于梳理其它 API 是通用的,可以做为一定的参考,最后使用 Nodejs 对 CPU 利用率的计算进行了实践。

Reference