相信不少的人,天天在終端不止一遍的執行着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