nodejs啟動流程分析

妖怪來了發表於2018-09-11

前言

之前用過一段時間的v8 ,也只是會初始化那個流程,最近想深入瞭解一下,所以想要通過學習 nodejs 來加深理解。這篇文章主要是講講 nodejs 的初始化流程,如有錯誤,煩請指教~。(本文分析基於 v10.9.0,本文會盡量避免大段原始碼,但是為了有理有據,還是會放上一些精簡過並帶有註釋的程式碼上來)。

Helloworld 鎮樓:

const http = require('http');
const hostname = '127.0.0.1';
const port = 8888;

http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('Hello World\n');
}).listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});
複製程式碼

寫過 nodejs 的都能看懂如上程式碼。寥寥數行,就建立了一個 http 服務。第一行程式碼,就出現了一個 require 關鍵字,那麼 require 是從何而來呢?帶著這個問題,我們一起去看下吧。

啟動流程

1. node 的目錄結構,此處就不再分析了。最重要的就是 src 和 lib 了。 src 路徑下是 node 的 C++ 實現的主要原始碼目錄,而 lib 主要是 JavaScript 實現所在目錄。稍微有一些 C++ 程式設計基礎的同學應該知道,C++ 的啟動函式就是 main 函式。那麼 node 的啟動函式在哪呢。通過全文搜尋,可以確定,啟動函式就在 src/node_main.cc 這個檔案當中了。此處擷取部分原始碼:

// windows 啟動方法。
int wmain(int argc, wchar_t* wargv[]) {
  //...
  // 啟動方法。
  return node::Start(argc, argv);
}
//...
// 類linux 啟動方法。
int main(int argc, char* argv[]) {
    // ...
    // 啟動方法。
    return node::Start(argc, argv);
}
複製程式碼

可以看到,這個只是一個外殼,做了一些邏輯判斷,最終的核心就是呼叫 Start 方法。

2. Start 方法位於 src/node.cc:

int Start(int argc, char** argv) {
    //...
    Init(&argc, const_cast<const char**>(argv), &exec_argc, &exec_argv); // 1.
    // v8 初始化。
    InitializeV8Platform(per_process_opts->v8_thread_pool_size);
    v8_initialized = true;
    // 開始事件迴圈。
    const int exit_code =
        Start(uv_default_loop(), args, exec_args);  // 2.
    //... v8 開始銷燬。
    v8_initialized = false;
    V8::Dispose();
	//...
    return exit_code;
}
複製程式碼

可以看到,Start 方法主要是執行了一個 Init 方法以及對 v8 進行了初始化的操作,然後開啟了整個事件迴圈流程。

2.1 來看看 Init 方法做了些什麼事情,同樣位於 src/node.cc 中:

void Init(int* argc,
          const char** argv,
          int* exec_argc,
          const char*** exec_argv) {
  //... 註冊內部模組。 此處暫時不細講。
  RegisterBuiltinModules();
  //...  處理引數,列印 help 等。
  ProcessArgv(argv, exec_argv, false);
  //...
}
複製程式碼

2.2 接著讓我們看看裡面這個 Start 方法做了什麼。同樣位於 src/node.cc 中:

inline int Start(uv_loop_t* event_loop,
                 const std::vector<std::string>& args,
                 const std::vector<std::string>& exec_args) {
  //... 開始建立 Isolate 例項。 
  Isolate* const isolate = NewIsolate(allocator.get(), event_loop);
  //...
  {
    //... 又是一個 Start 。
    exit_code = Start(isolate, isolate_data.get(), args, exec_args);
  }
  // isolate 銷燬。
  isolate->Dispose();
  //...
  return exit_code;
}
複製程式碼

引數檢查什麼的就略過了,上來先建立了一個 Isolate 例項,這個例項相當於是一個 js 獨立環境,更粗略一點,比作一個頁面。 中間又呼叫了一個 Start 方法,最終處理一下 isolate 的銷燬。

3. 那接著來看這個 Start 方法(麻木了,都叫 Start 方法。)同樣位於 src/node.cc 中:

inline int Start(Isolate* isolate, IsolateData* isolate_data,
                 const std::vector<std::string>& args,
                 const std::vector<std::string>& exec_args) {
  //... 建立一個 Context
  Local<Context> context = NewContext(isolate); // 1.
  //... 建立一個 Environment 例項,並開啟 Start 方法。
  Environment env(isolate_data, context, v8_platform.GetTracingAgentWriter());
  env.Start(args, exec_args, v8_is_profiling); // 2.
  {
    //... 環境載入
    LoadEnvironment(&env);  // 3.
    //...
  }

  {
    //...
    do {
      // 事件迴圈啟動。libuv 相關。 4.
      uv_run(env.event_loop(), UV_RUN_DEFAULT);
      //...
    } while (more == true);
    //...
  }
  //...
  const int exit_code = EmitExit(&env);
  //... 善後工作,資源回收等等。
  return exit_code;
}
複製程式碼

Context 又是 v8 的一個概念,相當於執行上下文,js 的執行上下文,可以實現互不影響。比如一個頁面上巢狀了某個頁面,那麼他們之間的 js 上下文環境就不一樣。此處需要關注 1 , 2,3,4 四個方法。

3.1 先來看看 1 ,如何建立的 Context。NewContext 同樣位於 src/node.cc 中:

Local<Context> NewContext(Isolate* isolate,
                          Local<ObjectTemplate> object_template) {
  // 使用 v8 的 api 建立 Context。 
  auto context = Context::New(isolate, nullptr, object_template);
  // ...
  {
    // ... Run lib/internal/per_context.js
    // 獲取 per_context.js 檔案的字串。
    Local<String> per_context = NodePerContextSource(isolate);
    // 編譯執行,v8的模板程式碼。
    ScriptCompiler::Source per_context_src(per_context, nullptr);
    Local<Script> s = ScriptCompiler::Compile(
        context,
        &per_context_src).ToLocalChecked();
    s->Run(context).ToLocalChecked();
  }
  return context;
}

複製程式碼

此方法不僅僅建立了一個 Context,而且還預載入執行了一段js。注意這個 NodePerContextSource 方法只有編譯過才會有這個檔案。

3.1.1 看一下這個方法.檔案位於node_javascript.cc 中:

v8::Local<v8::String> NodePerContextSource(v8::Isolate* isolate) {
    return internal_per_context_value.ToStringChecked(isolate);
}
static const uint8_t raw_internal_per_context_value[] = { 39,...}
static struct : public v8::String::ExternalOneByteStringResource {
    const char* data() const override {
        return reinterpret_cast<const char*>(raw_internal_per_context_value);
    }
    //...
    v8::Local<v8::String> ToStringChecked(v8::Isolate* isolate) {
        return v8::String::NewExternalOneByte(isolate, this).ToLocalChecked();
    }
} internal_per_context_value;
複製程式碼

看到這裡應該知道了,就是把 raw_internal_per_context_value 這個陣列轉成 v8 的字串返回出去。那麼問題來了,這個陣列裡面到底是什麼東西呢。

3.1.2 猜也沒法猜,那就列印一下唄。列印陣列相關程式碼如下:

#include <string>
#include <iostream>
static const unsigned char raw_internal_per_context_value[] = {39,...}
int main() {
    std::cout << (char *)raw_internal_bootstrap_loaders_value << std::endl;
}
複製程式碼

g++ -o test test.cc & ./test 就可以看到內容了。你會驚奇的發現,這不就是 lib/internal/per_context.js 檔案的內容嗎?是的,的確是這樣,他就是把這段文字直接在編譯期間就編成C++字元陣列,為了在啟動的時候加快啟動速度,不至於現場去讀檔案從而引發檔案載入速度的等等一系列問題。至於此 js 檔案內容,在此先不做講解。接著讓我回到 4~5步的方法2當中。

**3.2 ** env.Start 方法位於 src/env.cc 中:

void Environment::Start(const std::vector<std::string>& args,
                        const std::vector<std::string>& exec_args,
                        bool start_profiler_idle_notifier) {
    //... 一大堆的 uv 操作等等。
    // 設定了 process。
    auto process_template = FunctionTemplate::New(isolate()); 
    process_template->SetClassName(FIXED_ONE_BYTE_STRING(isolate(), "process"));
    // ...
}
複製程式碼

可以看到其中設定了 process 是什麼,此處設定了之後,在js裡面就可以直接拿到 process 變數了。

3.3 LoadEnvironment 方法在 src/node.cc 中:

void LoadEnvironment(Environment* env) {
  //...
  // 載入 lib/internal/bootstrap/loaders.js 和 node.js 進來。
  // FIXED_ONE_BYTE_STRING 就是一個轉換字串的巨集。
  Local<String> loaders_name =
      FIXED_ONE_BYTE_STRING(env->isolate(), "internal/bootstrap/loaders.js");
  // LoadersBootstrapperSource 是獲取 loaders.js 的檔案內容。 GetBootstrapper 方法是用來
  // 執行 js 的。
  MaybeLocal<Function> loaders_bootstrapper =
      GetBootstrapper(env, LoadersBootstrapperSource(env), loaders_name);
  //...
  // 獲取 global 物件
  Local<Object> global = env->context()->Global();
  //...
  // 暴露 global 出去,在 js 中可以訪問。
  global->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "global"), global);

  // 建立bind,linked_binding,internal_binding
  Local<Function> get_binding_fn =
      env->NewFunctionTemplate(GetBinding)->GetFunction(env->context())
          .ToLocalChecked();
  //...

  // 執行 internal/loaders.js,node.js 裡面的方法。
  if (!ExecuteBootstrapper(env, loaders_bootstrapper.ToLocalChecked(),
                           arraysize(loaders_bootstrapper_args),
                           loaders_bootstrapper_args,
                           &bootstrapped_loaders)) {
    return;
  }
  //...
}

static void GetBinding(const FunctionCallbackInfo<Value>& args) {
  // ... 通過引數獲取模組名。
  Local<String> module = args[0].As<String>();
  //... 獲取內部模組。此處就是通過2.1步驟中的 RegisterBuiltinModules 巨集處理之後的東西來獲取的。
  node_module* mod = get_builtin_module(*module_v);
  Local<Object> exports;
  if (mod != nullptr) {
    // 呼叫模組初始化方法。
    exports = InitModule(env, mod, module);
  }
  // ... 設定返回值。
  args.GetReturnValue().Set(exports);
}
複製程式碼

程式碼很長,但是條理還是挺清晰的。這裡進行了一些繫結操作和一些初始化方法的呼叫邏輯。此處也可以知道,GetBinding 類似的東西是什麼。呼叫的 js 如何執行需要和 js 一起看才能明白。此處先不講解了。

3.4 uv_run 這個方法此處也不細講了。 libuv 這個庫還沒有詳細瞭解。等待了解之後,補上 libuv 的相關呼叫分析,此處我們知道,在這裡開始執行事件迴圈了。

結語

講了這麼多,大家應該對 nodejs 的啟動流程有了一個大致的瞭解了吧。雖然開頭說少點原始碼,可是後來還是夾雜了很多的原始碼,哈哈,有一種上當的感覺。後面再講講模組載入,libuv載入的相關東西。這次分析就到此結束吧,大家休息~

相關文章