美文网首页我爱编程
Node.js介绍6-Node的启动

Node.js介绍6-Node的启动

作者: 转角遇见一直熊 | 来源:发表于2016-05-17 18:09 被阅读853次

    在前面几篇文章介绍到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用户体会不到这种快感


      vs打开node

    运行

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

    1. 上面的第三部使用vcbuild nosign,就会编译完,生成node.exe。
      生成node.exe
    2. 通过visual studio运行


      vs运行node

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

    入口

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

    找到入口
    虽然大部分软狗对此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对象到底有什么:

    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<String> script_name = FIXED_ONE_BYTE_STRING(env->isolate(), "node.js");
      Local<Value> f_value = ExecuteString(env, MainSource(env), script_name);
    
      Local<Function> f = Local<Function>::Cast(f_value);
    ......
    }
    

    我们注意一下ExecuteString干了什么


    • ExecuteString
    // Executes a str within the current v8 context.
    static Local<Value> ExecuteString(Environment* env,
                                      Handle<String> source,
                                      Handle<String> filename) {
    ......
      Local<v8::Script> script = v8::Script::Compile(source, filename);
    ......
    
      Local<Value> result = script->Run();
      if (result.IsEmpty()) {
        ReportException(env, try_catch);
        exit(4);
      }
    
      return scope.Escape(result);
    }
    
    

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

      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中。

    native js 编译选项

    从代码中可以看到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<Value>& 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的启动

        本文链接:https://www.haomeiwen.com/subject/qbgmrttx.html