1. 程式人生 > 其它 >Node.js 原始碼分析 - 從 main 函式開始

Node.js 原始碼分析 - 從 main 函式開始


title: Node.js 原始碼分析 - 從 main 函式開始
date: 2018-11-27 21:30:15
tags:
- Node.js
- Node.js 原始碼分析
- 原始碼分析
categories:
- Node.js 原始碼分析


此文最初於四年前釋出在個人站上的,現遷移至此重發,原連結: https://laogen.site/nodejs/nodejs-src/the-main/
《Node.js 原始碼分析》 系列目錄頁:https://laogen.site/nodejs/nodejs-src/index/

小目標

知道程式大概執行邏輯,關鍵點執行的順序

我們平時在終端敲下 node app.js

後,發生了什麼。

具體點,知道 node.js 原生(C++)模組什麼時候載入的,在哪載入的;
知道我們的 js 程式碼是在哪個環節被載入執行的;
知道程序的主迴圈(事件迴圈)什麼時候啟動的;

有了這個小目標的基礎,在接下來的文章中,我們再進一步的探索 node.js 原生模組的註冊是怎麼實現的,怎麼獲取 & 初始化的,怎麼曝露給 js 環境呼叫的;再細說 node.js 的模組機制,我們通常的 app.js 怎麼被執行的;

貼程式碼說明

限於篇幅,本文只先把大體執行流程捋出來,後面再開文一塊塊的捋。

原始碼太長,先把不影響我們分析的無關程式碼去掉,貼上來有關整體執行邏輯的程式碼,程式碼中的 // ...

註釋意思是這個地方有被省略的程式碼。

每段程式碼第一行的註釋都會指出原始檔位置,一些程式碼講解會在程式碼段中的註釋中進行;

本文不再介紹 V8 和 Libuv 的知識,會開專門的分類寫 V8 和 Libuv,參考 {% post_link nodejs/nodejs-src/index Node.js 原始碼分析 - 前言 %}

開捋:從 main 函式到程序主迴圈

main 函式

/* src/node_main.cc:93 */
int main(int argc, char* argv[]) {
  // ...
  return node::Start(argc, argv);
}

main函式

src/node_main.cc 這個檔案中,這個檔案主要就是存放 main函式

很簡單,只是呼叫了 node::Start(),這個函式在 src/node.cc 這個檔案中,接下來的核心程式碼都在這個檔案中。

初始化 V8 引擎

/* src/node.cc:3011 */
int Start(int argc, char** argv) {
  // ...

  std::vector<std::string> args(argv, argv + argc);
  std::vector<std::string> exec_args;

  // This needs to run *before* V8::Initialize().
  Init(&args, &exec_args);

  // ...
  v8_platform.Initialize(per_process_opts->v8_thread_pool_size);
  V8::Initialize();

  // ...
  const int exit_code = Start(uv_default_loop(), args, exec_args);

  v8_platform.StopTracingAgent();
  v8_initialized = false;
  V8::Dispose();
  v8_platform.Dispose();

  return exit_code;
}

在這段程式碼,首先進行 V8 的初始化,然後呼叫了另外一個 Start(uv_loop_t*, ...)函式,最後釋放資源,程序結束;

其中值得注意的一點,在初始化 V8 之前,呼叫了一個 Init() 函式,這個函式主要完成了 Node.js 原生(C++)模組的註冊,就是 fs http等模組的 C++ 實現模組。

/* src/node.cc:2559 */
void Init(std::vector<std::string>* argv, std::vector<std::string>* exec_argv) {
  // ...
  // Register built-in modules
  RegisterBuiltinModules();
  // ...
}

Init() 中呼叫了 RegisterBuiltinModules(),它註冊了所有 Node.js 原生模組,關於原生模組的註冊,本文不再繼續跟進去,下一篇會單獨展開這一塊,這裡先知道這個流程。

記住這個 RegisterBuiltinModules(),下一篇文章就從這裡開始展開。

建立 Isolate 例項

/* src/node.cc:2964 */
inline int Start(uv_loop_t* event_loop,
                 const std::vector<std::string>& args,
                 const std::vector<std::string>& exec_args) {
  std::unique_ptr<ArrayBufferAllocator, decltype(&FreeArrayBufferAllocator)>
      allocator(CreateArrayBufferAllocator(), &FreeArrayBufferAllocator);
  // 建立 Isolate 例項
  Isolate* const isolate = NewIsolate(allocator.get());

  // ...
  int exit_code;
  {
    Locker locker(isolate);
    Isolate::Scope isolate_scope(isolate);
    HandleScope handle_scope(isolate);
    // ...
    exit_code = Start(isolate, isolate_data.get(), args, exec_args);
  }
  // ...
  isolate->Dispose();
  return exit_code;
}

這個 Start() 倒也沒做什麼,主要工作是建立了 Isolate 例項,然後呼叫了另外一個 Start(Isolate*...)

程序主迴圈

/* src/node.cc:2868 */
inline int Start(Isolate* isolate, IsolateData* isolate_data,
                 const std::vector<std::string>& args,
                 const std::vector<std::string>& exec_args) {
  HandleScope handle_scope(isolate);
  // 建立 V8 Context 物件
  Local<Context> context = NewContext(isolate);
  Context::Scope context_scope(context);
  
  // 建立 Environment 物件,這個是 Node.js 的類
  Environment env(isolate_data, context, v8_platform.GetTracingAgentWriter());
  
  // 這裡面主要完成 libuv 的初始化,以及建立 process 物件
  // 就是 Node.js 中那個全域性的 process 物件,這裡不細展開
  env.Start(args, exec_args, v8_is_profiling);

  {
    // ...
    // LoadEnvironment 是本文重要的關鍵點
    LoadEnvironment(&env);
    env.async_hooks()->pop_async_id(1);
  }

  // 下面就是程序的主迴圈
  {
    // ...
    bool more;
    // ...
    do {
      uv_run(env.event_loop(), UV_RUN_DEFAULT);
      // ...
      more = uv_loop_alive(env.event_loop());
      if (more)
        continue;

      // ...
    } while (more == true);
  }
  // ...
  return exit_code;
}

這段程式碼建立並使用了 js 執行需要的 context,然後建立了 Environment 物件;

這個 Environment 物件是 Node.js 原始碼中重要的一個物件,它是一個全域性單例,定義和儲存了一些重要的全域性物件和函式,比如剛開始建立的 Isolate 物件、剛剛建立的 Context 物件等,注意它不是 V8 的,是 Node.js 定義的,對它的使用貫穿整個 Node.js 執行的生命週期。

再下面是程序的主迴圈,uv_run() 啟動了 Libuv 的事件迴圈, 它也是 Node.js 程序的主迴圈,Libuv 會單獨寫文介紹。

最後說一下,中間的 LoadEnvironment() 呼叫,它是在程式進入主迴圈之前最關鍵的一環;

LoadEnvironment() 完成了一些 js 檔案的載入和執行,其中就包括載入執行通常編寫的 app.js

主迴圈之前

/* src/node.cc:2115 */

void LoadEnvironment(Environment* env) {
  HandleScope handle_scope(env->isolate());
  // ...

  // The bootstrapper scripts are lib/internal/bootstrap/loaders.js and
  // lib/internal/bootstrap/node.js, each included as a static C string
  // defined in node_javascript.h, generated in node_javascript.cc by
  // node_js2c.
  // 載入兩個重要的 js 檔案:internal/bootstrap/loaders.js 
  // 和 internal/bootstrap/node.js
  Local<String> loaders_name =
      FIXED_ONE_BYTE_STRING(env->isolate(), "internal/bootstrap/loaders.js");
  MaybeLocal<Function> loaders_bootstrapper =
      GetBootstrapper(env, LoadersBootstrapperSource(env), loaders_name);
  Local<String> node_name =
      FIXED_ONE_BYTE_STRING(env->isolate(), "internal/bootstrap/node.js");
  MaybeLocal<Function> node_bootstrapper =
      GetBootstrapper(env, NodeBootstrapperSource(env), node_name);
  // ...
  // Add a reference to the global object
  Local<Object> global = env->context()->Global();

  env->SetMethod(env->process_object(), "_rawDebug", RawDebug);

  // Expose the global object as a property on itself
  // (Allows you to set stuff on `global` from anywhere in JavaScript.)
  global->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "global"), global);

  // 準備 binding 函式,下面呼叫 js 會作為引數傳給 js 環境
  // Create binding loaders
  Local<Function> get_binding_fn =
      env->NewFunctionTemplate(GetBinding)->GetFunction(env->context())
          .ToLocalChecked();

  Local<Function> get_linked_binding_fn =
      env->NewFunctionTemplate(GetLinkedBinding)->GetFunction(env->context())
          .ToLocalChecked();

  Local<Function> get_internal_binding_fn =
      env->NewFunctionTemplate(GetInternalBinding)->GetFunction(env->context())
          .ToLocalChecked();

  // 準備執行 internal/bootstrap/loaders.js 檔案的引數
  Local<Value> loaders_bootstrapper_args[] = {
    env->process_object(),
    get_binding_fn,
    get_linked_binding_fn,
    get_internal_binding_fn,
    Boolean::New(env->isolate(),
                 env->options()->debug_options->break_node_first_line)
  };

  // 執行 internal/bootstrap/loaders.js 
  // Bootstrap internal loaders
  // 這個物件是用來接收執行結果的,記住是 bootstrapped_loaders,下面會用到
  Local<Value> bootstrapped_loaders;
  if (!ExecuteBootstrapper(env, loaders_bootstrapper.ToLocalChecked(),
                           arraysize(loaders_bootstrapper_args),
                           loaders_bootstrapper_args,
                           &bootstrapped_loaders)) {
    return;
  }

  // 準備執行 internal/bootstrap/node.js 的引數
  // Bootstrap Node.js
  Local<Object> bootstrapper = Object::New(env->isolate());
  SetupBootstrapObject(env, bootstrapper);
  Local<Value> bootstrapped_node;
  Local<Value> node_bootstrapper_args[] = {
    env->process_object(),
    bootstrapper,
    // 注意,這裡是上面執行 loaders.js 返回的結果物件,
    // 作為執行引數傳給 internal/bootstrap/node.js
    bootstrapped_loaders  
  };

  // 執行 internal/bootstrap/node.js
  if (!ExecuteBootstrapper(env, node_bootstrapper.ToLocalChecked(),
                           arraysize(node_bootstrapper_args),
                           node_bootstrapper_args,
                           &bootstrapped_node)) {
    return;
  }
}

LoadEnvironment() 首先載入了兩個 js 檔案,這兩個 js 檔案的位置分別在:
lib/internal/bootstrap/loaders.jslib/internal/bootstrap/node.js

我們 Node.js 開發者寫的 app.js 其實就是在這兩個 js 檔案中載入並執行的,這塊是最重要的邏輯之一,內容也很多,後面的文章會詳細展開。

LoadEnvironment() 接下來建立了三個 binding 函式:

  • get_binding_fn
  • get_linked_binding_fn
  • get_internal_binding_fn

這3個 binding 函式是用來獲取和載入 Node.js 原生模組的,會傳入到 js 執行環境中,也就是你在 js 程式碼中是可以呼叫的,比如 process.binding('fs'),在我們用 C++ 開發 Node.js 擴充套件模組的時候,也會用到,以後會詳細展開。

LoadEnvironment() 接下來要執行 lib/internal/bootstrap/loaders.js,在這個 js 檔案中主要定義了內部(internal)模組載入器(loaders)。

lib/internal/bootstrap/loaders.js 定義的模組載入器(loaders) 接下來做為執行引數,傳入了 lib/internal/bootstrap/node.js,在 lib/internal/bootstrap/node.js 中會使用這些 loaders 來載入 internal 模組。

lib/internal/bootstrap/node.js 做了很多工作,這裡只需要知道,它最終載入並執行了我們 Node.js 程式設計師編寫的 app.js 就可以了。

到此為止,我們就知道了在命令列敲下 node app.js 大概發生了哪些事!

小結

這只是個大概邏輯,可以配合 Node.js 原始碼,再花時間捋一捋,光靠貼的這點程式碼,可能還是會迷糊的。

接下來的文章,就是對這個執行邏輯中的關鍵點分別展開。

作者水平有限,寫的也倉促,有誤之處還請指出。

Maslow ([email protected]), laf.js 作者。

lafyun.com 開源雲開發平臺,前端變全棧,無需服務端。