arm nodejs_nodejs是如何和libuv以及v8一起合作的?(文末有彩蛋哦)
該文章當前使用的nodejs版本是v13.1.0(網上那些分析nodejs源碼的文章不寫清基于的版本都是耍流氓),非常干貨的一篇文章,請耐心閱讀,否則建議收藏
閱讀本篇文章之前請先閱讀前置文章:
- [譯文]V8學習的高級進階
- nodejs深入學習系列之v8基礎篇
- 如何正確地使用v8嵌入到我們的C++應用中
- nodejs深入學習系列之libuv基礎篇(一)
- nodejs深入學習系列之libuv基礎篇(二)
讀完本篇文章你會掌握:
- nodejs啟動過程
- nodejs模塊的分類以及各自的加載過程和原理
- nodejs中的js代碼調用C++函數的原理
- 額外的面試題~~
1、Nodejs依賴些啥?
首先,nodejs提供那么多模塊,以及能在各個平臺上跑的飛起,不是因為js很牛逼,而是因為底層依賴了一些你不知道的技術。最大的兩個依賴便是v8和libuv。為什么這么說呢?因為一個幫助你將js代碼轉變成可以在各個平臺和機器上運行的機器碼,另外一個幫助你調用平臺和機器上各種系統特性,包括操作文件、監聽socket等等。先撇開這兩個最大的依賴,我們看一下nodejs源碼中的deps目錄都有些啥?
上圖便是Nodejs依賴的包,在官網我們可以找到里面一些依賴包的介紹:Dependencies
其他幾個沒在官網提到的這里也說一下:
2、有了uv和v8,那nodejs自己做些啥?
因為是要面向Javascript開發人員,所以我們不可能直接上來就寫C++/C代碼,那么肯定需要一個東西去封裝這些C++/C代碼,并提供一套優雅的接口給開發者,于是Nodejs就是干這事的。一言以蔽之:
Nodejs封裝了所有與底層交流的信息,給開發者提供一致的接口定義。在不斷升級v8和libuv的同時,依然能夠做到接口的一致性,這個就是nodejs想要實現的目標。那么問題來了,nodejs到底是怎么將libuv和v8封裝起來并提供接口的?搞懂這一切之前,我們先看看Nodejs的目錄結構,這個目錄結構在后面的講解中有用到:
nodejs源碼有兩個重要的目錄:
然后我們隨便查看一個lib目錄下的文件可以看到,除了正常的js語法之外,出現了一個在平時應用程序沒有見到的方法:internalBinding。這個是啥?有啥作用?
我們的探索之旅便是從這個方法開始,一步步深入到nodejs內部,一步步帶大家揭開nodejs的神秘面紗。首先我們要從nodejs的編譯過程說起。
再講編譯過程之前,我們還得普及一下Nodejs源碼內部的模塊分類和C++加載綁定器兩個概念。
2.1、Nodejs模塊分類
nodejs模塊可以分為下面三類:
- 核心模塊(native模塊):包含在 Node.js 源碼中,被編譯進 Node.js 可執行二進制文件 JavaScript 模塊,其實也就是lib和deps目錄下的js文件,比如常用的http,fs等等
- 內建模塊(built-in模塊):一般我們不直接調用,而是在 native 模塊中調用,然后我們再require
- 第三方模塊:非 Node.js 源碼自帶的模塊都可以統稱第三方模塊,比如 express,webpack 等等。
- JavaScript 模塊,這是最常見的,我們開發的時候一般都寫的是 JavaScript 模塊
- JSON 模塊,這個很簡單,就是一個 JSON 文件
- C/C++ 擴展模塊,使用 C/C++ 編寫,編譯之后后綴名為 .node
比如lib目錄下的fs.js就是native模塊,而fs.js調用的src目錄下的node_fs.cc就是內建模塊。知道了模塊的分類,那么好奇這些模塊是怎么加載進來的呢?(本文非講解模塊加載的,所以第三方模塊不在討論范圍內)
2.2、C++加載綁定器分類
后面會有文字涉及到這幾個概念:
- process.binding(): 以前C++綁定加載器,因為是掛載在全局進程對象上的一個對象,所以可以從用戶空間上訪問到。這些C++綁定使用這個宏:NODE_BUILTIN_MODULE_CONTEXT_AWARE()來創建,并且它們的nm_flags都設置為NM_F_BUILTIN
- process._linkedBinding(): 用于開發者想在自己應用添加額外的C++綁定,使用NODE_MODULE_CONTEXT_AWARE_CPP()宏來創建,其flag設置為NM_F_LINKED
- internalBinding:私有的內部C++綁定加載器,用戶空間上訪問不到,因為只有在NativeModule.require()下可用。使用NODE_MODULE_CONTEXT_AWARE_INTERNAL()宏來創建,其flag設置為NM_F_INTERNAL
3、nodejs的編譯過程
根據官網的推薦,源碼編譯簡單粗暴:
$ ./configure $ make -j4我們可以從nodejs編譯配置文件中提取出一些重要信息。
眾所周知,Nodejs使用了GYP的編譯方式,其GYP編譯文件是:node.gyp,我們從該文件的兩處地方獲取到兩個重要的信息。
3.1、node.gyp
3.1.1、可執行應用程序的入口文件
從該文件的target字段可以看到,編譯之后會生成多個target,但是最重要的是第一個target,其配置:
{// 定義的'node_core_target_name%'就是'node','target_name': '<(node_core_target_name)','type': 'executable', // 這里的類型是可執行文件'defines': ['NODE_WANT_INTERNALS=1',],'includes': ['node.gypi'],'include_dirs': ['src','deps/v8/include'],'sources': ['src/node_main.cc'],... ... }由此可知,整個node應用程序的入口文件其實就是node_main.cc。
3.1.2、Nodejs源碼中所有的js文件編譯方式
編譯文件的第二個target是libnode,它是將其余剩余的C++文件編譯成庫文件,但是有一個特殊的地方就是該target在編譯之前有個action:
{// 這里定義的'node_lib_target_name'就是libnode'target_name': '<(node_lib_target_name)','type': '<(node_intermediate_lib_type)','includes': ['node.gypi',],'include_dirs': ['src','<(SHARED_INTERMEDIATE_DIR)' # for node_natives.h],... ...'actions': [{'action_name': 'node_js2c','process_outputs_as_sources': 1,'inputs': [# Put the code first so it's a dependency and can be used for invocation.'tools/js2c.py','<@(library_files)','config.gypi','tools/js2c_macros/check_macros.py'],'outputs': ['<(SHARED_INTERMEDIATE_DIR)/node_javascript.cc',],'conditions': [[ 'node_use_dtrace=="false" and node_use_etw=="false"', {'inputs': [ 'tools/js2c_macros/notrace_macros.py' ]}],[ 'node_debug_lib=="false"', {'inputs': [ 'tools/js2c_macros/nodcheck_macros.py' ]}],[ 'node_debug_lib=="true"', {'inputs': [ 'tools/js2c_macros/dcheck_macros.py' ]}]],'action': ['python', '<@(_inputs)','--target', '<@(_outputs)',],},],從這個配置信息來看是說有個js2c.py的python文件會將lib/**/*.js和deps/**/*.js的所有js文件按照其ASCII碼轉化為一個個數組放到node_javascript.cc文件中。
生成的node_javascript.cc文件內容大致如下:
namespace node {namespace native_module {...static const uint8_t fs_raw[] = {...}...void NativeModuleLoader::LoadJavaScriptSource() {...source_.emplace("fs", UnionBytes{fs_raw, 50659});...}UnionBytes NativeModuleLoader::GetConfig() {return UnionBytes(config_raw, 3017); // config.gypi} }這種做法直接就將js文件全都緩存到內存,避免了多余的I/O操作,提高了效率。
因此從上述配置信息我們可以總結出這樣一張編譯過程:
好了,清楚了編譯流程之后,我們再從nodejs的啟動過程來分析internalBinding到底是何方神圣。
4、nodejs的啟動過程
上一小節我們知道nodejs應用程序的入口文件是node_main.cc,于是我們從這個文件開始追蹤代碼,得到以下一個流程圖:
其中標注紅色的是需要關注的重點,里面有些知識和之前的那些文章可以聯系起來,如果你閱讀過耗時兩個月,網上最全的原創nodejs深入系列文章(長達十來萬字的文章,歡迎收藏)中列舉的一些基礎文章,看到這里,相信有種恍然大悟的感覺,感覺知識點一下子都可以聯系起來了,這就是系統學習的魅力~
回到上圖,所有的線索都聚焦到了這個函數中:NativeModuleLoader::LookupAndCompile。在調用這個函數之前,還有一個重點就是:此時NativeModuleLoader是實例化的,所以其構造函數是被執行掉的,而其構造函數執行的只有一個函數:LoadJavaScriptSource(),該函數就是上一小節我們看到http://node_javascript.cc文件中的函數,于是我們有以下結論:
- internal/bootstrap/loader.js是我們執行的第一個js文件
那么NativeModuleLoader::LookupAndCompile都做了些什么呢?
4.1、NativeModuleLoader::LookupAndCompile
它利用我們傳入的文件id(這次傳遞的是internal/bootstrap/loader.js)在_source變量中查找,找到之后將整個文件內容包裹起來成為一個新的函數,并追加進一些函數的定義(這次傳遞的是getLinkedBinding和getInternalBinding)以便在js文件中可以調用這些C++函數,然后執行該新函數。這個參數的傳遞是在上圖中的Environment::BootstrapInternalLoaders函數中:
MaybeLocal<Value> Environment::BootstrapInternalLoaders() {EscapableHandleScope scope(isolate_);// Create binding loadersstd::vector<Local<String>> loaders_params = {process_string(),FIXED_ONE_BYTE_STRING(isolate_, "getLinkedBinding"),FIXED_ONE_BYTE_STRING(isolate_, "getInternalBinding"),primordials_string()};// 這里的GetInternalBinding便是我們調用`getInternalBinding`執行的函數。如果你不知道為什么js可以調用C++函數的話,請參考這篇文章:《如何正確地使用v8嵌入到我們的C++應用中》std::vector<Local<Value>> loaders_args = {process_object(),NewFunctionTemplate(binding::GetLinkedBinding)->GetFunction(context()).ToLocalChecked(),NewFunctionTemplate(binding::GetInternalBinding)->GetFunction(context()).ToLocalChecked(),primordials()};... }這個時候加載進loader.js之后,我們來看看該文件做了些啥?
4.2、internal/bootstrap/loader.js
這個文件非常特殊,是唯一一個沒有出現require關鍵詞的js文件,它唯一使用的外部函數就是剛才提到的getLinkedBinding和getInternalBinding,這一點可以通過文件源碼進行核實
該文件就是構建出NativeModule這么一個對象,里面有一些原型方法,最后返回這么一個數據結構:
const loaderExports = {internalBinding,NativeModule,require: nativeModuleRequire };在里面我們找到了internalBinding這個方法的原始實現:
let internalBinding; {const bindingObj = Object.create(null);// eslint-disable-next-line no-global-assigninternalBinding = function internalBinding(module) {let mod = bindingObj[module];if (typeof mod !== 'object') {// 這里調用我們的C++方法mod = bindingObj[module] = getInternalBinding(module);moduleLoadList.push(`Internal Binding ${module}`);}return mod;}; }接著我們順藤摸瓜,看上圖的流程圖的一個紅色線,loader.js執行完后的返回值繼續傳遞到了internal/bootstrap/node.js這個文件使用。
代碼如下:
MaybeLocal<Value> Environment::BootstrapInternalLoaders() {... ...// 這里的loader_exports便是執行完loader.js之后返回的值Local<Value> loader_exports;if (!ExecuteBootstrapper(this, "internal/bootstrap/loaders", &loaders_params, &loaders_args).ToLocal(&loader_exports)) {return MaybeLocal<Value>();}CHECK(loader_exports->IsObject());Local<Object> loader_exports_obj = loader_exports.As<Object>();// 此時internal_binding_loader的值便是loader_exports.internalBinding,下面的同理Local<Value> internal_binding_loader =loader_exports_obj->Get(context(), internal_binding_string()).ToLocalChecked();CHECK(internal_binding_loader->IsFunction());set_internal_binding_loader(internal_binding_loader.As<Function>());// 注意這里的require是native_module的require,有別于第三方包的reuqireLocal<Value> require =loader_exports_obj->Get(context(), require_string()).ToLocalChecked();CHECK(require->IsFunction());set_native_module_require(require.As<Function>());... }MaybeLocal<Value> Environment::BootstrapNode() {... ...std::vector<Local<Value>> node_args = {process_object(),native_module_require(),internal_binding_loader(), // 這個就是剛才的那個internalBindingBoolean::New(isolate_, is_main_thread()),Boolean::New(isolate_, owns_process_state()),primordials()};... ... }該文件同理,也會注入isMainThread、ownsProcessState以及process、require、primordials和internalBinding六個C++函數供js文件調用。
由此又得到的一個結論就是:
- js調用internalBinding => C++的internal_binding_loader函數 => js的internalBinding函數 => C++的GetInternalBinding函數
但是到這里,我們的問題還有一些沒有解開,還需要繼續深入。
4.3、GetInternalBinding
在internal/bootstrap/node.js中,大部分都是給process和global對象賦值初始化,按照上面給的結論,當我們調用internalBinding的時候,實際會執行的是GetInternalBinding這個C++函數。所以我們來看看這個函數的實現。
js調用C++函數的規則在如何正確地使用v8嵌入到我們的C++應用中文章中已經提及過,所以我們就不再贅述這個是怎么調用的,我們關注重點:
void GetInternalBinding(const FunctionCallbackInfo<Value>& args) {... ...// 查找模塊,在哪里查找?node_module* mod = FindModule(modlist_internal, *module_v, NM_F_INTERNAL);if (mod != nullptr) {exports = InitModule(env, mod, module);// 什么是constants模塊?} 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")) {exports = native_module::NativeModuleEnv::GetSourceObject(env->context());// Legacy feature: process.binding('natives').config contains stringified// config.gypiCHECK(exports->Set(env->context(),env->config_string(),native_module::NativeModuleEnv::GetConfigString(env->isolate())).FromJust());} else {return ThrowIfNoSuchModule(env, *module_v);}// 這里導出了exports這個變量~args.GetReturnValue().Set(exports); }這個函數又留給了我們一些疑問:
- FindModule中的modlist_internal從哪里來?
- native模塊名稱為什么還有名為constants和natives的呢?
為了揭開這些問題,我們繼續往下深入。
4.4、NODE_MODULE_CONTEXT_AWARE_INTERNAL
這個時候NODE_MODULE_CONTEXT_AWARE_INTERNAL隆重登場,細心的童鞋肯定發現諸如src/node_fs.cc這種文件都是以這個宏定義結束的。
在node_binding.h文件中可以找到其定義:
#define NODE_MODULE_CONTEXT_AWARE_INTERNAL(modname, regfunc) NODE_MODULE_CONTEXT_AWARE_CPP(modname, regfunc, nullptr, NM_F_INTERNAL可以看到實際調用的是宏定義NODE_MODULE_CONTEXT_AWARE_CPP,只是將flag設置為NM_F_INTERNAL。
而NODE_MODULE_CONTEXT_AWARE_CPP宏定義則實際上調用了方法:node_module_register。
node_module_register這個方法便是往全局的靜態變量modlist_internal和modlist_linked兩個鏈表掛載模塊:
if (mp->nm_flags & NM_F_INTERNAL) {mp->nm_link = modlist_internal;modlist_internal = mp; } else if (!node_is_initialized) {// "Linked" modules are included as part of the node project.// Like builtins they are registered *before* node::Init runs.mp->nm_flags = NM_F_LINKED;mp->nm_link = modlist_linked;modlist_linked = mp; } else {thread_local_modpending = mp; }于是modlist_internal就是一個鏈表,里面鏈接著所有內建模塊,所以上面的GetInternalBinding方法是這樣的一個執行邏輯:
上圖中的那些internalBinding的調用,提供了各種各樣的模塊名,其中就有我們剛才問到constants和natives這兩個特殊的模塊名。
這樣,上面的兩個問題就迎刃而解了。
但是,問題真的全解決完了嗎?如果僅僅是單純地編譯文件的話,這個NODE_MODULE_CONTEXT_AWARE_INTERNAL是不會被調用的,那么哪里來的調用node_module_register?
,就欣賞你們這種執著的精神。最后的這個問題,連同整篇文章的一個總結性的流程一起釋放給大家,算是個大彩蛋~
4.5、終極大圖
上圖便是一個完整的nodejs和libuv以及v8一起合作的流程圖,其中有一個點解釋了剛才的問題:什么時候把所有內建模塊都加載到modlist_internal的?答案就是nodejs啟動的時候調用binding::RegisterBuiltinModules()。
至此,按理說整篇文章是可以結束了的,但為了鞏固我們之前的學(zhuang)習(bi),我們還是決定以一個例子來看看之前在如何正確地使用v8嵌入到我們的C++應用中文章中講的那么多理論,是不是在Nodejs源碼中都是對的?
5、舉個 (彩蛋~)
假設有這么一個index.js:
const fs = require('fs')module.exports = () => {fs.open('test.js', () => {// balabala}) }當你在命令行敲入node index.js回車之后,會有哪些處理流程?
這道題真的太TMD像“當你在瀏覽器輸入某個url回車之后,會經過哪些流程”了。還好,這不是面試(很有可能會成為面試題哦~)
大家一看也就是兩三行代碼嗎?但是就這么簡單的兩三行代碼,可以出很多面試題哦~比如說:
- 為什么這里require可以不用聲明而直接引用?
- 這里的module.export換成exports可以嗎?
- fs.open是不是有同步的方法?
- fs.open可以傳值指定打開模式,請問這個“0o666"表示什么?
- fs.open底層調用了uv_fs_open,請問是在libuv主線程中執行還是另起一個線程執行?
還有好多題目可以問,這里就不一一列舉了,想要更多問題歡迎留言( )
今天我們重點不在這些面試題,而是驗證C++代碼是不是如之前文章寫的那樣。我們一行一行解析過去(不會太深入)。
5.1、require('fs')
當你require的時候,實際上nodejs不直接執行您在js文件中編寫的任何代碼(除了上面提到的internal/bootstrap/loader.js和internal/bootstrap/node.js)。它將您的代碼放入一個包裝器函數中,然后執行該包裝函數。這就是將在任何模塊中定義的頂級變量保留在該模塊范圍內的原因。
比如:
~ $ node > require('module').wrapper [ '(function (exports, require, module, __filename, __dirname) { ','n});' ] >可以看到該包裝器函數有5個參數:exports, require, module, __filename和__dirname. 所以你在js文件中寫的那些require、module.exports其實都是這些形參,而不是真的全局變量
更多細節就不展開了,要不真的就說不完了~
5.2、fs.open
open的js文件就不關注了,最終是調用了:
binding.open(pathModule.toNamespacedPath(path),flagsNumber,mode,req);接著我們跳到node_fs.cc中,一步步校驗之前的理論。
5.2.1、Initialize
還記得上圖中那個終極彩蛋里,當調用internalBinding的時候,是會初始化對應的內建模塊,也就是調用其初始化函數,這里便是Initialize函數。
這個函數一開始便是給target設置method,比如:
env->SetMethod(target, "close", Close); env->SetMethod(target, "open", Open);那么該方法最后都是調用了that->Set(context, name_string, function).Check();,這個是不是和我們在如何正確地使用v8嵌入到我們的C++應用中中的第二小節2、調用 C++ 函數講的一模一樣?
接著開始暴露FSReqCallback這個類,這個在fs.js文件中有調用到:
const req = new FSReqCallback(); req.oncomplete = callback;那么這個時候我們就要用到如何正確地使用v8嵌入到我們的C++應用中中的第三小節3、使用 C++ 類的知識了:
Local<FunctionTemplate> fst = env->NewFunctionTemplate(NewFSReqCallback); fst->InstanceTemplate()->SetInternalFieldCount(1); fst->Inherit(AsyncWrap::GetConstructorTemplate(env)); Local<String> wrapString =FIXED_ONE_BYTE_STRING(isolate, "FSReqCallback"); fst->SetClassName(wrapString); target->Set(context, wrapString,fst->GetFunction(env->context()).ToLocalChecked()).Check();完美契合了之前講的那些理論知識。
接著我們看看是如何使用libuv的
5.2.2、Open
異步調用統一封裝了一個叫做AsyncCall的函數,它又調用了AsyncDestCall:
AsyncCall(env, req_wrap_async, args, "open", UTF8, AfterInteger,uv_fs_open, *path, flags, mode);之后的調用依舊按照我們之前在fs.c提供的示例一樣,只是為了封裝,將很多東西隱藏起來,閱讀起來比較費勁。
到這里, 你完成了本篇文章的閱讀,也感謝你的耐心讓你又掌握了一塊知識,還沒讀懂的話,點個收藏,以后遇到的時候可以拿出來參考參考~
感恩~
參考
總結
以上是生活随笔為你收集整理的arm nodejs_nodejs是如何和libuv以及v8一起合作的?(文末有彩蛋哦)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 记一下怎样关闭windows defen
- 下一篇: 一款自制的视频录制软件