nodejs源码—初始化

栏目: C++ · 发布时间: 5年前

内容简介:相信很多的人,每天在终端不止一遍的执行着这个问题很多人都会回答就是从这里可以看出的是

相信很多的人,每天在终端不止一遍的执行着 node 这条命令,对于很多人来说,它就像一个黑盒,并不知道背后到底发生了什么,本文将会为大家揭开这个神秘的面纱,由于本人水平有限,所以只是讲一个大概其,主要关注的过程就是 node 模块的初始化, event loopv8 的部分基本没有深入,这些部分可以关注一下我以后的文章。(提示本文非常的长,希望大家不要看烦~) 本文node基于10.5.0

node是什么?

这个问题很多人都会回答就是 v8 + libuv ,但是除了这个两个库以外 node 还依赖许多优秀的开源库,可以通过 process.versions 来看一下:

nodejs源码—初始化
  • http_parser 主要用于解析http数据包的模块,在这个库的作者也是 ry ,一个纯 c 的库,无任何依赖
  • v8 这个大家就非常熟悉了,一个优秀的 js 引擎
  • uv 这个就是 ry 实现的 libuv ,其封装了 libevIOCP ,实现了跨平台, node 中的 i/o 就是它,尽管 js 是单线程的,但是 libuv 并不是,其有一个线程池来处理这些 i/o 操作。
  • zlib 主要来处理压缩操作,诸如熟悉的 gzip 操作
  • aresc-ares ,这个库主要用于解析 dns ,其也是异步的
  • modules 就是 node 的模块系统,其遵循的规范为 commonjs ,不过 node 也支持了 ES 模块,不过需要加上参数并且文件名后缀需要为 mjs ,通过源码看, nodeES 模块的名称作为了一种 url 来看待,具体可以参见 这里
  • nghttp2 如其名字一样,是一个 http2 的库
  • napi 是在 node8 出现, node10 稳定下来的,可以给编写 node 原生模块更好的体验(终于不用在依赖于 nan ,每次更换 node 版本还要重新编译一次了)
  • openssl 非常著名的库, tls 模块依赖于这个库,当然还包括 https
  • icu 就是 small-icu ,主要用于解决跨平台的编码问题, versions 对象中的 unicodecldrtz 也源自 icu ,这个的定义可以参见 这里

从这里可以看出的是 process 对象在 node 中非常的重要, 个人的理解,其实 node 与浏览器端最主要的区别,就在于这个 process 对象

注: node 只是用 v8 来进行 js 的解析,所以不一定非要依赖 v8 ,也可以用其他的引擎来代替,比如利用微软的 ChakraCore ,对应的 node仓库

node初始化

经过上面的一通分析,对 node 的所有依赖有了一定的了解,下面来进入正题,看一下 node 的初始化过程:

挖坑

node_main.cc 为入口文件,可以看到的是除了调用了 node::Start 之外,还做了两件事情:

NODE_SHARED_MODE忽略SIGPIPE信号

SIGPIPE 信号出现的情况一般在 socket 收到 RST packet 之后,扔向这个 socket 写数据时产生,简单来说就是 clientserver 发请求,但是这时候 client 已经挂掉,这时候就会产生 SIGPIPE 信号,产生这个信号会使 server 端挂掉,其实 node::PlatformInit 中也做了这种操作,不过只是针对 non-shared lib build

改变缓冲行为

stdout 的默认缓冲行为为 _IOLBF (行缓冲),但是对于这种来说交互性会非常的差,所以将其改为 _IONBF (不缓冲)

探索

node.cc 文件中总共有三个 Start 函数,先从 node_main.cc 中掉的这个 Start 函数开始看:

int Start(int argc, char** argv) {
  // 退出之前终止libuv的终端行为,为正常退出的情况
  atexit([] () { uv_tty_reset_mode(); });
  // 针对平台进行初始化
  PlatformInit();
  // ...
  Init(&argc, const_cast<const char**>(argv), &exec_argc, &exec_argv);
  // ...
  v8_platform.Initialize(v8_thread_pool_size);
  // 熟悉的v8初始化函数
  V8::Initialize();
  // ..
  const int exit_code =
    Start(uv_default_loop(), argc, argv, exec_argc, exec_argv);
}
复制代码

上面函数只保留了一些关键不走,先来看看 PlatformInit

PlatfromInit

unix 中将一切都看作文件,进程启动时会默认打开三个 i/o 设备文件,也就是 stdin stdout stderr ,默认会分配 0 1 2 三个描述符出去,对应的文件描述符常量为 STDIN_FILENO STDOUT_FILENO STDERR_FILENO ,而 windows 中没有文件描述符的这个概念,对应的是句柄, PlatformInit 首先是检查是否将这个三个文件描述符已经分配出去,若没有,则利用 open("/dev/null", O_RDWR) 分配出去,对于 windows 做了同样的操作,分配句柄出去,而且 windows 只做了这一个操作;对于 unix 来说还会针对 SIGINT (用户调用Ctrl-C时发出)和 SIGTERMSIGTERMSIGKILL 类似,但是不同的是该信号可以被阻塞和处理,要求程序自己退出)信号来做一些特殊处理,这个处理与正常退出时一样;另一个重要的事情就是下面这段代码:

struct rlimit lim;
  // soft limit 不等于 hard limit, 意味着可以增加
  if (getrlimit(RLIMIT_NOFILE, &lim) == 0 && lim.rlim_cur != lim.rlim_max) {
    // Do a binary search for the limit.
    rlim_t min = lim.rlim_cur;
    rlim_t max = 1 << 20;
    // But if there's a defined upper bound, don't search, just set it.
    if (lim.rlim_max != RLIM_INFINITY) {
      min = lim.rlim_max;
      max = lim.rlim_max;
    }
    do {
      lim.rlim_cur = min + (max - min) / 2;
      // 对于mac来说 hard limit 为unlimited
      // 但是内核有限制最大的文件描述符,超过这个限制则设置失败
      if (setrlimit(RLIMIT_NOFILE, &lim)) {
        max = lim.rlim_cur;
      } else {
        min = lim.rlim_cur;
      }
    } while (min + 1 < max);
  }
复制代码

这个件事情也就是提高一个进程允许打开的最大文件描述符,但是在 mac 上非常的奇怪,执行 ulimit -H -n 得到 hard limitunlimited ,所以我认为 mac 上的最大文件描述符会被设置为 1 << 20 ,但是最后经过实验发现最大只能为 24576 ,非常的诡异,最后经过一顿搜索,查到了原来 mac 的内核对能打开的文件描述符也有限制,可以用 sysctl -A | grep kern.maxfiles 进行查看,果然这个数字就是 24576

nodejs源码—初始化

Init

Init 函数调用了 RegisterBuiltinModules

// node.cc
void RegisterBuiltinModules() {
#define V(modname) _register_##modname();
  NODE_BUILTIN_MODULES(V)
#undef V
}

// node_internals.h
#define NODE_BUILTIN_MODULES(V)                                       
  NODE_BUILTIN_STANDARD_MODULES(V)   
  NODE_BUILTIN_OPENSSL_MODULES(V) 
  NODE_BUILTIN_ICU_MODULES(V)
复制代码

从名字也可以看出上面的过程是进行 c++ 模块的初始化, node 利用了一些宏定义的方式,主要关注 NODE_BUILTIN_STANDARD_MODULES 这个宏:

#define NODE_BUILTIN_STANDARD_MODULES(V)
    V(async_wrap)                                                       
    V(buffer)
    ...
复制代码

结合上面的定义,可以得出编译后的代码大概为:

void RegisterBuiltinModules() {
  _register_async_wrap();
  _register_buffer();
}
复制代码

而这些 _register 又是从哪里来的呢?以 buffer 来说,对应 c++ 文件为 src/node_buffer.cc ,来看这个文件的最后一行,第二个参数是模块的初始化函数:

NODE_BUILTIN_MODULE_CONTEXT_AWARE(buffer, node::Buffer::Initialize)
复制代码

这个宏存在于 node_internals.h 中:

#define NODE_MODULE_CONTEXT_AWARE_CPP(modname, regfunc, priv, flags)
  static node::node_module _module = {
    NODE_MODULE_VERSION,                                                      
    flags,                                                                    
    nullptr,                                                                  
    __FILE__,                                                                  
    nullptr,                                                                   
    (node::addon_context_register_func) (regfunc),// 暴露给js使用的模块的初始化函数
    NODE_STRINGIFY(modname),                                                 
    priv,                                                                     
    nullptr                                                                   
  };                                                                          
  void _register_ ## modname() {                                              
    node_module_register(&_module);                                           
  }


#define NODE_BUILTIN_MODULE_CONTEXT_AWARE(modname, regfunc)                   
  NODE_MODULE_CONTEXT_AWARE_CPP(modname, regfunc, nullptr, NM_F_BUILTIN)
复制代码

发现调用的 _register_buffer 实质上调用的是 node_module_register(&_module) ,每一个 c++ 模块对应的为一个 node_module 结构体,再来看看 node_module_register 发生了什么:

extern "C" void node_module_register(void* m) {
  struct node_module* mp = reinterpret_cast<struct node_module*>(m);

  if (mp->nm_flags & NM_F_BUILTIN) {
    mp->nm_link = modlist_builtin;
    modlist_builtin = mp;
  }
  ...
}
复制代码

由此可以见, c++ 模块被存储在了一个链表中,后面 process.binding() 本质上就是在这个链表中查找对应 c++ 模块, node_module 是链表中的一个节点,除此之外 Init 还初始化了一些变量,这些变量基本上都是取决于环境变量用 getenv 获得即可

v8初始化

到执行完 Init 为止,还没有涉及的 jsc++ 的交互,在将一些环境初始化之后,就要开始用 v8 这个大杀器了, v8_platform 是一个结构体,可以理解为是 node 对于 v8v8::platform 一个封装,紧接着的就是对 v8 进行初始化,自此开始具备了与 js 进行交互的能力,初始化 v8 之后,创建了一个 libuv 事件循环就进入了下一个 Start 函数

第二个Start函数

inline int Start(uv_loop_t* event_loop,
                 int argc, const char* const* argv,
                 int exec_argc, const char* const* exec_argv) {
  std::unique_ptr<ArrayBufferAllocator, decltype(&FreeArrayBufferAllocator)>
      allocator(CreateArrayBufferAllocator(), &FreeArrayBufferAllocator);
  Isolate* const isolate = NewIsolate(allocator.get());
  // ...
  {
    Locker locker(isolate);
    Isolate::Scope isolate_scope(isolate);
    HandleScope handle_scope(isolate);
  }
}
复制代码

首先创建了一个 v8Isolate (隔离),隔离在 v8 中非常常见,仿佛和进程一样,不同隔离不共享资源,有着自己得堆栈,但是正是因为这个原因在多线程的情况下,要是对每一个线程都创建一个隔离的话,那么开销会非常的大(可喜可贺的是 node 有了 worker_threads ),这时候可以借助 Locker 来进行同步,同时也保证了一个 Isolate 同一时刻只能被一个线程使用;下面两行就是 v8 的常规套路,下一步一般就是创建一个 Context (最简化的一个流程可以参见 v8hello world ), HandleScope 叫做句柄作用域,一般都是放在函数的开头,来管理函数创建的一些句柄(水平有限,暂时不深究,先挖个坑);第二个 Start 的主要流程就是这个,下面就会进入最后一个 Start 函数,这个函数可以说是非常的关键,会揭开所有的谜题

解开谜题

inline int Start(Isolate* isolate, IsolateData* isolate_data,
                 int argc, const char* const* argv,
                 int exec_argc, const char* const* exec_argv) {
  HandleScope handle_scope(isolate);
  // 常规套路
  Local<Context> context = NewContext(isolate);
  Context::Scope context_scope(context);
  Environment env(isolate_data, context, v8_platform.GetTracingAgentWriter());
  env.Start(argc, argv, exec_argc, exec_argv, v8_is_profiling);
  // ...
复制代码

可以见到 v8 的常见套路,创建了一个上下文,这个上下文就是 js 的执行环境, Context::Scope 是用来管理这个 ContextEnvironment 可以理解为一个 node 的运行环境,记录了 isolate,event loop 等, Start 的过程主要是做了一些 libuv 的初始化以及 process 对象的定义:

auto process_template = FunctionTemplate::New(isolate());
  process_template->SetClassName(FIXED_ONE_BYTE_STRING(isolate(), "process"));

  auto process_object =
      process_template->GetFunction()->NewInstance(context()).ToLocalChecked();
  set_process_object(process_object);

  SetupProcessObject(this, argc, argv, exec_argc, exec_argv);
复制代码

SetupProcessObject 生成了一个 c++ 层面上的 process 对象,这个已经基本上和平时 node 中的 process 对象一致,但是还会有一些出入,比如没有 binding 等,完成了这个过程之后就开始了 LoadEnvironment

LoadEnvironment

Local<String> loaders_name =
    FIXED_ONE_BYTE_STRING(env->isolate(), "internal/bootstrap/loaders.js");
MaybeLocal<Function> loaders_bootstrapper =
    GetBootstrapper(env, LoadersBootstrapperSource(env), loaders_name);
Local<String> node_name =
    FIXED_ONE_BYTE_STRING(env->isolate(), "internal/bootstrap/node.js");
MaybeLocal<Function> node_bootstrapper =
    GetBootstrapper(env, NodeBootstrapperSource(env), node_name);
复制代码

先将 lib/internal/bootstrap 文件夹下的两个文件读进来,然后利用 GetBootstrapper 来执行 js 代码分别得到了一个函数,一步步来看,先看看 GetBootstrapper 为什么可以执行 js 代码,查看这个函数可以发现主要是因为 ExecuteString

MaybeLocal<v8::Script> script =
    v8::Script::Compile(env->context(), source, &origin);
...
MaybeLocal<Value> result = script.ToLocalChecked()->Run(env->context());
复制代码

这个主要利用了 v8 的能力,对 js 文件进行了解析和执行,打开 loaders.js 看看其参数,需要五个,捡两个最重要的来说,分别是 processgetBinding ,这里面往后继续看 LoadEnvironment 发现 process 对象就是刚刚生成的,而 getBinding 是函数 GetBinding

node_module* mod = get_builtin_module(*module_v);
Local<Object> exports;
if (mod != nullptr) {
  exports = InitModule(env, mod, module);
} else if (!strcmp(*module_v, "constants")) {
  exports = Object::New(env->isolate());
  CHECK(exports->SetPrototype(env->context(),
                              Null(env->isolate())).FromJust());
  DefineConstants(env->isolate(), exports);
} else if (!strcmp(*module_v, "natives")) { // NativeModule _source
  exports = Object::New(env->isolate());
  DefineJavaScript(env, exports);
} else {
  return ThrowIfNoSuchModule(env, *module_v);
}

args.GetReturnValue().Set(exports);
复制代码

其作用就是根据传参来初始化指定的模块,当然也有比较特殊的两个分别是 constantsnatives (后面再看), get_builtin_module 调用的就是 FindModule ,还记得之前在 Init 过程中将模块都注册到的链表吗? FindModule 就是遍历这个链表找到相应的模块:

struct node_module* mp;
for (mp = list; mp != nullptr; mp = mp->nm_link) {
  if (strcmp(mp->nm_modname, name) == 0)
    break;
}
复制代码

InitModule 就是调用之前注册模块定义的初始化函数,还以 buffer 看的话,就是执行 node::Buffer::Initialize 函数,打开着函数来看和平时写addon的方式一样,也会暴露一个对象出来供 js 调用; LoadEnvironment 下面就是将 process, GetBinding 等作为传入传给上面生成好的函数并且利用 v8 来执行,来到了大家熟悉的领域,来看看 loaders.js

const moduleLoadList = [];
ObjectDefineProperty(process, 'moduleLoadList', {
  value: moduleLoadList,
  configurable: true,
  enumerable: true,
  writable: false
});
复制代码

定义了一个已经加载的Module的数组,也可以在 node 通过 process.moduleLoadList 来看看加载了多少的原生模块进来

process.binding

process.binding = function binding(module) {
  module = String(module);
  let mod = bindingObj[module];
  if (typeof mod !== 'object') {
    mod = bindingObj[module] = getBinding(module);
    moduleLoadList.push(`Binding ${module}`);
  }
  return mod;
};
复制代码

终于到了这个方法,翻看 lib 中的 js 文件,有着非常多的这种调用,这个函数就是对 GetBinding 做了一个 js 层面的封装,做的无非是查看一下这个模块是否已经加载完成了,是的话直接返回回去,不需要再次初始化了,所以利用 prcoess.binding 加载了对应的 c++ 模块(可以执行一下 process.binding('buffer') ,然后再去 node_buffer.cc 中看看)继续向下看,会发现定义了一个 class 就是 NativeModule ,发现其有一个静态属性:

加载js

NativeModule._source = getBinding('natives');
复制代码

返回到 GetBinding 函数,看到的是一个 if 分支就是这种情况:

exports = Object::New(env->isolate());
DefineJavaScript(env, exports);
复制代码

来看看 DefineJavaScript 发生了什么样的事情,这个函数发现只能在头文件( node_javascript.h )里面找到,但是根本找不到具体的实现,这是个什么鬼???去翻一下 node.gyp 文件发现这个文件是用 js2c.py 这个文件生成的,去看一下这个 python 文件,可以发现许多的代码模板,每一个模板都是用 Render 返回的, data 参数就是 js 文件的内容,最终会被转换为 c++ 中的 byte 数组,同时定义了一个将其转换为字符串的方法,那么问题来了,这些文件都是那些呢?答案还是在 node.gyp 中,就是 library_files 数组,发现包含了 lib 下的所有的文件和一些 dep 下的 js 文件, DefineJavaScript 这个文件做的就是将待执行的 js 代码注册下,所以 NativeModule._source 中存储的是一些待执行的 js 代码,来看一下 NativeModule.require

NativeModule

const cached = NativeModule.getCached(id);
if (cached && (cached.loaded || cached.loading)) {
  return cached.exports;
}
moduleLoadList.push(`NativeModule ${id}`);

const nativeModule = new NativeModule(id);

nativeModule.cache();
nativeModule.compile();

return nativeModule.exports;
复制代码

可以发现 NativeModule 也有着缓存的策略, require 先把其放到 _cache 中再次 require 就不会像第一次那样执行这个模块,而是直接用缓存中执行好的,后面说的 Module 与其同理,看一下 compile 的实现:

let source = NativeModule.getSource(this.id);
source = NativeModule.wrap(source);

NativeModule.wrap = function(script) {
  return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
};
NativeModule.wrapper = [
  '(function (exports, require, module, process) {',
  '\n});'
];
复制代码

首先从 _source 中取出相应的模块,然后对这个模块进行包裹成一个函数,执行函数用的是什么呢?

const script = new ContextifyScript(
  source, this.filename, 0, 0,
  codeCache[this.id], false, undefined
);

this.script = script;
const fn = script.runInThisContext(-1, true, false);
const requireFn = this.id.startsWith('internal/deps/') ?
  NativeModule.requireForDeps :
  NativeModule.require;
fn(this.exports, requireFn, this, process);
复制代码

本质上就是调用了 vm 编译自妇产得到函数,然后给其传入了一些参数并执行, this.exports 就是一个对象, require 区分了一下是否加载 node 依赖的 js 文件, this 也就是参数 module ,这也说明了两者的关系, exports 就是 module 的一个属性,也解释了为什么 exports.xx 之后再指定 module.exports = yy 会将 xx 忽略掉,还记得 LoadEnvironment 吗? bootstrap/loaders.js 执行完之后执行了 bootstrap/node.js ,可以说这个文件是 node 真正的入口,比如定义了 global 对象上的属性,比如 console setTimeout 等,由于篇幅有限,来挑一个最常用的场景,来看看这个是什么一回事:

else if (process.argv[1] && process.argv[1] !== '-') {
  const path = NativeModule.require('path');
  process.argv[1] = path.resolve(process.argv[1]);

  const CJSModule = NativeModule.require('internal/modules/cjs/loader');
  ...
  CJSModule.runMain();
}
复制代码

这个过程就是熟悉的 node index.js 这个过程,可以看到的对于开发者自己的 js 来说,在 node 中对应的 classModule ,相信这个文件大家很多人都了解,与 NativeModule 相类似,不同的是,需要进行路径的解析和模块的查找等,来大致的看一下这个文件,先从上面调用的 runMain 来看:

if (experimentalModules) {
  // ...
} else {
  Module._load(process.argv[1], null, true);
}
复制代码

Module

node 中开启 --experimental-modules 可以加载 es 模块,也就是可以不用 babel 转义就可以使用 import/export 啦,这个不是重点,重点来看普通的 commonnjs 模块, process.argv[1] 一般就是要执行的入口文件,下面看看 Module._load

Module._load = function(request, parent, isMain) {
  if (parent) {
    debug('Module._load REQUEST %s parent: %s', request, parent.id);
  }
  // 查找文件具体位置
  var filename = Module._resolveFilename(request, parent, isMain);

  // 存在缓存,则不需要再次执行
  var cachedModule = Module._cache[filename];
  if (cachedModule) {
    updateChildren(parent, cachedModule, true);
    return cachedModule.exports;
  }

  // 加载node原生模块,原生模块不需要缓存,因为NativeModule中也存在缓存
  if (NativeModule.nonInternalExists(filename)) {
    debug('load native module %s', request);
    return NativeModule.require(filename);
  }

  // 加载并执行一个模块
  var module = new Module(filename, parent);

  if (isMain) {
    process.mainModule = module;
    module.id = '.';
  }

  Module._cache[filename] = module;

  // 调用load方法进行加载
  tryModuleLoad(module, filename);

  return module.exports;
};
复制代码

这里看每一个 Module 有一个 parent 的属性,假如 a.js 中引入了 b.js ,那么 Module bparent 就是 Module a ,利用 resolveFilename 可以得到文件具体的位置,这个过程而后调用 load 函数来加载文件,可以看到的是区分了几种类型,分别是 .js .json .node ,对应的 .js 是读文件然后执行, .json 是直接读文件后 JSON.parse 一下, .node 是调用 dlopenModule.compileNativeModule.compile 相类似都是想包裹一层成为函数,然后调用了 vm 编译得到这个函数,最后传入参数来执行,对于 Module 来说,包裹的代码如下:

Module.wrapper = [
  '(function (exports, require, module, __filename, __dirname) { ',
  '\n});'
];
复制代码

执行完上述过程后,前期工作就已经做得比较充分了,再次回到最后一个 Start 函数来看,从代码中可以看到开始了 nodeevent loop ,这就是 node 的初始化过程,关于 event loop 需要对 libuv 有一定的了解,可以说 node 真正离不开的是 libuv ,具体这方面的东西,可以继续关注我后面的文章

总结

总结一下这个过程,以首次加载没有任何缓存的情况开看: require('fs') ,先是调用了 Module.require ,而后发现为原生模块,于是调用 NativeModule.require ,从 NativeModule._sourcelib/fs 的内容拿出来包裹一下然后执行,这个文件第一行就可以看到 process.binding ,这个本质上是加载原生的 c++ 模块,这个模块在初始化的时候将其注册到了一个链表中,加载的过程就是将其拿出来然后执行

以上内容如果有错误的地方,还请大佬指出,万分感激,另外一件重要的事情就是: 我所在团队也在招人,如果有兴趣可以将简历发至zhoupeng.1996@bytedance.com


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

奔跑吧 Linux内核

奔跑吧 Linux内核

张天飞 / 人民邮电出版社 / 2017-9-1 / CNY 158.00

本书内容基于Linux4.x内核,主要选取了Linux内核中比较基本和常用的内存管理、进程管理、并发与同步,以及中断管理这4个内核模块进行讲述。全书共分为6章,依次介绍了ARM体系结构、Linux内存管理、进程调度管理、并发与同步、中断管理、内核调试技巧等内容。本书的每节内容都是一个Linux内核的话题或者技术点,读者可以根据每小节前的问题进行思考,进而围绕问题进行内核源代码的分析。 本书内......一起来看看 《奔跑吧 Linux内核》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具