nodejs源碼—初始化

概述

相信不少的人,天天在終端不止一遍的執行着node這條命令,對於不少人來講,它就像一個黑盒,並不知道背後到底發生了什麼,本文將會爲你們揭開這個神祕的面紗,因爲本人水平有限,因此只是講一個大概其,主要關注的過程就是node模塊的初始化,event loopv8的部分基本沒有深刻,這些部分能夠關注一下我之後的文章。(提示本文很是的長,但願你們不要看煩~)javascript

從官網學習Node.js Process模塊方法速查

node是什麼?

這個問題不少人都會回答就是v8 + libuv,可是除了這個兩個庫之外node還依賴許多優秀的開源庫,能夠經過process.versions來看一下:
html

  • http_parser主要用於解析http數據包的模塊,在這個庫的做者也是ry,一個純c的庫,無任何依賴
  • v8這個你們就很是熟悉了,一個優秀的js引擎
  • uv這個就是ry實現的libuv,其封裝了libevIOCP,實現了跨平臺,node中的i/o就是它,儘管js是單線程的,可是libuv並非,其有一個線程池來處理這些i/o操做。
  • zlib主要來處理壓縮操做,諸如熟悉的gzip操做
  • aresc-ares,這個庫主要用於解析dns,其也是異步的
  • modules就是node的模塊系統,其遵循的規範爲commonjs,不過node也支持了ES模塊,不過須要加上參數而且文件名後綴須要爲mjs,經過源碼看,nodeES模塊的名稱做爲了一種url來看待,具體能夠參見這裏
  • nghttp2如其名字同樣,是一個http2的庫
  • napi是在node8出現,node10穩定下來的,能夠給編寫node原生模塊更好的體驗(終於不用在依賴於nan,每次更換node版本還要從新編譯一次了)
  • openssl很是著名的庫,tls模塊依賴於這個庫,固然還包括https
  • icu就是small-icu,主要用於解決跨平臺的編碼問題,versions對象中的unicodecldrtz也源自icu,這個的定義能夠參見這裏

從這裏能夠看出的是process對象在node中很是的重要,我的的理解,其實node與瀏覽器端最主要的區別,就在於這個process對象java

注:node只是用v8來進行js的解析,因此不必定非要依賴v8,也能夠用其餘的引擎來代替,好比利用微軟的ChakraCore,對應的node倉庫node

node初始化

通過上面的一通分析,對node的全部依賴有了必定的瞭解,下面來進入正題,看一下node的初始化過程:python

挖坑

node_main.cc爲入口文件,能夠看到的是除了調用了node::Start以外,還作了兩件事情:c++

NODE_SHARED_MODE忽略SIGPIPE信號

SIGPIPE信號出現的狀況通常在socket收到RST packet以後,扔向這個socket寫數據時產生,簡單來講就是clientserver發請求,可是這時候client已經掛掉,這時候就會產生SIGPIPE信號,產生這個信號會使server端掛掉,其實node::PlatformInit中也作了這種操做,不過只是針對non-shared lib buildgit

改變緩衝行爲

stdout的默認緩衝行爲爲_IOLBF(行緩衝),可是對於這種來講交互性會很是的差,因此將其改成_IONBF(不緩衝)github

探索

node.cc文件中總共有三個Start函數,先從node_main.cc中掉的這個Start函數開始看:json

int Start(int argc, char** argv) {
  // 退出以前終止libuv的終端行爲,爲正常退出的狀況
  atexit([] () { uv_tty_reset_mode(); });
  // 針對平臺進行初始化
  PlatformInit();
  // ...
  Init(&argc, const_cast<const char**>(argv), &exec_argc, &exec_argv);
  // ...
  v8_platform.Initialize(v8_thread_pool_size);
  // 熟悉的v8初始化函數
  V8::Initialize();
  // ..
  const int exit_code =
    Start(uv_default_loop(), argc, argv, exec_argc, exec_argv);
}

上面函數只保留了一些關鍵不走,先來看看PlatformInitbootstrap

PlatfromInit

unix中將一切都看做文件,進程啓動時會默認打開三個i/o設備文件,也就是stdin stdout stderr,默認會分配0 1 2三個描述符出去,對應的文件描述符常量爲STDIN_FILENO STDOUT_FILENO STDERR_FILENO,而windows中沒有文件描述符的這個概念,對應的是句柄,PlatformInit首先是檢查是否將這個三個文件描述符已經分配出去,若沒有,則利用open("/dev/null", O_RDWR)分配出去,對於windows作了一樣的操做,分配句柄出去,並且windows只作了這一個操做;對於unix來講還會針對SIGINT(用戶調用Ctrl-C時發出)和SIGTERMSIGTERMSIGKILL相似,可是不一樣的是該信號能夠被阻塞和處理,要求程序本身退出)信號來作一些特殊處理,這個處理與正常退出時同樣;另外一個重要的事情就是下面這段代碼:

struct rlimit lim;
  // soft limit 不等於 hard limit, 意味着能夠增長
  if (getrlimit(RLIMIT_NOFILE, &lim) == 0 && lim.rlim_cur != lim.rlim_max) {
    // Do a binary search for the limit.
    rlim_t min = lim.rlim_cur;
    rlim_t max = 1 << 20;
    // But if there's a defined upper bound, don't search, just set it.
    if (lim.rlim_max != RLIM_INFINITY) {
      min = lim.rlim_max;
      max = lim.rlim_max;
    }
    do {
      lim.rlim_cur = min + (max - min) / 2;
      // 對於mac來講 hard limit 爲unlimited
      // 可是內核有限制最大的文件描述符,超過這個限制則設置失敗
      if (setrlimit(RLIMIT_NOFILE, &lim)) {
        max = lim.rlim_cur;
      } else {
        min = lim.rlim_cur;
      }
    } while (min + 1 < max);
  }

這件事情也就是提升一個進程容許打開的最大文件描述符,可是在mac上很是的奇怪,執行ulimit -H -n獲得hard limitunlimited,因此我認爲mac上的最大文件描述符會被設置爲1 << 20,可是最後通過實驗發現最大隻能爲24576,很是的詭異,最後通過一頓搜索,查到了原來mac的內核對能打開的文件描述符也有限制,能夠用sysctl -A | grep kern.maxfiles進行查看,果真這個數字就是24576

Init

Init函數調用了RegisterBuiltinModules

// node.cc
void RegisterBuiltinModules() {
#define V(modname) _register_##modname();
  NODE_BUILTIN_MODULES(V)
#undef V
}

// node_internals.h
#define NODE_BUILTIN_MODULES(V)                                               \
  NODE_BUILTIN_STANDARD_MODULES(V)                                            \
  NODE_BUILTIN_OPENSSL_MODULES(V)                                             \
  NODE_BUILTIN_ICU_MODULES(V)

從名字也能夠看出上面的過程是進行c++模塊的初始化,node利用了一些宏定義的方式,主要關注NODE_BUILTIN_STANDARD_MODULES這個宏:

#define NODE_BUILTIN_STANDARD_MODULES(V)                                      \
    V(async_wrap)                                                             \
    V(buffer)
    ...

結合上面的定義,能夠得出編譯後的代碼大概爲:

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

而這些_register又是從哪裏來的呢?以buffer來講,對應c++文件爲src/node_buffer.cc,來看這個文件的最後一行,第二個參數是模塊的初始化函數:

NODE_BUILTIN_MODULE_CONTEXT_AWARE(buffer, node::Buffer::Initialize)

這個宏存在於node_internals.h中:

#define NODE_MODULE_CONTEXT_AWARE_CPP(modname, regfunc, priv, flags)
  static node::node_module _module = {
    NODE_MODULE_VERSION,                                                      
    flags,                                                                    
    nullptr,                                                                  
    __FILE__,                                                                  
    nullptr,                                                                   
    (node::addon_context_register_func) (regfunc),// 暴露給js使用的模塊的初始化函數
    NODE_STRINGIFY(modname),                                                 
    priv,                                                                     
    nullptr                                                                   
  };                                                                          
  void _register_ ## modname() {                                              
    node_module_register(&amp;_module);                                           
  }


#define NODE_BUILTIN_MODULE_CONTEXT_AWARE(modname, regfunc)                   
  NODE_MODULE_CONTEXT_AWARE_CPP(modname, regfunc, nullptr, NM_F_BUILTIN)

發現調用的_register_buffer實質上調用的是node_module_register(&_module),每個c++模塊對應的爲一個node_module結構體,再來看看node_module_register發生了什麼:

extern "C" void node_module_register(void* m) {
  struct node_module* mp = reinterpret_cast&lt;struct node_module*&gt;(m);

  if (mp-&gt;nm_flags &amp; NM_F_BUILTIN) {
    mp-&gt;nm_link = modlist_builtin;
    modlist_builtin = mp;
  }
  ...
}

由此能夠見,c++模塊被存儲在了一個鏈表中,後面process.binding()本質上就是在這個鏈表中查找對應c++模塊,node_module是鏈表中的一個節點,除此以外Init還初始化了一些變量,這些變量基本上都是取決於環境變量用getenv得到便可

v8初始化

到執行完Init爲止,尚未涉及的jsc++的交互,在將一些環境初始化以後,就要開始用v8這個大殺器了,v8_platform是一個結構體,能夠理解爲是node對於v8v8::platform一個封裝,緊接着的就是對v8進行初始化,自此開始具有了與js進行交互的能力,初始化v8以後,建立了一個libuv事件循環就進入了下一個Start函數

第二個Start函數

inline int Start(uv_loop_t* event_loop,
                 int argc, const char* const* argv,
                 int exec_argc, const char* const* exec_argv) {
  std::unique_ptr&lt;ArrayBufferAllocator, decltype(&amp;FreeArrayBufferAllocator)&gt;
      allocator(CreateArrayBufferAllocator(), &amp;FreeArrayBufferAllocator);
  Isolate* const isolate = NewIsolate(allocator.get());
  // ...
  {
    Locker locker(isolate);
    Isolate::Scope isolate_scope(isolate);
    HandleScope handle_scope(isolate);
  }
}

首先建立了一個v8Isolate(隔離),隔離在v8中很是常見,彷彿和進程同樣,不一樣隔離不共享資源,有着本身得堆棧,可是正是由於這個緣由在多線程的狀況下,要是對每個線程都建立一個隔離的話,那麼開銷會很是的大(可喜可賀的是node有了worker_threads),這時候能夠藉助Locker來進行同步,同時也保證了一個Isolate同一時刻只能被一個線程使用;下面兩行就是v8的常規套路,下一步通常就是建立一個Context(最簡化的一個流程能夠參見v8hello world),HandleScope叫作句柄做用域,通常都是放在函數的開頭,來管理函數建立的一些句柄(水平有限,暫時不深究,先挖個坑);第二個Start的主要流程就是這個,下面就會進入最後一個Start函數,這個函數能夠說是很是的關鍵,會揭開全部的謎題

解開謎題

inline int Start(Isolate* isolate, IsolateData* isolate_data,
                 int argc, const char* const* argv,
                 int exec_argc, const char* const* exec_argv) {
  HandleScope handle_scope(isolate);
  // 常規套路
  Local&lt;Context&gt; context = NewContext(isolate);
  Context::Scope context_scope(context);
  Environment env(isolate_data, context, v8_platform.GetTracingAgentWriter());
  env.Start(argc, argv, exec_argc, exec_argv, v8_is_profiling);
  // ...

能夠見到v8的常見套路,建立了一個上下文,這個上下文就是js的執行環境,Context::Scope是用來管理這個ContextEnvironment能夠理解爲一個node的運行環境,記錄了isolate,event loop等,Start的過程主要是作了一些libuv的初始化以及process對象的定義:

auto process_template = FunctionTemplate::New(isolate());
  process_template-&gt;SetClassName(FIXED_ONE_BYTE_STRING(isolate(), "process"));

  auto process_object =
      process_template-&gt;GetFunction()-&gt;NewInstance(context()).ToLocalChecked();
  set_process_object(process_object);

  SetupProcessObject(this, argc, argv, exec_argc, exec_argv);

SetupProcessObject生成了一個c++層面上的process對象,這個已經基本上和平時node中的process對象一致,可是還會有一些出入,好比沒有binding等,完成了這個過程以後就開始了LoadEnvironment

LoadEnvironment

Local&lt;String&gt; loaders_name =
    FIXED_ONE_BYTE_STRING(env-&gt;isolate(), "internal/bootstrap/loaders.js");
MaybeLocal&lt;Function&gt; loaders_bootstrapper =
    GetBootstrapper(env, LoadersBootstrapperSource(env), loaders_name);
Local&lt;String&gt; node_name =
    FIXED_ONE_BYTE_STRING(env-&gt;isolate(), "internal/bootstrap/node.js");
MaybeLocal&lt;Function&gt; node_bootstrapper =
    GetBootstrapper(env, NodeBootstrapperSource(env), node_name);

先將lib/internal/bootstrap文件夾下的兩個文件讀進來,而後利用GetBootstrapper來執行js代碼分別獲得了一個函數,一步步來看,先看看GetBootstrapper爲何能夠執行js代碼,查看這個函數能夠發現主要是由於ExecuteString

MaybeLocal&lt;v8::Script&gt; script =
    v8::Script::Compile(env-&gt;context(), source, &amp;origin);
...
MaybeLocal&lt;Value&gt; result = script.ToLocalChecked()-&gt;Run(env-&gt;context());

這個主要利用了v8的能力,對js文件進行了解析和執行,打開loaders.js看看其參數,須要五個,撿兩個最重要的來講,分別是processgetBinding,這裏面日後繼續看LoadEnvironment發現process對象就是剛剛生成的,而getBinding是函數GetBinding

node_module* mod = get_builtin_module(*module_v);
Local&lt;Object&gt; exports;
if (mod != nullptr) {
  exports = InitModule(env, mod, module);
} else if (!strcmp(*module_v, "constants")) {
  exports = Object::New(env-&gt;isolate());
  CHECK(exports-&gt;SetPrototype(env-&gt;context(),
                              Null(env-&gt;isolate())).FromJust());
  DefineConstants(env-&gt;isolate(), exports);
} else if (!strcmp(*module_v, "natives")) { // NativeModule _source
  exports = Object::New(env-&gt;isolate());
  DefineJavaScript(env, exports);
} else {
  return ThrowIfNoSuchModule(env, *module_v);
}

args.GetReturnValue().Set(exports);

其做用就是根據傳參來初始化指定的模塊,固然也有比較特殊的兩個分別是constantsnatives(後面再看),get_builtin_module調用的就是FindModule,還記得以前在Init過程當中將模塊都註冊到的鏈表嗎?FindModule就是遍歷這個鏈表找到相應的模塊:

struct node_module* mp;
for (mp = list; mp != nullptr; mp = mp-&gt;nm_link) {
  if (strcmp(mp-&gt;nm_modname, name) == 0)
    break;
}

InitModule就是調用以前註冊模塊定義的初始化函數,還以buffer看的話,就是執行node::Buffer::Initialize函數,打開着函數來看和平時寫addon的方式同樣,也會暴露一個對象出來供js調用;LoadEnvironment下面就是將process, GetBinding等做爲傳入傳給上面生成好的函數而且利用v8來執行,來到了你們熟悉的領域,來看看loaders.js

const moduleLoadList = [];
ObjectDefineProperty(process, 'moduleLoadList', {
  value: moduleLoadList,
  configurable: true,
  enumerable: true,
  writable: false
});

定義了一個已經加載的Module的數組,也能夠在node經過process.moduleLoadList來看看加載了多少的原生模塊進來

process.binding

process.binding = function binding(module) {
  module = String(module);
  let mod = bindingObj[module];
  if (typeof mod !== 'object') {
    mod = bindingObj[module] = getBinding(module);
    moduleLoadList.push(`Binding ${module}`);
  }
  return mod;
};

終於到了這個方法,翻看lib中的js文件,有着很是多的這種調用,這個函數就是對GetBinding作了一個js層面的封裝,作的無非是查看一下這個模塊是否已經加載完成了,是的話直接返回回去,不須要再次初始化了,因此利用prcoess.binding加載了對應的c++模塊(能夠執行一下process.binding('buffer'),而後再去node_buffer.cc中看看)繼續向下看,會發現定義了一個class就是NativeModule,發現其有一個靜態屬性:

加載js

NativeModule._source = getBinding('natives');

返回到GetBinding函數,看到的是一個if分支就是這種狀況:

exports = Object::New(env-&gt;isolate());
DefineJavaScript(env, exports);

來看看DefineJavaScript發生了什麼樣的事情,這個函數發現只能在頭文件(node_javascript.h)裏面找到,可是根本找不到具體的實現,這是個什麼鬼???去翻一下node.gyp文件發現這個文件是用js2c.py這個文件生成的,去看一下這個python文件,能夠發現許多的代碼模板,每個模板都是用Render返回的,data參數就是js文件的內容,最終會被轉換爲c++中的byte數組,同時定義了一個將其轉換爲字符串的方法,那麼問題來了,這些文件都是那些呢?答案仍是在node.gyp中,就是library_files數組,發現包含了lib下的全部的文件和一些dep下的js文件,DefineJavaScript這個文件作的就是將待執行的js代碼註冊下,因此NativeModule._source中存儲的是一些待執行的js代碼,來看一下NativeModule.require

NativeModule

const cached = NativeModule.getCached(id);
if (cached &amp;&amp; (cached.loaded || cached.loading)) {
  return cached.exports;
}
moduleLoadList.push(`NativeModule ${id}`);

const nativeModule = new NativeModule(id);

nativeModule.cache();
nativeModule.compile();

return nativeModule.exports;

能夠發現NativeModule也有着緩存的策略,require先把其放到_cache中再次require就不會像第一次那樣執行這個模塊,而是直接用緩存中執行好的,後面說的Module與其同理,看一下compile的實現:

let source = NativeModule.getSource(this.id);
source = NativeModule.wrap(source);

NativeModule.wrap = function(script) {
  return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
};
NativeModule.wrapper = [
  '(function (exports, require, module, process) {',
  '\n});'
];

首先從_source中取出相應的模塊,而後對這個模塊進行包裹成一個函數,執行函數用的是什麼呢?

const script = new ContextifyScript(
  source, this.filename, 0, 0,
  codeCache[this.id], false, undefined
);

this.script = script;
const fn = script.runInThisContext(-1, true, false);
const requireFn = this.id.startsWith('internal/deps/') ?
  NativeModule.requireForDeps :
  NativeModule.require;
fn(this.exports, requireFn, this, process);

本質上就是調用了vm編譯自婦產獲得函數,而後給其傳入了一些參數並執行,this.exports就是一個對象,require區分了一下是否加載node依賴的js文件,this也就是參數module,這也說明了二者的關係,exports就是module的一個屬性,也解釋了爲何exports.xx以後再指定module.exports = yy會將xx忽略掉,還記得LoadEnvironment嗎?bootstrap/loaders.js執行完以後執行了bootstrap/node.js,能夠說這個文件是node真正的入口,好比定義了global對象上的屬性,好比console setTimeout等,因爲篇幅有限,來挑一個最經常使用的場景,來看看這個是什麼一回事:

else if (process.argv[1] &amp;&amp; process.argv[1] !== '-') {
  const path = NativeModule.require('path');
  process.argv[1] = path.resolve(process.argv[1]);

  const CJSModule = NativeModule.require('internal/modules/cjs/loader');
  ...
  CJSModule.runMain();
}

這個過程就是熟悉的node index.js這個過程,能夠看到的對於開發者本身的js來講,在node中對應的classModule,相信這個文件你們不少人都瞭解,與NativeModule相相似,不一樣的是,須要進行路徑的解析和模塊的查找等,來大體的看一下這個文件,先從上面調用的runMain來看:

if (experimentalModules) {
  // ...
} else {
  Module._load(process.argv[1], null, true);
}

Module

node中開啓--experimental-modules能夠加載es模塊,也就是能夠不用babel轉義就可使用import/export啦,這個不是重點,重點來看普通的commonnjs模塊,process.argv[1]通常就是要執行的入口文件,下面看看Module._load

Module._load = function(request, parent, isMain) {
  if (parent) {
    debug('Module._load REQUEST %s parent: %s', request, parent.id);
  }
  // 查找文件具體位置
  var filename = Module._resolveFilename(request, parent, isMain);

  // 存在緩存,則不須要再次執行
  var cachedModule = Module._cache[filename];
  if (cachedModule) {
    updateChildren(parent, cachedModule, true);
    return cachedModule.exports;
  }

  // 加載node原生模塊,原生模塊不須要緩存,由於NativeModule中也存在緩存
  if (NativeModule.nonInternalExists(filename)) {
    debug('load native module %s', request);
    return NativeModule.require(filename);
  }

  // 加載並執行一個模塊
  var module = new Module(filename, parent);

  if (isMain) {
    process.mainModule = module;
    module.id = '.';
  }

  Module._cache[filename] = module;

  // 調用load方法進行加載
  tryModuleLoad(module, filename);

  return module.exports;
};

這裏看每個Module有一個parent的屬性,假如a.js中引入了b.js,那麼Module bparent就是Module a,利用resolveFilename能夠獲得文件具體的位置,這個過程然後調用load函數來加載文件,能夠看到的是區分了幾種類型,分別是.js .json .node,對應的.js是讀文件而後執行,.json是直接讀文件後JSON.parse一下,.node是調用dlopenModule.compileNativeModule.compile相相似都是想包裹一層成爲函數,而後調用了vm編譯獲得這個函數,最後傳入參數來執行,對於Module來講,包裹的代碼以下:

Module.wrapper = [
  '(function (exports, require, module, __filename, __dirname) { ',
  '\n});'
];

執行完上述過程後,前期工做就已經作得比較充分了,再次回到最後一個Start函數來看,從代碼中能夠看到開始了nodeevent loop,這就是node的初始化過程,關於event loop須要對libuv有必定的瞭解,能夠說node真正離不開的是libuv,具體這方面的東西,能夠繼續關注我後面的文章

總結

總結一下這個過程,以首次加載沒有任何緩存的狀況開看:require('fs'),先是調用了Module.require,然後發現爲原生模塊,因而調用NativeModule.require,從NativeModule._sourcelib/fs的內容拿出來包裹一下而後執行,這個文件第一行就能夠看到process.binding,這個本質上是加載原生的c++模塊,這個模塊在初始化的時候將其註冊到了一個鏈表中,加載的過程就是將其拿出來而後執行

以上內容若是有錯誤的地方,還請大佬指出,萬分感激,另一件重要的事情就是:我所在團隊也在招人,若是有興趣能夠將簡歷發至zhoupeng.1996@bytedance.com

原文地址:https://segmentfault.com/a/1190000016318567
相關文章
相關標籤/搜索