nodejs是如何和libuv以及v8一起合作的?(文末有彩蛋哦)


nodejs是如何和libuv以及v8一起合作的?(文末有彩蛋哦)

該文章當前使用的nodejs版本是v13.1.0(網上那些分析nodejs源碼的文章不寫清基於的版本都是耍流氓),非常干貨的一篇文章,請耐心閱讀,否則建議收藏

閱讀本篇文章之前請先閱讀前置文章:

讀完本篇文章你會掌握:

  • nodejs啟動過程
  • nodejs模塊的分類以及各自的加載過程和原理
  • nodejs中的js代碼調用C++函數的原理
  • 額外的面試題~~

1、Nodejs依賴些啥?

首先,nodejs提供那么多模塊,以及能在各個平台上跑的飛起,不是因為js很牛逼,而是因為底層依賴了一些你不知道的技術。最大的兩個依賴便是v8和libuv。為什么這么說呢?因為一個幫助你將js代碼轉變成可以在各個平台和機器上運行的機器碼,另外一個幫助你調用平台和機器上各種系統特性,包括操作文件、監聽socket等等。先撇開這兩個最大的依賴,我們看一下nodejs源碼中的deps目錄都有些啥?

 

 

上圖便是Nodejs依賴的包,在官網我們可以找到里面一些依賴包的介紹:Dependencies

  1. http_parser: 顧名思義,是一個HTTP解析器,是一款由C語言寫的輕量級解析器。因為該解析器設計成不進行任何系統調用或分配,因此每個請求占用的內存非常小。
  2. c-ares: 對於一些異步DNS解析,nodejs使用了該C庫。在js層面上暴露出去的便是DNS模塊中的resolve()族函數。
  3. OpenSSL: OpenSSL在tls和密碼模塊中都得到了廣泛的應用。它提供了經過嚴密測試的許多加密功能的實現,現代web依賴這些功能來實現安全性。
  4. zlib: 為了實現快速得壓縮和解壓縮,Node.js依賴於工業標准的zlib庫,也因其在gzip和libpng中的使用而聞名。Nodejs用zlib來創建同步的、或異步或流式的壓縮和解壓縮接口。
  5. npm: 這個就不贅述了

其他幾個沒在官網提到的這里也說一下:

  1. acorn: 一款體積小但效率高的javascript解析器
  2. acorn-plugins: acorn使用的一些插件,從名稱上來看,該版本的Nodejs支持bigInt特性、支持private類和方法特性等等
  3. brotli: 提供C語言版本的Brotli壓縮算法實現。
  4. histogram: C語言版本實現高動態范圍的柱狀圖,看了遍介紹,不知道為啥nodejs需要引用這個?
  5. icu: ICU(International Components for Unicode)是一套成熟並廣泛使用的C/C++和Java庫集合,為軟件應用提供Unicode和Globalization的支持
  6. llhttp: 更加高性能可維護性更好的http解析器。
  7. nghttp2: HTTP/2協議的C語言實現,頭部壓縮算法使用了HPACK
  8. node-inspect: 該庫嘗試在新的V8版本下提供node debug命令。
  9. uv: Nodejs的一大精髓之一,提供Nodejs訪問操作系統各種特性的能力,包括文件系統、Socket等
  10. v8: 將Js代碼編譯為底層機器碼,這里就不再贅述

2、有了uv和v8,那nodejs自己做些啥?

因為是要面向Javascript開發人員,所以我們不可能直接上來就寫C++/C代碼,那么肯定需要一個東西去封裝這些C++/C代碼,並提供一套優雅的接口給開發者,於是Nodejs就是干這事的。一言以蔽之:

Nodejs封裝了所有與底層交流的信息,給開發者提供一致的接口定義。在不斷升級v8和libuv的同時,依然能夠做到接口的一致性,這個就是nodejs想要實現的目標。

那么問題來了,nodejs到底是怎么將libuv和v8封裝起來並提供接口的?搞懂這一切之前,我們先看看Nodejs的目錄結構,這個目錄結構在后面的講解中有用到:

nodejs源碼有兩個重要的目錄:

  1. lib: 包含了所有nodejs函數和模塊的javascript實現,這些實現都是可以直接在你js項目中引用進去的
  2. src: 包含了所有函數的C++版本實現,這里的代碼才會真正引用Libuv和V8。

然后我們隨便查看一個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/**/*.jsdeps/**/*.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(),該函數就是上一小節我們看到文件中的函數,於是我們有以下結論:

  • internal/bootstrap/loader.js是我們執行的第一個js文件

那么NativeModuleLoader::LookupAndCompile都做了些什么呢?

4.1、NativeModuleLoader::LookupAndCompile

它利用我們傳入的文件id(這次傳遞的是internal/bootstrap/loader.js)在_source變量中查找,找到之后將整個文件內容包裹起來成為一個新的函數,並追加進一些函數的定義(這次傳遞的是getLinkedBindinggetInternalBinding)以便在js文件中可以調用這些C++函數,然后執行該新函數。這個參數的傳遞是在上圖中的Environment::BootstrapInternalLoaders函數中:

MaybeLocal<Value> Environment::BootstrapInternalLoaders() { EscapableHandleScope scope(isolate_); // Create binding loaders  std::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-assign  internalBinding = 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,有別於第三方包的reuqire  Local<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(), // 這個就是剛才的那個internalBinding  Boolean::New(isolate_, is_main_thread()), Boolean::New(isolate_, owns_process_state()), primordials()}; ... ... } 

該文件同理,也會注入isMainThreadownsProcessState以及processrequireprimordialsinternalBinding六個C++函數供js文件調用。

由此又得到的一個結論就是:

  • js調用internalBinding => C++的internal_binding_loader函數 => js的internalBinding函數 => C++的GetInternalBinding函數

但是到這里,我們的問題還有一些沒有解開,還需要繼續深入。

4.3、GetInternalBinding

internal/bootstrap/node.js中,大部分都是給processglobal對象賦值初始化,按照上面給的結論,當我們調用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.gypi  CHECK(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模塊名稱為什么還有名為constantsnatives的呢?

為了揭開這些問題,我們繼續往下深入。

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_internalmodlist_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的調用,提供了各種各樣的模塊名,其中就有我們剛才問到constantsnatives這兩個特殊的模塊名。

這樣,上面的兩個問題就迎刃而解了。

但是,問題真的全解決完了嗎?如果僅僅是單純地編譯文件的話,這個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.jsinternal/bootstrap/node.js)。它將您的代碼放入一個包裝器函數中,然后執行該包裝函數。這就是將在任何模塊中定義的頂級變量保留在該模塊范圍內的原因。

比如:

~ $ node
> require('module').wrapper
[ '(function (exports, require, module, __filename, __dirname) { ',
  '\n});' ]
>

可以看到該包裝器函數有5個參數:exportsrequiremodule__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提供的示例一樣,只是為了封裝,將很多東西隱藏起來,閱讀起來比較費勁。

到這里, 你完成了本篇文章的閱讀,也感謝你的耐心讓你又掌握了一塊知識,還沒讀懂的話,點個收藏,以后遇到的時候可以拿出來參考參考~

感恩~

參考

  1. Internals of Node- Advance node
  2. 結合源碼分析 Node.js 模塊加載與運行原理
發布於 2019-11-18


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM