资讯专栏INFORMATION COLUMN

通过源码解析 Node.js 启动时第一个执行的 js 文件:bootstrap_node.js

TNFE / 3451人阅读

摘要:注很多以前的源码分析文章中,所写的第一个执行的文件代码为,但这个文件在中已被移除,并被拆解为了等其他下的文件,为正文作为第一段被执行的代码,它的历史使命免不了就是进行一些环境和全局变量的初始化工作。

大家可能会好奇,在 Node.js 启动后,第一个执行的 JavaScript 文件会是哪个?它具体又会干些什么事?

一步步来看,翻开 Node.js 的源码,不难看出,入口文件在 src/node_main.cc 中,主要任务为将参数传入 node::Start 函数:

// src/node_main.cc
// ...

int main(int argc, char *argv[]) {
  setvbuf(stderr, NULL, _IOLBF, 1024);
  return node::Start(argc, argv);
}

node::Start 函数定义于 src/node.cc 中,它进行了必要的初始化工作后,会调用 StartNodeInstance

// src/node.cc
// ...

int Start(int argc, char** argv) {
    // ...
    NodeInstanceData instance_data(NodeInstanceType::MAIN,
                                   uv_default_loop(),
                                   argc,
                                   const_cast(argv),
                                   exec_argc,
                                   exec_argv,
                                   use_debug_agent);
    StartNodeInstance(&instance_data);
}

而在 StartNodeInstance 函数中,又调用了 LoadEnvironment 函数,其中的 ExecuteString(env, MainSource(env), script_name); 步骤,便执行了第一个 JavaScript 文件代码:

// src/node.cc
// ...
void LoadEnvironment(Environment* env) { 
  // ...
  Local f_value = ExecuteString(env, MainSource(env), script_name);
  // ...
}

static void StartNodeInstance(void* arg) {
  // ...
  {
      Environment::AsyncCallbackScope callback_scope(env);
      LoadEnvironment(env);
  }
  // ...
}

// src/node_javascript.cc
// ...

Local MainSource(Environment* env) {
  return String::NewFromUtf8(
      env->isolate(),
      reinterpret_cast(internal_bootstrap_node_native),
      NewStringType::kNormal,
      sizeof(internal_bootstrap_node_native)).ToLocalChecked();
}

其中的 internal_bootstrap_node_native ,即为 lib/internal/bootstrap_node.js 中的代码。(注:很多以前的 Node.js 源码分析文章中,所写的第一个执行的 JavaScript 文件代码为 src/node.js ,但这个文件在 Node.js v5.10 中已被移除,并被拆解为了 lib/internal/bootstrap_node.js 等其他 lib/internal 下的文件,PR 为: https://github.com/nodejs/node/pull/5103 )

正文

作为第一段被执行的 JavaScript 代码,它的历史使命免不了就是进行一些环境和全局变量的初始化工作。代码的整体结构很简单,所有的初始化逻辑都被封装在了 startup 函数中:

// lib/internal/bootstrap_node.js
"use strict";

(function(process) {
  function startup() {
    // ...
  }
  // ...
  startup();
});

而在 startup 函数中,逻辑可以分为四块:

初始化全局 process 对象上的部分属性 / 行为

初始化全局的一些 timer 方法

初始化全局 console 等对象

开始执行用户执行指定的 JavaScript 代码

让我们一个个来解析。

初始化全局 process 对象上的部分属性 / 行为 添加 processuncaughtException 事件的默认行为

在 Node.js 中,如果没有为 process 上的 uncaughtException 事件注册监听器,那么该事件触发时,将会导致进程退出,这个行为便是在 startup 函数里添加的:

// lib/internal/bootstrap_node.js
"use strict";

(function(process) {
  function startup() {
    setupProcessFatal();
  }
  // ...

  function setupProcessFatal() {

    process._fatalException = function(er) {
      var caught;
      // ...
      if (!caught)
        caught = process.emit("uncaughtException", er);

      if (!caught) {
        try {
          if (!process._exiting) {
            process._exiting = true;
            process.emit("exit", 1);
          }
        } catch (er) {
        }
      } 
      // ...
      return caught;
    };
  }
});

逻辑十分直白,使用到了 EventEmitter#emit 的返回值来判断该事件上是否有注册过的监听器,并最终调用 c++ 的 exit() 函数退出进程:

// src/node.cc
// ...

void FatalException(Isolate* isolate,
                    Local error,
                    Local message) {
  // ...
  Local caught =
      fatal_exception_function->Call(process_object, 1, &error);
      
  // ...
  if (false == caught->BooleanValue()) {
    ReportException(env, error, message);
    exit(1);
  }
}
根据 Node.js 在启动时所带的某些参数,来调整 processwarning 事件触发时的行为

具体来说,这些参数是:--no-warnings--no-deprecation--trace-deprecation--throw-deprecation。这些参数的有无信息,会先被挂载在 process 对象上:

// src/node.cc
// ...

  if (no_deprecation) {
    READONLY_PROPERTY(process, "noDeprecation", True(env->isolate()));
  }

  if (no_process_warnings) {
    READONLY_PROPERTY(process, "noProcessWarnings", True(env->isolate()));
  }

  if (trace_warnings) {
    READONLY_PROPERTY(process, "traceProcessWarnings", True(env->isolate()));
  }

  if (throw_deprecation) {
    READONLY_PROPERTY(process, "throwDeprecation", True(env->isolate()));
  }

然后根据这些信息,控制行为:

// lib/internal/bootstrap_node.js
"use strict";

(function(process) {
  function startup() {
    // ...
    NativeModule.require("internal/process/warning").setup();
  }
  // ...
  startup();
});
// lib/internal/process/warning.js
"use strict";

const traceWarnings = process.traceProcessWarnings;
const noDeprecation = process.noDeprecation;
const traceDeprecation = process.traceDeprecation;
const throwDeprecation = process.throwDeprecation;
const prefix = `(${process.release.name}:${process.pid}) `;

exports.setup = setupProcessWarnings;

function setupProcessWarnings() {
  if (!process.noProcessWarnings) {
    process.on("warning", (warning) => {
      if (!(warning instanceof Error)) return;
      const isDeprecation = warning.name === "DeprecationWarning";
      if (isDeprecation && noDeprecation) return;
      const trace = traceWarnings || (isDeprecation && traceDeprecation);
      if (trace && warning.stack) {
        console.error(`${prefix}${warning.stack}`);
      } else {
        var toString = warning.toString;
        if (typeof toString !== "function")
          toString = Error.prototype.toString;
        console.error(`${prefix}${toString.apply(warning)}`);
      }
    });
  }
  // ...
}

具体行为的话,文档中已经有详细说明,逻辑总结来说,就是按需将警告打印到控制台,或者按需抛出特定的异常。其中 NativeModule 对象为 Node.js 在当前的函数体的局部作用域内,实现的一个最小可用的模块加载器,具有缓存等基本功能。

process 添加上 stdin, stdoutstderr 属性

通常为 tty.ReadStream 类和 tty.WriteStream 类的实例:

// lib/internal/bootstrap_node.js
"use strict";

(function(process) {
  function startup() {
    // ...
    NativeModule.require("internal/process/stdio").setup();
  }
  // ...
  startup();
});
// lib/internal/process/stdio.js
// ...

function setupStdio() {
  var stdin, stdout, stderr;

  process.__defineGetter__("stdout", function() {
    if (stdout) return stdout;
    stdout = createWritableStdioStream(1);
    // ...
    return stdout
  }

  process.__defineGetter__("stderr", function() {
    if (stderr) return stderr;
    stderr = createWritableStdioStream(2);
    // ...
    return stderr;
  });

  process.__defineGetter__("stdin", function() {
    if (stdin) return stdin;

    var tty_wrap = process.binding("tty_wrap");
    var fd = 0;

    switch (tty_wrap.guessHandleType(fd)) {
      case "TTY":
        var tty = require("tty");
        stdin = new tty.ReadStream(fd, {
          highWaterMark: 0,
          readable: true,
          writable: false
        });
        break;
      // ...
    }
    return stdin;
  }
}

function createWritableStdioStream(fd) {
  var stream;
  var tty_wrap = process.binding("tty_wrap");

  // Note stream._type is used for test-module-load-list.js

  switch (tty_wrap.guessHandleType(fd)) {
    case "TTY":
      var tty = require("tty");
      stream = new tty.WriteStream(fd);
      stream._type = "tty";
      break;
    // ...    
  }
  // ...
}
process 添加上 nextTick 方法

具体的做法便是将注册的回调推进队列中,等待事件循环的下一次 Tick ,一个个取出执行:

// lib/internal/bootstrap_node.js
"use strict";

(function(process) {
  function startup() {
    // ...
    NativeModule.require("internal/process/next_tick").setup();
  }
  // ...
  startup();
});
// lib/internal/process/next_tick.js
"use strict";

exports.setup = setupNextTick;

function setupNextTick() {
  var nextTickQueue = [];
  // ...
  var kIndex = 0;
  var kLength = 1;

  process.nextTick = nextTick;
  process._tickCallback = _tickCallback;

  function _tickCallback() {
    var callback, args, tock;

    do {
      while (tickInfo[kIndex] < tickInfo[kLength]) {
        tock = nextTickQueue[tickInfo[kIndex]++];
        callback = tock.callback;
        args = tock.args;
        _combinedTickCallback(args, callback);
        if (1e4 < tickInfo[kIndex])
          tickDone();
      }
      tickDone();
    } while (tickInfo[kLength] !== 0);
  }

  function nextTick(callback) {
    if (typeof callback !== "function")
      throw new TypeError("callback is not a function");
    if (process._exiting)
      return;

    var args;
    if (arguments.length > 1) {
      args = new Array(arguments.length - 1);
      for (var i = 1; i < arguments.length; i++)
        args[i - 1] = arguments[i];
    }

    nextTickQueue.push(new TickObject(callback, args));
    tickInfo[kLength]++;
  }
}
// ...
process 添加上 hrtime, kill, exit 方法
// lib/internal/bootstrap_node.js
"use strict";

(function(process) {
  function startup() {
    // ...
    _process.setup_hrtime();
    _process.setupKillAndExit();
  }
  // ...
  startup();
});

这些功能的核心实现也重度依赖于 c++ 函数:

hrtime 方法依赖于 libuv 提供的 uv_hrtime() 函数

kill 方法依赖于 libuv 提供的 uv_kill(pid, sig) 函数

exit 方法依赖于 c++ 提供的 exit(code) 函数

初始化全局的一些 timer 方法和 console 等对象

这些初始化都干的十分简单,直接赋值:

// lib/internal/bootstrap_node.js
"use strict";

(function(process) {
  function startup() {
    // ...
    setupGlobalVariables();
    if (!process._noBrowserGlobals) {
      setupGlobalTimeouts();
      setupGlobalConsole();
    }
    
    function setupGlobalVariables() {
      global.process = process;
      // ...
      global.Buffer = NativeModule.require("buffer").Buffer;
      process.domain = null;
      process._exiting = false;
    }
    
    function setupGlobalTimeouts() {
      const timers = NativeModule.require("timers");
      global.clearImmediate = timers.clearImmediate;
      global.clearInterval = timers.clearInterval;
      global.clearTimeout = timers.clearTimeout;
      global.setImmediate = timers.setImmediate;
      global.setInterval = timers.setInterval;
      global.setTimeout = timers.setTimeout;
    }

    function setupGlobalConsole() {
      global.__defineGetter__("console", function() {
        return NativeModule.require("console");
      });
    }
  }
  // ...
  startup();
});

值得注意的一点是,由于 console 是通过 __defineGetter__ 赋值给 global 对象的,所以在严格模式下给它赋值将会抛出异常,而非严格模式下,赋值将被忽略。

开始执行用户执行指定的 JavaScript 代码

这一部分的逻辑已经在之前的文章中有所阐述,这边就不再重复说明啦。

最后

还是再次总结下:

lib/internal/bootstrap_node.js 中的代码 为 Node.js 执行后第一段被执行的 JavaScript 代码,从 src/node.cc 中的 node::LoadEnvironment 被调用

lib/internal/bootstrap_node.js 主要进行了一些初始化工作:

初始化全局 process 对象上的部分属性 / 行为

添加接收到 uncaughtException 事件时的默认行为

根据 Node.js 启动时参数,调整 warning 事件的行为

添加上 stdinstdoutstderr 属性

添加上 nextTickhrtimeexit 方法

初始化全局的一些 timer 方法

初始化全局 console 等对象

开始执行用户执行指定的 JavaScript 代码

参考

https://github.com/nodejs/node/blob/master/src/node.cc

https://github.com/nodejs/node/blob/master/src/node_javascript.cc

https://github.com/nodejs/node/blob/master/lib/internal/process.js

https://github.com/nodejs/node/blob/master/lib/internal/process/next_tick.js

https://github.com/nodejs/node/blob/master/lib/internal/process/stdio.js

https://github.com/nodejs/node/blob/master/lib/internal/process/warning.js

https://github.com/nodejs/node/blob/master/lib/internal/bootstrap_node.js

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/79329.html

相关文章

  • 干货 | 走进Node.js启动过程剖析

    摘要:具体调用链路如图函数主要是解析启动参数,并过滤选项传给引擎。查阅文档之后发现,通过指定参数可以设置线程池大小。原来的字节码编译优化还有都是通过多线程完成又继续深入调查,发现环境变量会影响的线程池大小。执行过程如下调用执行。 作者:正龙 (沪江Web前端开发工程师)本文原创,转载请注明作者及出处。 随着Node.js的普及,越来越多的开发者使用Node.js来搭建环境,也有很多公司开始把...

    luck 评论0 收藏0
  • 干货剖析 | 走进Node.js启动过程

    摘要:具体调用链路如图函数主要是解析启动参数,并过滤选项传给引擎。查阅文档之后发现,通过指定参数可以设置线程池大小。原来的字节码编译优化还有都是通过多线程完成又继续深入调查,发现环境变量会影响的线程池大小。执行过程如下调用执行。 作者:正龙 (沪江Web前端开发工程师)本文原创,转载请注明作者及出处。 随着Node.js的普及,越来越多的开发者使用Node.js来搭建环境,也有很多公司开始把...

    Simon 评论0 收藏0
  • nodejs源码require问题

    摘要:提起中的模块,就会想到用去加在引用那个模块。看了不少博客,加载机制明白了,脑子里总是稀里糊涂的知道会每个文件会被文件的源码包裹,自然也就有文件中的命令了。于是想写写记录自己的整个过程。这就是几个的关系。 提起nodejs中的模块,就会想到用require去加在引用那个模块。看了不少博客,加载机制明白了,脑子里总是稀里糊涂的知道会每个文件会被(function (exports, req...

    LiveVideoStack 评论0 收藏0
  • JavaScript中递归

    摘要:第三次第四次设想,如果传入的参数值特别大,那么这个调用栈将会非常之大,最终可能超出调用栈的缓存大小而崩溃导致程序执行失败。注意尾递归不一定会将你的代码执行速度提高相反,可能会变慢。 译者按: 程序员应该知道递归,但是你真的知道是怎么回事么? 原文: All About Recursion, PTC, TCO and STC in JavaScript 译者: Fundebug ...

    Jacendfeng 评论0 收藏0
  • 系列3|走进Node.js之多进程模型

    摘要:例如,在方法中,如果需要主从进程之间建立管道,则通过环境变量来告知从进程应该绑定的相关的文件描述符,这个特殊的环境变量后面会被再次涉及到。 文:正龙(沪江网校Web前端工程师)本文原创,转载请注明作者及出处 之前的文章走进Node.js之HTTP实现分析中,大家已经了解 Node.js 是如何处理 HTTP 请求的,在整个处理过程,它仅仅用到单进程模型。那么如何让 Web 应用扩展到...

    snowell 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<