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

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

閱讀本篇文章以前請先閱讀前置文章:html

讀完本篇文章你會掌握:java

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

一、Nodejs依賴些啥?

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

上圖即是Nodejs依賴的包,在官網咱們能夠找到裏面一些依賴包的介紹:Dependenciespython

  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: 這個就不贅述了

其餘幾個沒在官網提到的這裏也說一下:webpack

  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代碼編譯爲底層機器碼,這裏就再也不贅述

二、有了uv和v8,那nodejs本身作些啥?

由於是要面向Javascript開發人員,因此咱們不可能直接上來就寫C++/C代碼,那麼確定須要一個東西去封裝這些C++/C代碼,並提供一套優雅的接口給開發者,因而Nodejs就是幹這事的。一言以蔽之:git

Nodejs封裝了全部與底層交流的信息,給開發者提供一致的接口定義。在不斷升級v8和libuv的同時,依然可以作到接口的一致性,這個就是nodejs想要實現的目標。
複製代碼

那麼問題來了,nodejs究竟是怎麼將libuv和v8封裝起來並提供接口的?搞懂這一切以前,咱們先看看Nodejs的目錄結構,這個目錄結構在後面的講解中有用到:github

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

  1. lib: 包含了全部nodejs函數和模塊的javascript實現,這些實現都是能夠直接在你js項目中引用進去的面試

  2. src: 包含了全部函數的C++版本實現,這裏的代碼纔會真正引用Libuv和V8。

而後咱們隨便查看一個lib目錄下的文件能夠看到,除了正常的js語法以外,出現了一個在平時應用程序沒有見到的方法:internalBinding。這個是啥?有啥做用?

咱們的探索之旅即是從這個方法開始,一步步深刻到nodejs內部,一步步帶你們揭開nodejs的神祕面紗。首先咱們要從nodejs的編譯過程提及。

再講編譯過程以前,咱們還得普及一下Nodejs源碼內部的模塊分類和C++加載綁定器兩個概念。

2.一、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.二、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

三、nodejs的編譯過程

根據官網的推薦,源碼編譯簡單粗暴:

$ ./configure
$ make -j4
複製代碼

咱們能夠從nodejs編譯配置文件中提取出一些重要信息。

衆所周知,Nodejs使用了GYP的編譯方式,其GYP編譯文件是:node.gyp,咱們從該文件的兩處地方獲取到兩個重要的信息。

3.一、node.gyp

3.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.二、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究竟是何方神聖。

四、nodejs的啓動過程

上一小節咱們知道nodejs應用程序的入口文件是node_main.cc,因而咱們從這個文件開始追蹤代碼,獲得如下一個流程圖:

其中標註紅色的是須要關注的重點,裏面有些知識和以前的那些文章能夠聯繫起來,若是你閱讀過耗時兩個月,網上最全的原創nodejs深刻系列文章(長達十來萬字的文章,歡迎收藏)中列舉的一些基礎文章,看到這裏,相信有種恍然大悟的感受,感受知識點一會兒均可以聯繫起來了,這就是系統學習的魅力~

回到上圖,全部的線索都聚焦到了這個函數中:NativeModuleLoader::LookupAndCompile。在調用這個函數以前,還有一個重點就是:此時NativeModuleLoader是實例化的,因此其構造函數是被執行掉的,而其構造函數執行的只有一個函數:LoadJavaScriptSource(),該函數就是上一小節咱們看到node_javascript.cc文件中的函數,因而咱們有如下結論:

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

那麼NativeModuleLoader::LookupAndCompile都作了些什麼呢?

4.一、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.二、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.三、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.四、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.五、終極大圖

上圖即是一個完整的nodejs和libuv以及v8一塊兒合做的流程圖,其中有一個點解釋了剛纔的問題:何時把全部內建模塊都加載到modlist_internal的?答案就是nodejs啓動的時候調用binding::RegisterBuiltinModules()

至此,按理說整篇文章是能夠結束了的,但爲了鞏固咱們以前的學(zhuang)習(bi),咱們仍是決定以一個例子來看看以前在如何正確地使用v8嵌入到咱們的C++應用中文章中講的那麼多理論,是否是在Nodejs源碼中都是對的?

五、舉個🌰(彩蛋~)

假設有這麼一個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.一、require('fs')

當你require的時候,實際上nodejs不直接執行您在js文件中編寫的任何代碼(除了上面提到的internal/bootstrap/loader.jsinternal/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.二、fs.open

open的js文件就不關注了,最終是調用了:

binding.open(pathModule.toNamespacedPath(path),
               flagsNumber,
               mode,
               req);
複製代碼

接着咱們跳到node_fs.cc中,一步步校驗以前的理論。

5.2.一、Initialize

還記得上圖中那個終極彩蛋裏,當調用internalBinding的時候,是會初始化對應的內建模塊,也就是調用其初始化函數,這裏即是Initialize函數。

這個函數一開始即是給target設置method,好比:

env->SetMethod(target, "close", Close);
env->SetMethod(target, "open", Open);
複製代碼

那麼該方法最後都是調用了that->Set(context, name_string, function).Check();,這個是否是和咱們在如何正確地使用v8嵌入到咱們的C++應用中中的第二小節二、調用 C++ 函數講的如出一轍?

接着開始暴露FSReqCallback這個類,這個在fs.js文件中有調用到:

const req = new FSReqCallback();
req.oncomplete = callback;
複製代碼

那麼這個時候咱們就要用到如何正確地使用v8嵌入到咱們的C++應用中中的第三小節三、使用 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.二、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 模塊加載與運行原理
相關文章
相關標籤/搜索