nodejs启动流程分析

前言

之前用过一段时间的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加载的相关东西。这次分析就到此结束吧,大家休息~

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

推荐阅读更多精彩内容

  • 新项目预研在KittenBlock上添加我们接口的Block,里面用到了NWJS,跟以往的Electron框架类似...
    JomarWu阅读 6,029评论 2 1
  • 前言 js是从网页小脚本演变过来的,至今,前端的js库,也不像一个真正的模块。前端js经历了工具类库、组件库、前端...
    白昔月阅读 3,269评论 2 11
  • 「年轻人,你的问题主要在于读书不多而想得太多。」 --- 杨绛 2015 年读了不少书,收获也不少,到了年底写个小...
    yyqian阅读 529评论 0 0
  • 据调查,脑瘫已成为儿童疾病的高发病,而导致脑瘫居高不下的原因也是各种各样,由于脑瘫是因为某种病因而损害了大脑部份,...
    孟青阅读 246评论 0 1
  • 虞美人 千里相思人寂寥。惆怅明月桥。北地南疆万里遥。鸿雁难寄,空守锦书老。...
    昭兮未央阅读 332评论 0 0