Node.js介绍6-Node的启动

在前面几篇文章介绍到v8,addon,libuv等知识后,现在终于可以有信心看node的源码了,对一个软件来说,启动和关闭是短暂的,但又是整个软件架构很关键的地方,一个设计良好的软件:在启动的时候快速稳定;运行的时候内存无泄漏,cup占用稳定有规律,服务可靠有包装;关闭的时候无错误。我们看看node是否认真考虑了这些。

准备

我是在windows下面用visual studio看代码的,先要做好准备工作:

  1. 下载node代码:https://github.com/nodejs/node.git
  2. 切换到自己想看的branch,我看的是v0.11.13
  3. 生成vs工程文件,详细步骤可以看BUILDING文档
  4. 打开工程,linux用户体会不到这种快感


    Node.js介绍6-Node的启动_第1张图片
    vs打开node

运行

可以用脚本编译,也可以用visual studio运行。

  1. 上面的第三部使用vcbuild nosign,就会编译完,生成node.exe。
    Node.js介绍6-Node的启动_第2张图片
    生成node.exe
  2. 通过visual studio运行


    Node.js介绍6-Node的启动_第3张图片
    vs运行node

node和我们平时写的程序也是一样生成exe文件,呵呵,感觉也没有那么神秘了。下面我们去看启动代码吧。node可是集成了v8和libuv,应该是蛮复杂的吧。

入口

代码这么多,怎么找到入口呢。幸亏我们有IDE,直接单步调试(F11)好了。发现代码在wmain停了下来。

Node.js介绍6-Node的启动_第4张图片
找到入口

虽然大部分软狗对此IDE使用场景很熟悉,但不排除还有一大部分开发linux系统的人还在用命令行和vim来看代码,实际上linux上面的用户可以尝试一下 Jetbrain的IDE

启动

由于代码有大量宏来处理跨平台差异和简化代码,下面的阅读不会关注这些宏,主要看如何启动libuv,v8和node内置模块的加载;文章也不会解释libuv和v8的相关api,因为前面有文章介绍过了。

好了,从node.cc的int Start(int argc, char** argv) {函数慢慢看吧。

初始化参数

在Start函数中,第一个调用的函数是Init,从名字来看便略知一二。

void Init(int* argc,
          const char** argv,
          int* exec_argc,
          const char*** exec_argv) {

这个函数处理一些初始化的工作,解析用户传入的参数,设置debug的相关的信息。这个版本的代码v8和libuv是混在一起的,在我看来是需要重构的。可见外国高手写代码也是先码功能。

初始化v8

下面的代码说明,在启动libuv循环前,先给v8实例node_isolate加了一个锁。这样保证当前node线程才能使用v8

V8::Initialize();
  {
    Locker locker(node_isolate);
    Environment* env =
        CreateEnvironment(node_isolate, argc, argv, exec_argc, exec_argv);
    // This Context::Scope is here so EnableDebug() can look up the current
    // environment with Environment::GetCurrentChecked().
    // TODO(bnoordhuis) Reorder the debugger initialization logic so it can
    // be removed.
    {
      Context::Scope context_scope(env->context());

CreateEnvironment创建了一个process对象(在JavaScript中),完成了进程相关信息的保存和一些全局设置。我们再测试一下process对象到底有什么:

Node.js介绍6-Node的启动_第5张图片
process对象

可见,我们如果想知道当前node一些全局的信息比如版本,可以通过process对象拿到。
另外, CreateEnvironment调用 Load加载src/node.js,后面再看。

初始化libuv循环

通过下面代码,我们可以看到:

  1. uv_run启动事件循环
  2. 设置循环模式为UV_RUN_ONCE,这样node会自动停止(如果没有要处理的handle的话)
  3. EmitBeforeExit调用env中回调函数emit。
  4. 如果还有新产生的需要处理的事物,继续循环。
    注意:这里又调用一次UV_RUN_NOWAIT,可能是因为uv_run比uv_loop_alive有更多语义,这里即用uv_loop_alive,又用UV_RUN_NOWAIT,至少代码不够清晰,可以考虑重构一下。
 do {
        more = uv_run(env->event_loop(), UV_RUN_ONCE);
        if (more == false) {
          EmitBeforeExit(env);

          // Emit `beforeExit` if the loop became alive either after emitting
          // event, or after running some callbacks.
          more = uv_loop_alive(env->event_loop());
          if (uv_run(env->event_loop(), UV_RUN_NOWAIT) != 0)
            more = true;
        }
      } while (more == true);

加载Node.js

前面说到CreateEnvironment调用Load加载src/node.js。现在我们看看node.js有哪些功能。同时,node.js又加载了一些native模块,我们看看这互相加载到底怎么弄的。

  • CreateEnvironment函数
Environment* CreateEnvironment(Isolate* isolate,
                               int argc,
                               const char* const* argv,
                               int exec_argc,
                               const char* const* exec_argv) {
......
 
  Load(env);

......

  • load函数
void Load(Environment* env) {
 HandleScope handle_scope(env->isolate());

  // Compile, execute the src/node.js file. (Which was included as static C
  // string in node_natives.h. 'natve_node' is the string containing that
  // source code.)

......

  Local script_name = FIXED_ONE_BYTE_STRING(env->isolate(), "node.js");
  Local f_value = ExecuteString(env, MainSource(env), script_name);

  Local f = Local::Cast(f_value);
......
}

我们注意一下ExecuteString干了什么


  • ExecuteString
// Executes a str within the current v8 context.
static Local ExecuteString(Environment* env,
                                  Handle source,
                                  Handle filename) {
......
  Local script = v8::Script::Compile(source, filename);
......

  Local result = script->Run();
  if (result.IsEmpty()) {
    ReportException(env, try_catch);
    exit(4);
  }

  return scope.Escape(result);
}

调试的时候, Handle并不能在调试器看到值,可以用下面的代码打印一下

  v8::String::Utf8Value param1(script_name);

或者下载Visual Studio Debugger Visualizers

git clone https://chromium.googlesource.com/chromium/src/tools/win

上面是chrome的调试工具,调试v8的时候好像不好用,


  • v8::Script::Compile(source, filename);
    这里进入v8编译JavaScript环节了。不在往下挖掘,因为这样对分析Node的启动没有什么好处。我们还是去看看node.js做了什么。

Node.js文件

我们看看node.js文件头部。

// Hello, and welcome to hacking node.js!
//
// This file is invoked by node::Load in src/node.cc, and responsible for
// bootstrapping the node.js core. Special caution is given to the performance
// of the startup process, so many dependencies are invoked lazily.

(function(process) {
  this.global = this;

......

});
  1. 定义了一个函数。
  2. 在src/node.cc中被node::Load调用。
  3. 为了加快速度,很多依赖都延迟加载了。

我们研究一下他加载了什么。

  • 第一部分: startup
    node.js定义了一个startup函数并调用,startup函数中使用NativeModule去加载很多模块
(function(process) {
  this.global = this;

  function startup() {
   ......
  }

  startup.globalVariables = function() {
  ......
  };

  startup();

  • 第二部分:定义NativeModule的加载机制
  // Below you find a minimal module system, which is used to load the node
  // core modules found in lib/*.js. All core modules are compiled into the
  // node binary, so they can be loaded faster.

  var ContextifyScript = process.binding('contextify').ContextifyScript;
  function runInThisContext(code, options) {
    var script = new ContextifyScript(code, options);
    return script.runInThisContext();
  }

  function NativeModule(id) {
    ......
  }

......

对于NativeModule,我们要仔细看看,到底是怎么加载的。。。

NativeModule

  • 什么是native模块

这里的native并不是c++代码,而是js,从图中可以看到node自带了很多js。为了加快加载速度,把这些js通过一个python工具转成了node_natives.h这个头文件,然后直接编译到node.exe中。

Node.js介绍6-Node的启动_第6张图片
native js
Node.js介绍6-Node的启动_第7张图片
编译选项

从代码中可以看到node.js也被放到头文件了。


  • NativeModule如何加载模块

1. 导入natives

 NativeModule._source = process.binding('natives');

这个natives就是在头文件中定义的数据。
process.binding实在node.cc中定义的:NODE_SET_METHOD(process, "binding", Binding);。上面看到的自带的js大量使用这个函数加载C++模块。

2. NativeModule.require函数
js都是使用require函数来加载模块,只不过这个require也是普通的函数而已,并不是语言本身支持的。我们看看代码。

NativeModule.require = function(id) {
    if (id == 'native_module') { //在module模块还会require('native_module')
      return NativeModule;
    }

    var cached = NativeModule.getCached(id);
    if (cached) {
      return cached.exports;
    }

    if (!NativeModule.exists(id)) {
      throw new Error('No such native module ' + id);
    }

    process.moduleLoadList.push('NativeModule ' + id);

    var nativeModule = new NativeModule(id);

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

    return nativeModule.exports;
  };

3. compile函数
这里看一下compile函数。实际上只是在第一步的数组中查找。

NativeModule.prototype.compile = function() {
    var source = NativeModule.getSource(this.id);
    source = NativeModule.wrap(source);

    var fn = runInThisContext(source, { filename: this.filename });
    fn(this.exports, NativeModule.require, this, this.filename);

    this.loaded = true;
  };

wrap就是包装了一个函数

NativeModule.wrap = function(script) {
    return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
  };

  NativeModule.wrapper = [
    '(function (exports, require, module, __filename, __dirname) { ',
    '\n});'
  ];

接下来看一下runInThisContext

4. runInThisContext

var ContextifyScript = process.binding('contextify').ContextifyScript;
  function runInThisContext(code, options) {
    var script = new ContextifyScript(code, options);
    return script.runInThisContext();
  }

node.js文件中的js代码只是调用了C++,我们得看一下C++代码。

 static void RunInThisContext(const FunctionCallbackInfo& args) {
    Isolate* isolate = args.GetIsolate();
    HandleScope handle_scope(isolate);

    // Assemble arguments
    TryCatch try_catch;
    uint64_t timeout = GetTimeoutArg(args, 0);
    bool display_errors = GetDisplayErrorsArg(args, 0);
    if (try_catch.HasCaught()) {
      try_catch.ReThrow();
      return;
    }

    // Do the eval within this context
    Environment* env = Environment::GetCurrent(isolate);
    EvalMachine(env, timeout, display_errors, args, try_catch);
  }

EvalMachine不看了,v8运行代码了。


总结

上面主要的逻辑都在CreateEnvironmentnode.js中,从c++掉用到js,再从js调用c++,js调用js。着实复杂。

启动过程大致如下:

  1. 初始化v8
  2. 创建process对象
  3. 加载node.js
  4. node.js reqiure更多模块
  5. native模块加载完毕
  6. 如果传入文件,比如node myIndex.js,加载用户模块(startup函数中处理)
  7. libuv循环建立
  8. 等待或者结束(根据启动参数不同)

我们可以看到为了加快native模块的加载速度,采用了把js编译成.h文件的方法,我们如果想加快启动速度也可以这么干。

如此粗糙的过程虽然不能完全了解到node启动的过程,很多细节有带进一步研究,但是我们至少又前进了一些。_

本文参考nodejs-source-reading-note

你可能感兴趣的:(Node.js介绍6-Node的启动)