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用户体会不到这种快感


    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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容