相信不少的人,天天在终端不止一遍的执行着node
这条命令,对于不少人来讲,它就像一个黑盒,并不知道背后到底发生了什么,本文将会为你们揭开这个神秘的面纱,因为本人水平有限,因此只是讲一个大概其,主要关注的过程就是node
模块的初始化,event loop
和v8
的部分基本没有深刻,这些部分能够关注一下我之后的文章。(提示本文很是的长,但愿你们不要看烦~)javascript
这个问题不少人都会回答就是v8
+ libuv
,可是除了这个两个库之外node
还依赖许多优秀的开源库,能够经过process.versions
来看一下:
html
http_parser
主要用于解析http数据包的模块,在这个库的做者也是ry
,一个纯c
的库,无任何依赖v8
这个你们就很是熟悉了,一个优秀的js
引擎uv
这个就是ry
实现的libuv
,其封装了libev
和IOCP
,实现了跨平台,node
中的i/o
就是它,尽管js
是单线程的,可是libuv
并非,其有一个线程池来处理这些i/o
操做。zlib
主要来处理压缩操做,诸如熟悉的gzip
操做ares
是c-ares
,这个库主要用于解析dns
,其也是异步的modules
就是node
的模块系统,其遵循的规范为commonjs
,不过node
也支持了ES
模块,不过须要加上参数而且文件名后缀须要为mjs
,经过源码看,node
将ES
模块的名称做为了一种url
来看待,具体能够参见这里 nghttp2
如其名字同样,是一个http2
的库napi
是在node8
出现,node10
稳定下来的,能够给编写node
原生模块更好的体验(终于不用在依赖于nan
,每次更换node
版本还要从新编译一次了)openssl
很是著名的库,tls
模块依赖于这个库,固然还包括https
icu
就是small-icu
,主要用于解决跨平台的编码问题,versions
对象中的unicode
,cldr
,tz
也源自icu
,这个的定义能够参见这里 从这里能够看出的是process
对象在node
中很是的重要,我的的理解,其实node
与浏览器端最主要的区别,就在于这个process
对象java
注:node
只是用v8
来进行js
的解析,因此不必定非要依赖v8
,也能够用其余的引擎来代替,好比利用微软的ChakraCore
,对应的node仓库node
通过上面的一通分析,对node
的全部依赖有了必定的了解,下面来进入正题,看一下node
的初始化过程:python
node_main.cc
为入口文件,能够看到的是除了调用了node::Start
以外,还作了两件事情:c++
SIGPIPE
信号出现的状况通常在socket
收到RST packet
以后,扔向这个socket
写数据时产生,简单来讲就是client
想server
发请求,可是这时候client
已经挂掉,这时候就会产生SIGPIPE
信号,产生这个信号会使server
端挂掉,其实node::PlatformInit
中也作了这种操做,不过只是针对non-shared lib build
git
stdout
的默认缓冲行为为_IOLBF
(行缓冲),可是对于这种来讲交互性会很是的差,因此将其改成_IONBF
(不缓冲)github
node.cc
文件中总共有三个Start
函数,先从node_main.cc
中掉的这个Start
函数开始看:json
int Start(int argc, char** argv) { // 退出以前终止libuv的终端行为,为正常退出的状况 atexit([] () { uv_tty_reset_mode(); }); // 针对平台进行初始化 PlatformInit(); // ... Init(&argc, const_cast<const char**>(argv), &exec_argc, &exec_argv); // ... v8_platform.Initialize(v8_thread_pool_size); // 熟悉的v8初始化函数 V8::Initialize(); // .. const int exit_code = Start(uv_default_loop(), argc, argv, exec_argc, exec_argv); }
上面函数只保留了一些关键不走,先来看看PlatformInit
bootstrap
unix
中将一切都看做文件,进程启动时会默认打开三个i/o
设备文件,也就是stdin stdout stderr
,默认会分配0 1 2
三个描述符出去,对应的文件描述符常量为STDIN_FILENO STDOUT_FILENO STDERR_FILENO
,而windows
中没有文件描述符的这个概念,对应的是句柄,PlatformInit
首先是检查是否将这个三个文件描述符已经分配出去,若没有,则利用open("/dev/null", O_RDWR)
分配出去,对于windows
作了一样的操做,分配句柄出去,并且windows
只作了这一个操做;对于unix
来讲还会针对SIGINT
(用户调用Ctrl-C时发出)和SIGTERM
(SIGTERM
与SIGKILL
相似,可是不一样的是该信号能够被阻塞和处理,要求程序本身退出)信号来作一些特殊处理,这个处理与正常退出时同样;另外一个重要的事情就是下面这段代码:
struct rlimit lim; // soft limit 不等于 hard limit, 意味着能够增长 if (getrlimit(RLIMIT_NOFILE, &lim) == 0 && lim.rlim_cur != lim.rlim_max) { // Do a binary search for the limit. rlim_t min = lim.rlim_cur; rlim_t max = 1 << 20; // But if there's a defined upper bound, don't search, just set it. if (lim.rlim_max != RLIM_INFINITY) { min = lim.rlim_max; max = lim.rlim_max; } do { lim.rlim_cur = min + (max - min) / 2; // 对于mac来讲 hard limit 为unlimited // 可是内核有限制最大的文件描述符,超过这个限制则设置失败 if (setrlimit(RLIMIT_NOFILE, &lim)) { max = lim.rlim_cur; } else { min = lim.rlim_cur; } } while (min + 1 < max); }
这件事情也就是提升一个进程容许打开的最大文件描述符,可是在mac
上很是的奇怪,执行ulimit -H -n
获得hard limit
是unlimited
,因此我认为mac
上的最大文件描述符会被设置为1 << 20
,可是最后通过实验发现最大只能为24576
,很是的诡异,最后通过一顿搜索,查到了原来mac
的内核对能打开的文件描述符也有限制,能够用sysctl -A | grep kern.maxfiles
进行查看,果真这个数字就是24576
Init
函数调用了RegisterBuiltinModules
:
// node.cc void RegisterBuiltinModules() { #define V(modname) _register_##modname(); NODE_BUILTIN_MODULES(V) #undef V } // node_internals.h #define NODE_BUILTIN_MODULES(V) \ NODE_BUILTIN_STANDARD_MODULES(V) \ NODE_BUILTIN_OPENSSL_MODULES(V) \ NODE_BUILTIN_ICU_MODULES(V)
从名字也能够看出上面的过程是进行c++
模块的初始化,node
利用了一些宏定义的方式,主要关注NODE_BUILTIN_STANDARD_MODULES
这个宏:
#define NODE_BUILTIN_STANDARD_MODULES(V) \ V(async_wrap) \ V(buffer) ...
结合上面的定义,能够得出编译后的代码大概为:
void RegisterBuiltinModules() { _register_async_wrap(); _register_buffer(); }
而这些_register
又是从哪里来的呢?以buffer
来讲,对应c++
文件为src/node_buffer.cc
,来看这个文件的最后一行,第二个参数是模块的初始化函数:
NODE_BUILTIN_MODULE_CONTEXT_AWARE(buffer, node::Buffer::Initialize)
这个宏存在于node_internals.h
中:
#define NODE_MODULE_CONTEXT_AWARE_CPP(modname, regfunc, priv, flags) static node::node_module _module = { NODE_MODULE_VERSION, flags, nullptr, __FILE__, nullptr, (node::addon_context_register_func) (regfunc),// 暴露给js使用的模块的初始化函数 NODE_STRINGIFY(modname), priv, nullptr }; void _register_ ## modname() { node_module_register(&_module); } #define NODE_BUILTIN_MODULE_CONTEXT_AWARE(modname, regfunc) NODE_MODULE_CONTEXT_AWARE_CPP(modname, regfunc, nullptr, NM_F_BUILTIN)
发现调用的_register_buffer
实质上调用的是node_module_register(&_module)
,每个c++
模块对应的为一个node_module
结构体,再来看看node_module_register
发生了什么:
extern "C" void node_module_register(void* m) { struct node_module* mp = reinterpret_cast<struct node_module*>(m); if (mp->nm_flags & NM_F_BUILTIN) { mp->nm_link = modlist_builtin; modlist_builtin = mp; } ... }
由此能够见,c++
模块被存储在了一个链表中,后面process.binding()
本质上就是在这个链表中查找对应c++
模块,node_module
是链表中的一个节点,除此以外Init
还初始化了一些变量,这些变量基本上都是取决于环境变量用getenv
得到便可
到执行完Init
为止,尚未涉及的js
与c++
的交互,在将一些环境初始化以后,就要开始用v8
这个大杀器了,v8_platform
是一个结构体,能够理解为是node
对于v8
的v8::platform
一个封装,紧接着的就是对v8
进行初始化,自此开始具有了与js
进行交互的能力,初始化v8
以后,建立了一个libuv
事件循环就进入了下一个Start
函数
inline int Start(uv_loop_t* event_loop, int argc, const char* const* argv, int exec_argc, const char* const* exec_argv) { std::unique_ptr<ArrayBufferAllocator, decltype(&FreeArrayBufferAllocator)> allocator(CreateArrayBufferAllocator(), &FreeArrayBufferAllocator); Isolate* const isolate = NewIsolate(allocator.get()); // ... { Locker locker(isolate); Isolate::Scope isolate_scope(isolate); HandleScope handle_scope(isolate); } }
首先建立了一个v8
的Isolate
(隔离),隔离在v8
中很是常见,仿佛和进程同样,不一样隔离不共享资源,有着本身得堆栈,可是正是由于这个缘由在多线程的状况下,要是对每个线程都建立一个隔离的话,那么开销会很是的大(可喜可贺的是node
有了worker_threads
),这时候能够借助Locker
来进行同步,同时也保证了一个Isolate
同一时刻只能被一个线程使用;下面两行就是v8
的常规套路,下一步通常就是建立一个Context
(最简化的一个流程能够参见v8
的hello world),HandleScope
叫作句柄做用域,通常都是放在函数的开头,来管理函数建立的一些句柄(水平有限,暂时不深究,先挖个坑);第二个Start
的主要流程就是这个,下面就会进入最后一个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); // 常规套路 Local<Context> context = NewContext(isolate); Context::Scope context_scope(context); Environment env(isolate_data, context, v8_platform.GetTracingAgentWriter()); env.Start(argc, argv, exec_argc, exec_argv, v8_is_profiling); // ...
能够见到v8
的常见套路,建立了一个上下文,这个上下文就是js
的执行环境,Context::Scope
是用来管理这个Context
,Environment
能够理解为一个node
的运行环境,记录了isolate,event loop
等,Start
的过程主要是作了一些libuv
的初始化以及process
对象的定义:
auto process_template = FunctionTemplate::New(isolate()); process_template->SetClassName(FIXED_ONE_BYTE_STRING(isolate(), "process")); auto process_object = process_template->GetFunction()->NewInstance(context()).ToLocalChecked(); set_process_object(process_object); SetupProcessObject(this, argc, argv, exec_argc, exec_argv);
SetupProcessObject
生成了一个c++
层面上的process
对象,这个已经基本上和平时node
中的process
对象一致,可是还会有一些出入,好比没有binding
等,完成了这个过程以后就开始了LoadEnvironment
Local<String> loaders_name = FIXED_ONE_BYTE_STRING(env->isolate(), "internal/bootstrap/loaders.js"); MaybeLocal<Function> loaders_bootstrapper = GetBootstrapper(env, LoadersBootstrapperSource(env), loaders_name); Local<String> node_name = FIXED_ONE_BYTE_STRING(env->isolate(), "internal/bootstrap/node.js"); MaybeLocal<Function> node_bootstrapper = GetBootstrapper(env, NodeBootstrapperSource(env), node_name);
先将lib/internal/bootstrap
文件夹下的两个文件读进来,而后利用GetBootstrapper
来执行js
代码分别获得了一个函数,一步步来看,先看看GetBootstrapper
为何能够执行js
代码,查看这个函数能够发现主要是由于ExecuteString
:
MaybeLocal<v8::Script> script = v8::Script::Compile(env->context(), source, &origin); ... MaybeLocal<Value> result = script.ToLocalChecked()->Run(env->context());
这个主要利用了v8
的能力,对js
文件进行了解析和执行,打开loaders.js
看看其参数,须要五个,捡两个最重要的来讲,分别是process
和getBinding
,这里面日后继续看LoadEnvironment
发现process
对象就是刚刚生成的,而getBinding
是函数GetBinding
:
node_module* mod = get_builtin_module(*module_v); Local<Object> exports; if (mod != nullptr) { exports = InitModule(env, mod, module); } else if (!strcmp(*module_v, "constants")) { exports = Object::New(env->isolate()); CHECK(exports->SetPrototype(env->context(), Null(env->isolate())).FromJust()); DefineConstants(env->isolate(), exports); } else if (!strcmp(*module_v, "natives")) { // NativeModule _source exports = Object::New(env->isolate()); DefineJavaScript(env, exports); } else { return ThrowIfNoSuchModule(env, *module_v); } args.GetReturnValue().Set(exports);
其做用就是根据传参来初始化指定的模块,固然也有比较特殊的两个分别是constants
和natives
(后面再看),get_builtin_module
调用的就是FindModule
,还记得以前在Init
过程当中将模块都注册到的链表吗?FindModule
就是遍历这个链表找到相应的模块:
struct node_module* mp; for (mp = list; mp != nullptr; mp = mp->nm_link) { if (strcmp(mp->nm_modname, name) == 0) break; }
InitModule
就是调用以前注册模块定义的初始化函数,还以buffer
看的话,就是执行node::Buffer::Initialize
函数,打开着函数来看和平时写addon的方式同样,也会暴露一个对象出来供js
调用;LoadEnvironment
下面就是将process, GetBinding
等做为传入传给上面生成好的函数而且利用v8
来执行,来到了你们熟悉的领域,来看看loaders.js
:
const moduleLoadList = []; ObjectDefineProperty(process, 'moduleLoadList', { value: moduleLoadList, configurable: true, enumerable: true, writable: false });
定义了一个已经加载的Module的数组,也能够在node
经过process.moduleLoadList
来看看加载了多少的原生模块进来
process.binding = function binding(module) { module = String(module); let mod = bindingObj[module]; if (typeof mod !== 'object') { mod = bindingObj[module] = getBinding(module); moduleLoadList.push(`Binding ${module}`); } return mod; };
终于到了这个方法,翻看lib
中的js
文件,有着很是多的这种调用,这个函数就是对GetBinding
作了一个js
层面的封装,作的无非是查看一下这个模块是否已经加载完成了,是的话直接返回回去,不须要再次初始化了,因此利用prcoess.binding
加载了对应的c++
模块(能够执行一下process.binding('buffer')
,而后再去node_buffer.cc
中看看)继续向下看,会发现定义了一个class
就是NativeModule
,发现其有一个静态属性:
NativeModule._source = getBinding('natives');
返回到GetBinding
函数,看到的是一个if
分支就是这种状况:
exports = Object::New(env->isolate()); DefineJavaScript(env, exports);
来看看DefineJavaScript
发生了什么样的事情,这个函数发现只能在头文件(node_javascript.h
)里面找到,可是根本找不到具体的实现,这是个什么鬼???去翻一下node.gyp
文件发现这个文件是用js2c.py
这个文件生成的,去看一下这个python
文件,能够发现许多的代码模板,每个模板都是用Render
返回的,data
参数就是js
文件的内容,最终会被转换为c++
中的byte
数组,同时定义了一个将其转换为字符串的方法,那么问题来了,这些文件都是那些呢?答案仍是在node.gyp
中,就是library_files
数组,发现包含了lib
下的全部的文件和一些dep
下的js
文件,DefineJavaScript
这个文件作的就是将待执行的js
代码注册下,因此NativeModule._source
中存储的是一些待执行的js
代码,来看一下NativeModule.require
:
const cached = NativeModule.getCached(id); if (cached && (cached.loaded || cached.loading)) { return cached.exports; } moduleLoadList.push(`NativeModule ${id}`); const nativeModule = new NativeModule(id); nativeModule.cache(); nativeModule.compile(); return nativeModule.exports;
能够发现NativeModule
也有着缓存的策略,require
先把其放到_cache
中再次require
就不会像第一次那样执行这个模块,而是直接用缓存中执行好的,后面说的Module
与其同理,看一下compile
的实现:
let source = NativeModule.getSource(this.id); source = NativeModule.wrap(source); NativeModule.wrap = function(script) { return NativeModule.wrapper[0] + script + NativeModule.wrapper[1]; }; NativeModule.wrapper = [ '(function (exports, require, module, process) {', '\n});' ];
首先从_source
中取出相应的模块,而后对这个模块进行包裹成一个函数,执行函数用的是什么呢?
const script = new ContextifyScript( source, this.filename, 0, 0, codeCache[this.id], false, undefined ); this.script = script; const fn = script.runInThisContext(-1, true, false); const requireFn = this.id.startsWith('internal/deps/') ? NativeModule.requireForDeps : NativeModule.require; fn(this.exports, requireFn, this, process);
本质上就是调用了vm
编译自妇产获得函数,而后给其传入了一些参数并执行,this.exports
就是一个对象,require
区分了一下是否加载node
依赖的js
文件,this
也就是参数module
,这也说明了二者的关系,exports
就是module
的一个属性,也解释了为何exports.xx
以后再指定module.exports = yy
会将xx
忽略掉,还记得LoadEnvironment
吗?bootstrap/loaders.js
执行完以后执行了bootstrap/node.js
,能够说这个文件是node
真正的入口,好比定义了global
对象上的属性,好比console setTimeout
等,因为篇幅有限,来挑一个最经常使用的场景,来看看这个是什么一回事:
else if (process.argv[1] && process.argv[1] !== '-') { const path = NativeModule.require('path'); process.argv[1] = path.resolve(process.argv[1]); const CJSModule = NativeModule.require('internal/modules/cjs/loader'); ... CJSModule.runMain(); }
这个过程就是熟悉的node index.js
这个过程,能够看到的对于开发者本身的js
来讲,在node
中对应的class
是Module
,相信这个文件你们不少人都了解,与NativeModule
相相似,不一样的是,须要进行路径的解析和模块的查找等,来大体的看一下这个文件,先从上面调用的runMain
来看:
if (experimentalModules) { // ... } else { Module._load(process.argv[1], null, true); }
node
中开启--experimental-modules
能够加载es
模块,也就是能够不用babel
转义就可使用import/export
啦,这个不是重点,重点来看普通的commonnjs
模块,process.argv[1]
通常就是要执行的入口文件,下面看看Module._load
:
Module._load = function(request, parent, isMain) { if (parent) { debug('Module._load REQUEST %s parent: %s', request, parent.id); } // 查找文件具体位置 var filename = Module._resolveFilename(request, parent, isMain); // 存在缓存,则不须要再次执行 var cachedModule = Module._cache[filename]; if (cachedModule) { updateChildren(parent, cachedModule, true); return cachedModule.exports; } // 加载node原生模块,原生模块不须要缓存,由于NativeModule中也存在缓存 if (NativeModule.nonInternalExists(filename)) { debug('load native module %s', request); return NativeModule.require(filename); } // 加载并执行一个模块 var module = new Module(filename, parent); if (isMain) { process.mainModule = module; module.id = '.'; } Module._cache[filename] = module; // 调用load方法进行加载 tryModuleLoad(module, filename); return module.exports; };
这里看每个Module
有一个parent
的属性,假如a.js
中引入了b.js
,那么Module b
的parent
就是Module a
,利用resolveFilename
能够获得文件具体的位置,这个过程然后调用load
函数来加载文件,能够看到的是区分了几种类型,分别是.js .json .node
,对应的.js
是读文件而后执行,.json
是直接读文件后JSON.parse
一下,.node
是调用dlopen
,Module.compile
于NativeModule.compile
相相似都是想包裹一层成为函数,而后调用了vm
编译获得这个函数,最后传入参数来执行,对于Module
来讲,包裹的代码以下:
Module.wrapper = [ '(function (exports, require, module, __filename, __dirname) { ', '\n});' ];
执行完上述过程后,前期工做就已经作得比较充分了,再次回到最后一个Start
函数来看,从代码中能够看到开始了node
的event loop
,这就是node
的初始化过程,关于event loop
须要对libuv
有必定的了解,能够说node
真正离不开的是libuv
,具体这方面的东西,能够继续关注我后面的文章
总结一下这个过程,以首次加载没有任何缓存的状况开看:require('fs')
,先是调用了Module.require
,然后发现为原生模块,因而调用NativeModule.require
,从NativeModule._source
将lib/fs
的内容拿出来包裹一下而后执行,这个文件第一行就能够看到process.binding
,这个本质上是加载原生的c++
模块,这个模块在初始化的时候将其注册到了一个链表中,加载的过程就是将其拿出来而后执行
以上内容若是有错误的地方,还请大佬指出,万分感激,另一件重要的事情就是:我所在团队也在招人,若是有兴趣能够将简历发至zhoupeng.1996@bytedance.com
原文地址:https://segmentfault.com/a/1190000016318567