前言
在前一篇文章 #32 之后,对于 Nodejs 启动有了一个大致的想法。这篇文章将概括讲述 Nodejs 的 3 种主要启动方式
- 正常启动方式(用户脚本,REPL,stdin)
- child_process 启动方式
- worker_thread 启动方式
源码以 https://github.com/nodejs/node/tree/f2170253b694c488f8ad2616dfc5c66b6a3c90a0 为基础进行分析,可自行下载比对。分析止步于 V8 和 libuv,仅分析在 Nodejs 层的流程。
启动方式
正常启动方式
旅程从 node.gyp
开始。(没错,整个 Nodejs 构建和写其插件的构建方式相同,都是使用 node-gyp 构建工具构建)
{
'sources': [
'src/node_main.cc'
],
}
进入 src/node_main.cc 文件
int main(int argc, char* argv[]) {
// 省略:跨平台处理
// 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);
// Start 实现在 node.cc
return node::Start(argc, argv);
}
进入 node.cc
int Start(int argc, char** argv) {
// 省略:前置处理
{
// 省略:缓存处理
// 配置 libuv,为启动 Event Loop 作准备
uv_loop_configure(uv_default_loop(), UV_METRICS_IDLE_TIME);
// 创建 Node 主实例,并运行
NodeMainInstance main_instance(¶ms,
uv_default_loop(),
per_process::v8_platform.Platform(),
result.args,
result.exec_args,
indices);
// Run 的实现在 node_main_instance.cc 文件
result.exit_code = main_instance.Run(env_info);
}
TearDownOncePerProcess();
return result.exit_code;
}
进入 node_main_instance.cc。在此处,终于进入核心启动逻辑,重要的是在 Run 函数中调用这 2 个函数
LoadEnvironment
实现在 src/api/environment.cc
SpinEventLoop
实现在 src/api/embed_helpers.cc
LoadEnvironment
MaybeLocal<Value> LoadEnvironment(
Environment* env,
StartExecutionCallback cb) {
env->InitializeLibuv();
env->InitializeDiagnostics();
return StartExecution(env, cb);
}
初始化 libuv 等相关组件,最重要的是调用 StartExecution
执行用户写的脚本。当然在真正执行用户脚步之前,还有许多工作要做,下面我们简单分析一下。
StartExecution
有 2 个实现,函数接口分别如下:
MaybeLocal<Value> StartExecution(Environment* env, StartExecutionCallback cb)
MaybeLocal<Value> StartExecution(Environment* env, const char* main_script_id)
前者负责处理不同情况下以什么入口启动运行,下文要说的 worker_thread 启动也是在这个函数内判断;后者负责找到具体入口并编译和运行。让我们把目光聚焦到主模式下启动的代码
// 用户脚本启动方式
if (!first_argv.empty() && first_argv != "-") {
return StartExecution(env, "internal/main/run_main_module");
}
// REPL
if (env->options()->force_repl || uv_guess_handle(STDIN_FILENO) == UV_TTY) {
return StartExecution(env, "internal/main/repl");
}
// stdin
return StartExecution(env, "internal/main/eval_stdin");
在了解了针对不同模式的分类处理之后,我们继续分析「真正」的 StartExecution
方法实现,其内部调用链关系
StartExecution -> ExecuteBootstrapper -> LookupAndCompile
ExecuteBootstrapper
内部会调用 LookupAndCompile
,LookupAndCompile
会根据前面不同模式下的分类,找到对应入口文件并经过 V8 处理成可调用的 Function
MaybeLocal<Value> ExecuteBootstrapper(Environment* env,
const char* id,
std::vector<Local<String>>* parameters,
std::vector<Local<Value>>* arguments) {
MaybeLocal<Function> maybe_fn =
NativeModuleEnv::LookupAndCompile(env->context(), id, parameters, env);
// 此处调用编译成功的用户代码,也就是用户代码在此时开始执行
MaybeLocal<Value> result = fn->Call(env->context(),
Undefined(env->isolate()),
arguments->size(),
arguments->data());
// ...
}
下面我们简单看一下 LookupAndCompile
是如何实现。其实现在 src/node_native_module.cc 文件,调用关系
LookupAndCompile -> CompileFunctionInContext
CompileFunctionInContext
已经来到 V8 API 层面,到此已经到达笔者极限。后续如果有能力再进一步探索。
关于 Nodejs 是如何读取用户代码,并交给 V8 进行编译和执行过程就如上所示了。我们回过头看一下,以用户脚本为例,其「引导」过程是什么样子。
用户脚本的入口脚本:internal/main/run_main_module.js
require('internal/bootstrap/pre_execution');
require('internal/modules/cjs/loader').Module.runMain(process.argv[1]);
我们稍微看一下 internal/modules/cjs/loader.js,这个是以 CJS 规范的模块加载,后续还有以 ESM 规范的加载方式(internal/process/esm_loader.js),就请读者自己查看啦:)
在 loader 文件内,会构造 exports, require, module, __filename, __dirname
包裹用户代码;还有对不同文件的加载处理,.js, .node, .json
等
SpinEventLoop
该部分主要是调用 libuv 函数 uv_run
Maybe<int> SpinEventLoop(Environment* env) {
{
do {
// 调用 uv_run 开启 Event Loop
uv_run(env->event_loop(), UV_RUN_DEFAULT);
more = uv_loop_alive(env->event_loop());
if (more && !env->is_stopping()) continue;
if (EmitProcessBeforeExit(env).IsNothing())
break;
// Emit `beforeExit` if the loop became alive either after emitting
// event, or after running some callbacks.
more = uv_loop_alive(env->event_loop());
} while (more == true && !env->is_stopping());
}
}
总结
从目前的代码分析来看,V8 执行用户代码和 Event Loop 是在同一个线程内,这个也能过验证长时间运行用户代码会「饿死」Event Loop。
Event Loop 本身也只使用一个线程,Nodejs 主实例最终会「卡」在 uv_run
,直到循环结束。
child_process 启动方式
child_process 的启动方式以 spawn 为线索开始搜寻。我们从 lib/child_process.js 开始
function spawn(file, args, options) {
// 删去细节,保留主干
const child = new ChildProcess();
return child;
}
ChildProcess
来自 internal/child_process.js
function ChildProcess() {
FunctionPrototypeCall(EventEmitter, this);
// 实例化 Process
this._handle = new Process();
}
ObjectSetPrototypeOf(ChildProcess.prototype, EventEmitter.prototype);
ObjectSetPrototypeOf(ChildProcess, EventEmitter);
ChildProcess.prototype.spawn = function(options) {
const err = this._handle.spawn(options);
}
Process
来自内部模块 process_wrap
。该模块实现在 src/process_wrap.cc
// 注册模块
NODE_MODULE_CONTEXT_AWARE_INTERNAL(process_wrap, node::ProcessWrap::Initialize)
// 绑定 Spawn 函数实现
env->SetProtoMethod(constructor, "spawn", Spawn);
// Spawn 函数实现
static void Spawn(const FunctionCallbackInfo<Value>& args) {
// 调用 uv_spawn 能力,创建进程执行
// process_ 是 uv_process_t,可以理解为从 libuv 获取结果的句柄
// options 是 uv_process_options_s,描述了执行的文件地址,参数等创建进程必须的信息
int err = uv_spawn(env->event_loop(), &wrap->process_, &options);
}
可以看到,最终调用的是 libuv 的 uv_spawn 能力
worker_thread 启动方式
worker_thread 启动方式详见 #32 ,这里不再赘述。
深入阅读
Nodejs
libuv
现代 C++
V8