nodejs启动流程分析

2,767 阅读6分钟

前言

之前用过一段时间的v8 ,也只是会初始化那个流程,最近想深入了解一下,所以想要通过学习 nodejs 来加深理解。这篇文章主要是讲讲 nodejs 的初始化流程,如有错误,烦请指教~。(本文分析基于 v10.9.0,本文会尽量避免大段源码,但是为了有理有据,还是会放上一些精简过并带有注释的代码上来)。

Helloworld 镇楼:

const http = require('http');
const hostname = '127.0.0.1';
const port = 8888;

http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('Hello World\n');
}).listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});

写过 nodejs 的都能看懂如上代码。寥寥数行,就创建了一个 http 服务。第一行代码,就出现了一个 require 关键字,那么 require 是从何而来呢?带着这个问题,我们一起去看下吧。

启动流程

1. node 的目录结构,此处就不再分析了。最重要的就是 src 和 lib 了。 src 路径下是 node 的 C++ 实现的主要源码目录,而 lib 主要是 JavaScript 实现所在目录。稍微有一些 C++ 编程基础的同学应该知道,C++ 的启动函数就是 main 函数。那么 node 的启动函数在哪呢。通过全文搜索,可以确定,启动函数就在 src/node_main.cc 这个文件当中了。此处截取部分源码:

// windows 启动方法。
int wmain(int argc, wchar_t* wargv[]) {
  //...
  // 启动方法。
  return node::Start(argc, argv);
}
//...
// 类linux 启动方法。
int main(int argc, char* argv[]) {
    // ...
    // 启动方法。
    return node::Start(argc, argv);
}

可以看到,这个只是一个外壳,做了一些逻辑判断,最终的核心就是调用 Start 方法。

2. Start 方法位于 src/node.cc:

int Start(int argc, char** argv) {
    //...
    Init(&argc, const_cast<const char**>(argv), &exec_argc, &exec_argv); // 1.
    // v8 初始化。
    InitializeV8Platform(per_process_opts->v8_thread_pool_size);
    v8_initialized = true;
    // 开始事件循环。
    const int exit_code =
        Start(uv_default_loop(), args, exec_args);  // 2.
    //... v8 开始销毁。
    v8_initialized = false;
    V8::Dispose();
	//...
    return exit_code;
}

可以看到,Start 方法主要是执行了一个 Init 方法以及对 v8 进行了初始化的操作,然后开启了整个事件循环流程。

2.1 来看看 Init 方法做了些什么事情,同样位于 src/node.cc 中:

void Init(int* argc,
          const char** argv,
          int* exec_argc,
          const char*** exec_argv) {
  //... 注册内部模块。 此处暂时不细讲。
  RegisterBuiltinModules();
  //...  处理参数,打印 help 等。
  ProcessArgv(argv, exec_argv, false);
  //...
}

2.2 接着让我们看看里面这个 Start 方法做了什么。同样位于 src/node.cc 中:

inline int Start(uv_loop_t* event_loop,
                 const std::vector<std::string>& args,
                 const std::vector<std::string>& exec_args) {
  //... 开始创建 Isolate 实例。 
  Isolate* const isolate = NewIsolate(allocator.get(), event_loop);
  //...
  {
    //... 又是一个 Start 。
    exit_code = Start(isolate, isolate_data.get(), args, exec_args);
  }
  // isolate 销毁。
  isolate->Dispose();
  //...
  return exit_code;
}

参数检查什么的就略过了,上来先创建了一个 Isolate 实例,这个实例相当于是一个 js 独立环境,更粗略一点,比作一个页面。 中间又调用了一个 Start 方法,最终处理一下 isolate 的销毁。

3. 那接着来看这个 Start 方法(麻木了,都叫 Start 方法。)同样位于 src/node.cc 中:

inline int Start(Isolate* isolate, IsolateData* isolate_data,
                 const std::vector<std::string>& args,
                 const std::vector<std::string>& exec_args) {
  //... 创建一个 Context
  Local<Context> context = NewContext(isolate); // 1.
  //... 创建一个 Environment 实例,并开启 Start 方法。
  Environment env(isolate_data, context, v8_platform.GetTracingAgentWriter());
  env.Start(args, exec_args, v8_is_profiling); // 2.
  {
    //... 环境加载
    LoadEnvironment(&env);  // 3.
    //...
  }

  {
    //...
    do {
      // 事件循环启动。libuv 相关。 4.
      uv_run(env.event_loop(), UV_RUN_DEFAULT);
      //...
    } while (more == true);
    //...
  }
  //...
  const int exit_code = EmitExit(&env);
  //... 善后工作,资源回收等等。
  return exit_code;
}

Context 又是 v8 的一个概念,相当于执行上下文,js 的执行上下文,可以实现互不影响。比如一个页面上嵌套了某个页面,那么他们之间的 js 上下文环境就不一样。此处需要关注 1 , 2,3,4 四个方法。

3.1 先来看看 1 ,如何创建的 Context。NewContext 同样位于 src/node.cc 中:

Local<Context> NewContext(Isolate* isolate,
                          Local<ObjectTemplate> object_template) {
  // 使用 v8 的 api 创建 Context。 
  auto context = Context::New(isolate, nullptr, object_template);
  // ...
  {
    // ... Run lib/internal/per_context.js
    // 获取 per_context.js 文件的字符串。
    Local<String> per_context = NodePerContextSource(isolate);
    // 编译运行,v8的模板代码。
    ScriptCompiler::Source per_context_src(per_context, nullptr);
    Local<Script> s = ScriptCompiler::Compile(
        context,
        &per_context_src).ToLocalChecked();
    s->Run(context).ToLocalChecked();
  }
  return context;
}

此方法不仅仅创建了一个 Context,而且还预加载执行了一段js。注意这个 NodePerContextSource 方法只有编译过才会有这个文件。

3.1.1 看一下这个方法.文件位于node_javascript.cc 中:

v8::Local<v8::String> NodePerContextSource(v8::Isolate* isolate) {
    return internal_per_context_value.ToStringChecked(isolate);
}
static const uint8_t raw_internal_per_context_value[] = { 39,...}
static struct : public v8::String::ExternalOneByteStringResource {
    const char* data() const override {
        return reinterpret_cast<const char*>(raw_internal_per_context_value);
    }
    //...
    v8::Local<v8::String> ToStringChecked(v8::Isolate* isolate) {
        return v8::String::NewExternalOneByte(isolate, this).ToLocalChecked();
    }
} internal_per_context_value;

看到这里应该知道了,就是把 raw_internal_per_context_value 这个数组转成 v8 的字符串返回出去。那么问题来了,这个数组里面到底是什么东西呢。

3.1.2 猜也没法猜,那就打印一下呗。打印数组相关代码如下:

#include <string>
#include <iostream>
static const unsigned char raw_internal_per_context_value[] = {39,...}
int main() {
    std::cout << (char *)raw_internal_bootstrap_loaders_value << std::endl;
}

g++ -o test test.cc & ./test 就可以看到内容了。你会惊奇的发现,这不就是 lib/internal/per_context.js 文件的内容吗?是的,的确是这样,他就是把这段文本直接在编译期间就编成C++字符数组,为了在启动的时候加快启动速度,不至于现场去读文件从而引发文件加载速度的等等一系列问题。至于此 js 文件内容,在此先不做讲解。接着让我回到 4~5步的方法2当中。

**3.2 ** env.Start 方法位于 src/env.cc 中:

void Environment::Start(const std::vector<std::string>& args,
                        const std::vector<std::string>& exec_args,
                        bool start_profiler_idle_notifier) {
    //... 一大堆的 uv 操作等等。
    // 设置了 process。
    auto process_template = FunctionTemplate::New(isolate()); 
    process_template->SetClassName(FIXED_ONE_BYTE_STRING(isolate(), "process"));
    // ...
}

可以看到其中设置了 process 是什么,此处设置了之后,在js里面就可以直接拿到 process 变量了。

3.3 LoadEnvironment 方法在 src/node.cc 中:

void LoadEnvironment(Environment* env) {
  //...
  // 加载 lib/internal/bootstrap/loaders.js 和 node.js 进来。
  // FIXED_ONE_BYTE_STRING 就是一个转换字符串的宏。
  Local<String> loaders_name =
      FIXED_ONE_BYTE_STRING(env->isolate(), "internal/bootstrap/loaders.js");
  // LoadersBootstrapperSource 是获取 loaders.js 的文件内容。 GetBootstrapper 方法是用来
  // 执行 js 的。
  MaybeLocal<Function> loaders_bootstrapper =
      GetBootstrapper(env, LoadersBootstrapperSource(env), loaders_name);
  //...
  // 获取 global 对象
  Local<Object> global = env->context()->Global();
  //...
  // 暴露 global 出去,在 js 中可以访问。
  global->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "global"), global);

  // 创建bind,linked_binding,internal_binding
  Local<Function> get_binding_fn =
      env->NewFunctionTemplate(GetBinding)->GetFunction(env->context())
          .ToLocalChecked();
  //...

  // 执行 internal/loaders.js,node.js 里面的方法。
  if (!ExecuteBootstrapper(env, loaders_bootstrapper.ToLocalChecked(),
                           arraysize(loaders_bootstrapper_args),
                           loaders_bootstrapper_args,
                           &bootstrapped_loaders)) {
    return;
  }
  //...
}

static void GetBinding(const FunctionCallbackInfo<Value>& args) {
  // ... 通过参数获取模块名。
  Local<String> module = args[0].As<String>();
  //... 获取内部模块。此处就是通过2.1步骤中的 RegisterBuiltinModules 宏处理之后的东西来获取的。
  node_module* mod = get_builtin_module(*module_v);
  Local<Object> exports;
  if (mod != nullptr) {
    // 调用模块初始化方法。
    exports = InitModule(env, mod, module);
  }
  // ... 设置返回值。
  args.GetReturnValue().Set(exports);
}

代码很长,但是条理还是挺清晰的。这里进行了一些绑定操作和一些初始化方法的调用逻辑。此处也可以知道,GetBinding 类似的东西是什么。调用的 js 如何执行需要和 js 一起看才能明白。此处先不讲解了。

3.4 uv_run 这个方法此处也不细讲了。 libuv 这个库还没有详细了解。等待了解之后,补上 libuv 的相关调用分析,此处我们知道,在这里开始执行事件循环了。

结语

讲了这么多,大家应该对 nodejs 的启动流程有了一个大致的了解了吧。虽然开头说少点源码,可是后来还是夹杂了很多的源码,哈哈,有一种上当的感觉。后面再讲讲模块加载,libuv加载的相关东西。这次分析就到此结束吧,大家休息~