深入出不來nodejs源碼-流程總覽


  花了差不多兩周時間過了下primer C++5th,完成了《C++從入門到精通》。(手動滑稽)

  這兩天看了下node源碼的一些入口方法,其實還是比較懵逼的,語法倒不是難點,主要是大量的宏造成直接閱讀上的不方便。

  有些宏感覺真是一點鳥用都沒有,比如說:

#define LIKELY(expr) expr
#define UNLIKELY(expr) expr
#define PRETTY_FUNCTION_NAME ""

  這玩意翻譯成JS大概就是:

const LIKELY = (expr) => expr;

  JS中有些情況確實是需要這么一個函數,比如vue組件中的data,主要是為了防止復雜類型的引用問題。但是在C++我就不明白了,深拷貝簡直不要太簡單,可能這里為了強行語義化吧。

  不過好在源碼的規范超級棒,比如說:

1、內置常量宏都是全大寫多單詞下划線連接:FILE_TYPE_UNKNOWN代表無法識別的文件類型,對應16進制數字0x0000

2、函數名基本上能猜到用處:IsWindow7OrGreater函數判斷操作系統是不是win7版本以上

3、注釋隨處可見

4、代碼結構划分非常清晰,部分看不懂(或者不想看)的可以直接跳過,不影響后續內容理解

  另外IDE也好使,直接幫我把LINUX系統的兼容代碼直接置灰了,跳轉大部分情況也很好用。

  本來打算從簡單一點的API來講,比如說require方法是如何運作的,但是這東西過於簡單,百度一搜一大把,已經是被人寫爛的內容,所以不打算開這個坑(更重要的是那玩意是JS,如何對得起我學了兩周C++)。

  因此我決定開一個目錄坑,總覽一下node從啟動到運行,哪些部分我能看懂,理一理。

 

正文開始

  聲明:本文目前基於node-v10.1.0,未來如有更改會說明。代碼來源於官網下載的源碼壓縮包,詳情見上一篇文章。

  首先上個圖瞧一眼大概的流程:

  是不是很少很簡單?對啊,因為我只能看懂這么多……

  開始吹。

  首先每個C++項目都有一個啟動項目,每個項目里有一個主函數,當初我以為只能叫main,后來發現wmain也是OK的。

  而在node中,wmain是windows系統的主函數,main是LINUX系統的主函數。區分系統的關鍵在於一個特定的宏,windows是_WIN32,這個宏會被自動定義,相關源碼如下:

#ifdef _WIN32
int wmain(int argc, wchar_t *wargv[]) {
    // ...
    return node::Start(argc, argv);
}
#else
// UNIX
#ifdef __linux__
int main(int argc, char *argv[]) {
    // ...
}

  這里就不扯什么頭文件了,主函數會調用node模塊的Start方法,node模塊文件名是node.cc,相當於項目的主文件,從這里開始,也從這里結束。

  從上面可以看到,這個函數有三大步(能看懂的),非常簡單粗暴,一個一個講。

Init

  這個函數相關源碼如下:

void Init(int* argc,
    const char** argv,
    int* exec_argc,
    const char*** exec_argv) {
    // Initialize prog_start_time to get relative uptime.
    prog_start_time = static_cast<double>(uv_now(uv_default_loop()));
    // 加載內置模塊
    RegisterBuiltinModules();
    // Make inherited handles noninheritable.
    uv_disable_stdio_inheritance();

    // 后面還有很多代碼 但是我看不懂 流下了沒技術的眼淚……
}

  需要關注的只要那個RegisterBuiltinModules方法,從名字也可以看出來,就是加載內置模塊,描述有誤,下一節修正。

  這個函數的定義也很奇妙,簡直就是宏函數,如下:

void RegisterBuiltinModules() {
#define V(modname) _register_##modname();
  NODE_BUILTIN_MODULES(V)
#undef V
}

  函數的聲明相信學JS的也能看懂是什么,主要是函數內容很奇怪,竟然是一個宏定義。

  用JS翻譯一下那個宏,意思大概就是:

// C++:#define V(modname) _register_##modname();
const V = (modname) => `_register_${modname}`();

  當然,這個鳥代碼是不可能執行的,只是為了方便理解。

  打個比方就能明白了,假如我調用了V(a),那么在執行的時候,實際上調用的是_register_a這個方法,雙警號只是一個字符串拼接。

  好,解決了宏問題,可以看下這個NODE_BUILTIN_MODULES是什么函數了。然而,這也是一個宏,內容如下:

#define NODE_BUILTIN_MODULES(V)                                               \
  NODE_BUILTIN_STANDARD_MODULES(V)                                            \
  NODE_BUILTIN_OPENSSL_MODULES(V)                                             \
  NODE_BUILTIN_ICU_MODULES(V)

  看到這閃亮亮的格式,應該可以猜到,這三個東西還是宏!!!

  好在宏不過三代,隨便點一個跳轉就會發現,實際上這些宏都是為了調用具體的模塊加載方法,比如:

#define NODE_BUILTIN_STANDARD_MODULES(V)                                      \
    V(async_wrap)                                                             \
    V(buffer)                                                                 \
// 還有很多比如fs、url等

  這樣把最初的函數簡單轉換一下,大概就是:

void RegisterBuiltinModules() {
    _register_async_wrap();
    _register_buffer();
    // 等等
}

  至於這些方法的內容是什么?地點在哪?我還沒找到。

Start

  V8引擎的加載就先忽略,觸及到我的知識盲區。最后看一下那個內聯Start函數,這個方法主要完成三件事:

1、初始化並加載全局變量global

2、加載輔助工具

3、事件輪詢

  其中1、2兩個都是在同一個函數中執行的,即LoadEnviroment,上一些關鍵的源碼證明下:

void LoadEnvironment(Environment* env) {
    // ...

    Local<String> loaders_name =
        FIXED_ONE_BYTE_STRING(env->isolate(), "internal/bootstrap/loaders.js");
    Local<Function> loaders_bootstrapper =
        GetBootstrapper(env, LoadersBootstrapperSource(env), loaders_name);
    // 還有"internal/bootstrap/node.js"

    // 生成全局global對象的引用
    Local<Object> global = env->context()->Global();

    // ...

    // 設置全局對象
    global->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "global"), global);

    // ...

    // 生成函數參數
    Local<Value> loaders_bootstrapper_args[] = {
        env->process_object(),
        get_binding_fn,
        get_linked_binding_fn,
        get_internal_binding_fn
    };

    // 加載輔助函數
    Local<Value> bootstrapped_loaders;
    if (!ExecuteBootstrapper(env, loaders_bootstrapper,
        arraysize(loaders_bootstrapper_args),
        loaders_bootstrapper_args,
        &bootstrapped_loaders)) {
        return;
    }

    // 加載Bootstrap Node.js
}

  這里通過Environment類生成了global對象,然后掛載到全局。

  輔助函數則是加載了internal/bootstrap中的兩個JS文件,加載的時候參數傳入了C++代碼生成的特殊對象,配合對應的JS源文件就很好理解:

'use strict';
// internal/bootstrap/loader.js
(function bootstrapInternalLoaders(process, getBinding, getLinkedBinding,getInternalBinding) {
    // ...模塊內容
});

  這個JS文件內容看起來只是一個函數定義,但是實際上在啟動后已經執行完了,四個參數是通過上述C++代碼注入的,依次對應上面的4個東西。

  這里有一個小地方稍微講講吧,如果在啟動時已經執行完了,那么看一下下面的代碼:

// internal/bootstrap/loader.js
const { NativeModule } = require('internal/bootstrap/loaders');

  這是node中調用require方法的入口JS文件,理論上引入的應該是上面那個函數,不可能得到NativeModule對象的。

  不過node的require並不像webpack那么簡單粗暴,readFile + parse直接得到文件輸出對象,而是區分了內部模塊與外部模塊。對於內部模塊,有一套新的邏輯,相關代碼如下:

// require => _load
// 判斷傳入的字符串是否是內部模塊名
if (NativeModule.nonInternalExists(filename)) {
    debug('load native module %s', request);
    // 調用另一套邏輯代碼
    return NativeModule.require(filename);
}

  而NativeModule就是一開始加載過的輔助工具JS,涉及到的代碼如下:

const loaderExports = { internalBinding, NativeModule };
const loaderId = 'internal/bootstrap/loaders';
NativeModule.require = function(id) {
    // Do not expose this to user land even with --expose-internals
    // 對此require進行特殊處理
    if (id === loaderId) {
        return loaderExports;
    }
    // ...
}

  可以看到在require的地方做了特殊處理,會直接返回指定的對象,至於這兩個對象是什么,后面再慢慢講吧。

  值得注意的是那個注釋,這個字符串十分特殊,node並不希望用戶獲取該模塊,因為得到的對象擁有直接調用底層C++代碼的能力,十分危險。

 

  完成准備工作后,就開始了事件輪詢跑進程,相關代碼如下:

{
  SealHandleScope seal(isolate);
  // 循環控制參數
  bool more;
  // 標記事件輪詢開始
  env.performance_state()->Mark(
      node::performance::NODE_PERFORMANCE_MILESTONE_LOOP_START);
  // 開始event_loop
  do {
      uv_run(env.event_loop(), UV_RUN_DEFAULT);
      v8_platform.DrainVMTasks(isolate);
      more = uv_loop_alive(env.event_loop());
      if (more)
          continue;
      RunBeforeExit(&env);
      // Emit `beforeExit` if the loop became alive either after emitting
      // event, or after running some callbacks.
      more = uv_loop_alive(env.event_loop());
  } while (more == true);
  // 標記事件輪詢結束
  env.performance_state()->Mark(
      node::performance::NODE_PERFORMANCE_MILESTONE_LOOP_EXIT);
}

  事件機制的實現在第三方依賴模塊uv當中,輪詢過程則是這里的do-while循環。

  一旦node輪詢結束,會返回一個exit_code,然后退出整個進程。

 

  完結,下一節寫啥還得再想想。


免責聲明!

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



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