天天看點

nodejs源碼—初始化

概述

相信很多的人,每天在終端不止一遍的執行着

node

這條指令,對于很多人來說,它就像一個黑盒,并不知道背後到底發生了什麼,本文将會為大家揭開這個神秘的面紗,由于本人水準有限,是以隻是講一個大概其,主要關注的過程就是

node

子產品的初始化,

event loop

v8

的部分基本沒有深入,這些部分可以關注一下我以後的文章。(提示本文非常的長,希望大家不要看煩~)

node是什麼?

這個問題很多人都會回答就是

v8

+

libuv

,但是除了這個兩個庫以外

node

還依賴許多優秀的開源庫,可以通過

process.versions

來看一下:

  • http_parser

    主要用于解析http資料包的子產品,在這個庫的作者也是

    ry

    ,一個純

    c

    的庫,無任何依賴
  • v8

    這個大家就非常熟悉了,一個優秀的

    js

    引擎
  • uv

    這個就是

    ry

    實作的

    libuv

    ,其封裝了

    libev

    IOCP

    ,實作了跨平台,

    node

    中的

    i/o

    就是它,盡管

    js

    是單線程的,但是

    libuv

    并不是,其有一個線程池來處理這些

    i/o

    操作。
  • zlib

    主要來處理壓縮操作,諸如熟悉的

    gzip

    操作
  • ares

    c-ares

    ,這個庫主要用于解析

    dns

    ,其也是異步的
  • modules

    就是

    node

    的子產品系統,其遵循的規範為

    commonjs

    ,不過

    node

    也支援了

    ES

    子產品,不過需要加上參數并且檔案名字尾需要為

    mjs

    ,通過源碼看,

    node

    ES

    子產品的名稱作為了一種

    url

    來看待,具體可以參見 這裡
  • nghttp2

    如其名字一樣,是一個

    http2

    的庫
  • napi

    是在

    node8

    出現,

    node10

    穩定下來的,可以給編寫

    node

    原生子產品更好的體驗(終于不用在依賴于

    nan

    ,每次更換

    node

    版本還要重新編譯一次了)
  • openssl

    非常著名的庫,

    tls

    子產品依賴于這個庫,當然還包括

    https

  • icu

    small-icu

    ,主要用于解決跨平台的編碼問題,

    versions

    對象中的

    unicode

    cldr

    tz

    也源自

    icu

    ,這個的定義可以參見

從這裡可以看出的是

process

對象在

node

中非常的重要,個人的了解,其實

node

與浏覽器端最主要的差別,就在于這個

process

對象

注:

node

隻是用

v8

來進行

js

的解析,是以不一定非要依賴

v8

,也可以用其他的引擎來代替,比如利用微軟的

ChakraCore

,對應的

node倉庫

node初始化

經過上面的一通分析,對

node

的所有依賴有了一定的了解,下面來進入正題,看一下

node

的初始化過程:

挖坑

node_main.cc

為入口檔案,可以看到的是除了調用了

node::Start

之外,還做了兩件事情:

NODE_SHARED_MODE忽略SIGPIPE信号

SIGPIPE

信号出現的情況一般在

socket

收到

RST packet

之後,扔向這個

socket

寫資料時産生,簡單來說就是

client

server

發請求,但是這時候

client

已經挂掉,這時候就會産生

SIGPIPE

信号,産生這個信号會使

server

端挂掉,其實

node::PlatformInit

中也做了這種操作,不過隻是針對

non-shared lib build

改變緩沖行為

stdout

的預設緩沖行為為

_IOLBF

(行緩沖),但是對于這種來說互動性會非常的差,是以将其改為

_IONBF

(不緩沖)

探索

node.cc

檔案中總共有三個

Start

函數,先從

node_main.cc

中掉的這個

Start

函數開始看:

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);
}
           

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

PlatformInit

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時發出)和

SIGTERM

SIGTERM

SIGKILL

類似,但是不同的是該信号可以被阻塞和處理,要求程式自己退出)信号來做一些特殊處理,這個處理與正常退出時一樣;另一個重要的事情就是下面這段代碼:

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 limit

unlimited

,是以我認為

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

為止,還沒有涉及的

js

c++

的互動,在将一些環境初始化之後,就要開始用

v8

這個大殺器了,

v8_platform

是一個結構體,可以了解為是

node

對于

v8

v8::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);
  }
}
           

首先建立了一個

v8

Isolate

(隔離),隔離在

v8

中非常常見,仿佛和程序一樣,不同隔離不共享資源,有着自己得堆棧,但是正是因為這個原因在多線程的情況下,要是對每一個線程都建立一個隔離的話,那麼開銷會非常的大(可喜可賀的是

node

有了

worker_threads

),這時候可以借助

Locker

來進行同步,同時也保證了一個

Isolate

同一時刻隻能被一個線程使用;下面兩行就是

v8

的正常套路,下一步一般就是建立一個

Context

(最簡化的一個流程可以參見

v8

hello 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

是用來管理這個

Context

Environment

可以了解為一個

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

看看其參數,需要五個,撿兩個最重要的來說,分别是

process

getBinding

,這裡面往後繼續看

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);
           

其作用就是根據傳參來初始化指定的子產品,當然也有比較特殊的兩個分别是

constants

natives

(後面再看),

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

中對應的

class

Module

,相信這個檔案大家很多人都了解,與

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 b

parent

Module a

,利用

resolveFilename

可以得到檔案具體的位置,這個過程而後調用

load

函數來加載檔案,可以看到的是區分了幾種類型,分别是

.js .json .node

.js

是讀檔案然後執行,

.json

是直接讀檔案後

JSON.parse

一下,

.node

是調用

dlopen

Module.compile

NativeModule.compile

相類似都是想包裹一層成為函數,然後調用了

vm

編譯得到這個函數,最後傳入參數來執行,對于

Module

來說,包裹的代碼如下:

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

執行完上述過程後,前期工作就已經做得比較充分了,再次回到最後一個

Start

函數來看,從代碼中可以看到開始了

node

event loop

,這就是

node

的初始化過程,關于

event loop

需要對

libuv

有一定的了解,可以說

node

真正離不開的是

libuv

,具體這方面的東西,可以繼續關注我後面的文章

總結

總結一下這個過程,以首次加載沒有任何緩存的情況開看:

require('fs')

,先是調用了

Module.require

,而後發現為原生子產品,于是調用

NativeModule.require

,從

NativeModule._source

lib/fs

的内容拿出來包裹一下然後執行,這個檔案第一行就可以看到

process.binding

,這個本質上是加載原生的

c++

子產品,這個子產品在初始化的時候将其注冊到了一個連結清單中,加載的過程就是将其拿出來然後執行

以上内容如果有錯誤的地方,還請大佬指出,萬分感激,另外一件重要的事情就是:我所在團隊也在招人,如果有興趣可以将履歷發至[email protected]

原文位址:https://segmentfault.com/a/1190000016318567

繼續閱讀