在前面几篇文章介绍到v8,addon,libuv等知识后,现在终于可以有信心看node的源码了,对一个软件来说,启动和关闭是短暂的,但又是整个软件架构很关键的地方,一个设计良好的软件:在启动的时候快速稳定;运行的时候内存无泄漏,cup占用稳定有规律,服务可靠有包装;关闭的时候无错误。我们看看node是否认真考虑了这些。
准备
我是在windows下面用visual studio看代码的,先要做好准备工作:
- 下载node代码:https://github.com/nodejs/node.git
- 切换到自己想看的branch,我看的是v0.11.13
- 生成vs工程文件,详细步骤可以看BUILDING文档
-
打开工程,linux用户体会不到这种快感
运行
可以用脚本编译,也可以用visual studio运行。
- 上面的第三部使用
vcbuild nosign
,就会编译完,生成node.exe。
-
通过visual studio运行
node和我们平时写的程序也是一样生成exe文件,呵呵,感觉也没有那么神秘了。下面我们去看启动代码吧。node可是集成了v8和libuv,应该是蛮复杂的吧。
入口
代码这么多,怎么找到入口呢。幸亏我们有IDE,直接单步调试(F11)好了。发现代码在wmain停了下来。
虽然大部分软狗对此IDE使用场景很熟悉,但不排除还有一大部分开发linux系统的人还在用命令行和vim来看代码,实际上linux上面的用户可以尝试一下Jetbrain的IDE
启动
由于代码有大量宏来处理跨平台差异和简化代码,下面的阅读不会关注这些宏,主要看如何启动libuv,v8和node内置模块的加载;文章也不会解释libuv和v8的相关api,因为前面有文章介绍过了。
好了,从node.cc的int Start(int argc, char** argv) {
函数慢慢看吧。
初始化参数
在Start函数中,第一个调用的函数是Init,从名字来看便略知一二。
void Init(int* argc,
const char** argv,
int* exec_argc,
const char*** exec_argv) {
这个函数处理一些初始化的工作,解析用户传入的参数,设置debug的相关的信息。这个版本的代码v8和libuv是混在一起的,在我看来是需要重构的。可见外国高手写代码也是先码功能。
初始化v8
下面的代码说明,在启动libuv循环前,先给v8实例node_isolate
加了一个锁。这样保证当前node线程才能使用v8
V8::Initialize();
{
Locker locker(node_isolate);
Environment* env =
CreateEnvironment(node_isolate, argc, argv, exec_argc, exec_argv);
// This Context::Scope is here so EnableDebug() can look up the current
// environment with Environment::GetCurrentChecked().
// TODO(bnoordhuis) Reorder the debugger initialization logic so it can
// be removed.
{
Context::Scope context_scope(env->context());
CreateEnvironment
创建了一个process
对象(在JavaScript中),完成了进程相关信息的保存和一些全局设置。我们再测试一下process对象到底有什么:
可见,我们如果想知道当前node一些全局的信息比如版本,可以通过process对象拿到。
另外,
CreateEnvironment
调用Load
加载src/node.js,后面再看。
初始化libuv循环
通过下面代码,我们可以看到:
- uv_run启动事件循环
- 设置循环模式为UV_RUN_ONCE,这样node会自动停止(如果没有要处理的handle的话)
- EmitBeforeExit调用env中回调函数emit。
- 如果还有新产生的需要处理的事物,继续循环。
注意:这里又调用一次UV_RUN_NOWAIT,可能是因为uv_run比uv_loop_alive有更多语义,这里即用uv_loop_alive,又用UV_RUN_NOWAIT,至少代码不够清晰,可以考虑重构一下。
do {
more = uv_run(env->event_loop(), UV_RUN_ONCE);
if (more == false) {
EmitBeforeExit(env);
// Emit `beforeExit` if the loop became alive either after emitting
// event, or after running some callbacks.
more = uv_loop_alive(env->event_loop());
if (uv_run(env->event_loop(), UV_RUN_NOWAIT) != 0)
more = true;
}
} while (more == true);
加载Node.js
前面说到CreateEnvironment
调用Load
加载src/node.js。现在我们看看node.js有哪些功能。同时,node.js又加载了一些native模块,我们看看这互相加载到底怎么弄的。
- CreateEnvironment函数
Environment* CreateEnvironment(Isolate* isolate,
int argc,
const char* const* argv,
int exec_argc,
const char* const* exec_argv) {
......
Load(env);
......
- load函数
void Load(Environment* env) {
HandleScope handle_scope(env->isolate());
// Compile, execute the src/node.js file. (Which was included as static C
// string in node_natives.h. 'natve_node' is the string containing that
// source code.)
......
Local<String> script_name = FIXED_ONE_BYTE_STRING(env->isolate(), "node.js");
Local<Value> f_value = ExecuteString(env, MainSource(env), script_name);
Local<Function> f = Local<Function>::Cast(f_value);
......
}
我们注意一下ExecuteString干了什么
- ExecuteString
// Executes a str within the current v8 context.
static Local<Value> ExecuteString(Environment* env,
Handle<String> source,
Handle<String> filename) {
......
Local<v8::Script> script = v8::Script::Compile(source, filename);
......
Local<Value> result = script->Run();
if (result.IsEmpty()) {
ReportException(env, try_catch);
exit(4);
}
return scope.Escape(result);
}
调试的时候, Handle<String>并不能在调试器看到值,可以用下面的代码打印一下
v8::String::Utf8Value param1(script_name);
或者下载Visual Studio Debugger Visualizers
git clone https://chromium.googlesource.com/chromium/src/tools/win
上面是chrome的调试工具,调试v8的时候好像不好用,
- v8::Script::Compile(source, filename);
这里进入v8编译JavaScript环节了。不在往下挖掘,因为这样对分析Node的启动没有什么好处。我们还是去看看node.js做了什么。
Node.js文件
我们看看node.js文件头部。
// Hello, and welcome to hacking node.js!
//
// This file is invoked by node::Load in src/node.cc, and responsible for
// bootstrapping the node.js core. Special caution is given to the performance
// of the startup process, so many dependencies are invoked lazily.
(function(process) {
this.global = this;
......
});
- 定义了一个函数。
- 在src/node.cc中被
node::Load
调用。 - 为了加快速度,很多依赖都延迟加载了。
我们研究一下他加载了什么。
- 第一部分: startup
node.js定义了一个startup函数并调用,startup函数中使用NativeModule去加载很多模块
(function(process) {
this.global = this;
function startup() {
......
}
startup.globalVariables = function() {
......
};
startup();
- 第二部分:定义NativeModule的加载机制
// Below you find a minimal module system, which is used to load the node
// core modules found in lib/*.js. All core modules are compiled into the
// node binary, so they can be loaded faster.
var ContextifyScript = process.binding('contextify').ContextifyScript;
function runInThisContext(code, options) {
var script = new ContextifyScript(code, options);
return script.runInThisContext();
}
function NativeModule(id) {
......
}
......
对于NativeModule,我们要仔细看看,到底是怎么加载的。。。
NativeModule
- 什么是native模块
这里的native并不是c++代码,而是js,从图中可以看到node自带了很多js。为了加快加载速度,把这些js通过一个python工具转成了node_natives.h
这个头文件,然后直接编译到node.exe中。
从代码中可以看到node.js也被放到头文件了。
- NativeModule如何加载模块
1. 导入natives
NativeModule._source = process.binding('natives');
这个natives就是在头文件中定义的数据。
process.binding
实在node.cc
中定义的:NODE_SET_METHOD(process, "binding", Binding);
。上面看到的自带的js大量使用这个函数加载C++模块。
2. NativeModule.require
函数
js都是使用require函数来加载模块,只不过这个require也是普通的函数而已,并不是语言本身支持的。我们看看代码。
NativeModule.require = function(id) {
if (id == 'native_module') { //在module模块还会require('native_module')
return NativeModule;
}
var cached = NativeModule.getCached(id);
if (cached) {
return cached.exports;
}
if (!NativeModule.exists(id)) {
throw new Error('No such native module ' + id);
}
process.moduleLoadList.push('NativeModule ' + id);
var nativeModule = new NativeModule(id);
nativeModule.cache();
nativeModule.compile();
return nativeModule.exports;
};
3. compile函数
这里看一下compile函数。实际上只是在第一步的数组中查找。
NativeModule.prototype.compile = function() {
var source = NativeModule.getSource(this.id);
source = NativeModule.wrap(source);
var fn = runInThisContext(source, { filename: this.filename });
fn(this.exports, NativeModule.require, this, this.filename);
this.loaded = true;
};
wrap就是包装了一个函数
NativeModule.wrap = function(script) {
return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
};
NativeModule.wrapper = [
'(function (exports, require, module, __filename, __dirname) { ',
'\n});'
];
接下来看一下runInThisContext
4. runInThisContext
var ContextifyScript = process.binding('contextify').ContextifyScript;
function runInThisContext(code, options) {
var script = new ContextifyScript(code, options);
return script.runInThisContext();
}
node.js文件中的js代码只是调用了C++,我们得看一下C++代码。
static void RunInThisContext(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
HandleScope handle_scope(isolate);
// Assemble arguments
TryCatch try_catch;
uint64_t timeout = GetTimeoutArg(args, 0);
bool display_errors = GetDisplayErrorsArg(args, 0);
if (try_catch.HasCaught()) {
try_catch.ReThrow();
return;
}
// Do the eval within this context
Environment* env = Environment::GetCurrent(isolate);
EvalMachine(env, timeout, display_errors, args, try_catch);
}
EvalMachine不看了,v8运行代码了。
总结
上面主要的逻辑都在CreateEnvironment
和node.js
中,从c++掉用到js,再从js调用c++,js调用js。着实复杂。
启动过程大致如下:
- 初始化v8
- 创建process对象
- 加载node.js
- node.js reqiure更多模块
- native模块加载完毕
- 如果传入文件,比如node myIndex.js,加载用户模块(startup函数中处理)
- libuv循环建立
- 等待或者结束(根据启动参数不同)
我们可以看到为了加快native模块的加载速度,采用了把js编译成.h文件的方法,我们如果想加快启动速度也可以这么干。
如此粗糙的过程虽然不能完全了解到node启动的过程,很多细节有带进一步研究,但是我们至少又前进了一些。_