日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

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

發布時間:2023/12/18 编程问答 30 豆豆
生活随笔 收集整理的這篇文章主要介紹了 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

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

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

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

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

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

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

  • lib: 包含了所有nodejs函數和模塊的javascript實現,這些實現都是可以直接在你js項目中引用進去的
  • 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/**/*.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提供的示例一樣,只是為了封裝,將很多東西隱藏起來,閱讀起來比較費勁。

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

    感恩~

    參考

  • Internals of Node- Advance node
  • 結合源碼分析 Node.js 模塊加載與運行原理
  • 總結

    以上是生活随笔為你收集整理的arm nodejs_nodejs是如何和libuv以及v8一起合作的?(文末有彩蛋哦)的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。