[NodeJS源码探秘]之启动全流程
文章目录
NodeJS是时下非常流行的服务器语言, 这个系列将着重研究NodeJS的源码,以期为之做出贡献。
这篇文章将研究NodeJS的启动全流程。
NodeJS的启动全流程涉及到以下几个主要的文件:
- src/node_main.cc
- src/node.cc
- src/env.cc
- lib/internal/bootstrap/loader.js
- lib/internal/bootstrap/node.js
首先,我们看node_main.cc。这个文件源码比较简单,主要是根据操作系统的不同运行了对应的变量设置。最重要的一行代码就是调用node.cc中的__Start()__函数
int main(int argc, char *argv[]) {
#if defined(__POSIX__) && defined(NODE_SHARED_MODE)
// ...
#endif
#if defined(__linux__)
// ...
#endif
// Disable stdio buffering, it interacts poorly with printf()
// calls elsewhere in the program (e.g., any logging from V8.)
setvbuf(stdout, nullptr, _IONBF, 0);
setvbuf(stderr, nullptr, _IONBF, 0);
return node::Start(argc, argv); // <- 从这里,我们进入node.cc文件
}
#endif
node.cc里面有三个__Start()__函数,逐级调用,让我们按照调用的顺序来看一下每个__Start()__都干了什么:
首先看第一个Start():
int Start(int argc, char** argv) {
// ...处理argc和argv, 在此省略
// v8_platform是一个匿名struct, 定义了若干涉及V8引擎的辅助函数
v8_platform.Initialize(v8_thread_pool_size);
// 在这里V8被初始化
V8::Initialize();
performance::performance_v8_start = PERFORMANCE_NOW();
v8_initialized = true;
// 在V8初始化成功之后,我们会进入第二个Start()函数
// 请特别关注此函数的第一个参数
// 我们很惊喜的发现了有关libuv的代码!
// uv_default_loop顾名思义,会创建一个默认配置的event loop!
const int exit_code =
Start(uv_default_loop(), argc, argv, exec_argc, exec_argv);
// ...一些在Node退出时所需要的清理工作,在此省略
return exit_code;
}
现在看第二个Start():
inline int Start(uv_loop_t* event_loop,
int argc, const char* const* argv,
int exec_argc, const char* const* exec_argv) {
// 这里,我们生成一个V8的Isolate。
// 一个Isolate是一份独立的V8 runtime,
// 包括但不限于一个heap管理器,垃圾回收器等。
// 在一个时间段内,有且只有一个线程能使用此Isolate。
Isolate* const isolate = Isolate::New(params);
if (isolate == nullptr)
return 12; // Signal internal error.
// ...给这个isolate定义了一些回调函数,在此省略
int exit_code;
{
// 在看NodeJS源码的时候总是会碰到以下代码
// 总是要申明Scope,有isolate_scope, handle_scope等
// 只需要知道isolate_scope是让v8步入指定的isolate
// handle_scope是让步入特定isolate之后的v8创建一个
// 具有垃圾回收功能的工作区,我们可以在此工作区中
// 索引多个JS对象。
Locker locker(isolate);
Isolate::Scope isolate_scope(isolate);
HandleScope handle_scope(isolate);
// IsolateData是个辅助类
// 它是我们以后在Node中获得此前定义的
// isolate, event_loop的统一入口
// 它在env.cc中被定义
IsolateData isolate_data(
isolate,
event_loop,
v8_platform.Platform(),
allocator.zero_fill_field());
if (track_heap_objects) {
isolate->GetHeapProfiler()->StartTrackingHeapObjects(true);
}
// 在这里,我们进入第三个Start()
// 由isolate参数我们便知道,这个Start()要开始编译我们的JS代码了!
// 而且它还用到了isolate_data,其中有event loop,
// 所以相应的,我们肯定会看到很多libuv的代码!
exit_code = Start(isolate, &isolate_data, argc, argv, exec_argc, exec_argv);
}
}
让我们来看看第三个Start():
inline int Start(Isolate* isolate, IsolateData* isolate_data,
int argc, const char* const* argv,
int exec_argc, const char* const* exec_argv) {
HandleScope handle_scope(isolate);
// 一个Context是一个V8的上下文,它存在于一个特定的Isolate中
// 一个Isolate可以有多个Context,且Context可以嵌套
// context_scope就是步入指定的context
Local<Context> context = NewContext(isolate);
Context::Scope context_scope(context);
// 请关注下面的这个env,以后我们会经常看到他!
// env顾名思义是是一个抽象出来的node运行环境
// 它记录了v8的实例,其对应的isolate,
// 也记录了libuv生成的event loop
// 我们以后如果想获得event loop就可以使用env的函数调取
Environment env(isolate_data, context);
// 好了,我们有碰到了一个Start()
// 这个Start()最主要的做了两件事情:
// 1. 设置libuv的一些handle函数,最最重要的两个prepare和idle
// 2. 设置了我们未来在JS中会用到的全局变量process在C++中的原型
// 这两个在此就不展开了,将在独立的文章中解析!
env.Start(argc, argv, exec_argc, exec_argv, v8_is_profiling);
const char* path = argc > 1 ? argv[1] : nullptr;
StartInspector(&env, path, debug_options);
if (debug_options.inspector_enabled() && !v8_platform.InspectorStarted(&env))
return 12; // Signal internal error.
env.set_abort_on_uncaught_exception(abort_on_uncaught_exception);
if (no_force_async_hooks_checks) {
env.async_hooks()->no_force_checks();
}
// 在这个代码块中,发生了很多重要的事情
// 最主要的就是Node加载原生模块
// 以及编译我们自己的JS代码!
// 因为这个要涉及另外几个文件的代码,
// 为了方便,此代码块的细节留在后面再讨论
{
Environment::AsyncCallbackScope callback_scope(&env);
env.async_hooks()->push_async_ids(1, 0);
LoadEnvironment(&env);
env.async_hooks()->pop_async_id(1);
}
env.set_trace_sync_io(trace_sync_io);
{
SealHandleScope seal(isolate);
bool more;
env.performance_state()->Mark(
node::performance::NODE_PERFORMANCE_MILESTONE_LOOP_START);
// 在这里我们是真正的步入到了event loop!
do {
// uv_run会执行一个对应event loop的所有阶段
// 这些阶段在libuv的文档中都有记载
uv_run(env.event_loop(), UV_RUN_DEFAULT);
v8_platform.DrainVMTasks(isolate);
more = uv_loop_alive(env.event_loop());
if (more)
continue;
// 当我们的event loop中没有待处理的事件时,
// NodeJS会在此函数内抛出一个'beforeExit'时间,
// 我们可以在自己的JS代码中捕捉此时间并执行代码
// 可以同步,也可以异步
RunBeforeExit(&env);
// 如果在RunBeforeExit中,
// 我们的JS代码给event loop新增了事件,
// 那么,我们是不会真的结束进程的!
// 所以收到'beforeExit'并不意味着一定会退出进程
more = uv_loop_alive(env.event_loop());
} while (more == true);
env.performance_state()->Mark(
node::performance::NODE_PERFORMANCE_MILESTONE_LOOP_EXIT);
}
env.set_trace_sync_io(false);
// 这里会抛出'exit'事件, 可以看到
// 这里的回调已经不在我们libuv的event loop中了
// 所以在这里我们无法执行任何异步代码
// 同时这里是真的要退出进程了!
const int exit_code = EmitExit(&env);
RunAtExit(&env);
v8_platform.DrainVMTasks(isolate);
v8_platform.CancelVMTasks(isolate);
WaitForInspectorDisconnect(&env);
#if defined(LEAK_SANITIZER)
__lsan_do_leak_check();
#endif
return exit_code;
}
通过这三个Start()函数,我们大体了解了NodeJS的一个启动流程。我们知道了Node会先初始化V8实例,并使用唯一一个Isolate来运行我们所有的JS代码,然后设置了libuv的event loop,加载内置模块和编译我们的JS文件,最后进入event loop。这个流程中,与我们NodeJS使用者最相关的,应该就是加载和编译我们自己的JS文件了。关于这部分的分析,我们将在下一篇文章中仔细讨论。