概述
相信很多的人,每天在終端不止一遍的執行着
node
這條指令,對于很多人來說,它就像一個黑盒,并不知道背後到底發生了什麼,本文将會為大家揭開這個神秘的面紗,由于本人水準有限,是以隻是講一個大概其,主要關注的過程就是
node
子產品的初始化,
event loop
和
v8
的部分基本沒有深入,這些部分可以關注一下我以後的文章。(提示本文非常的長,希望大家不要看煩~)
node是什麼?
這個問題很多人都會回答就是
v8
+
libuv
,但是除了這個兩個庫以外
node
還依賴許多優秀的開源庫,可以通過
process.versions
來看一下:
-
主要用于解析http資料包的子產品,在這個庫的作者也是http_parser
,一個純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(&_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<struct node_module*>(m);
if (mp->nm_flags & NM_F_BUILTIN) {
mp->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<ArrayBufferAllocator, decltype(&FreeArrayBufferAllocator)>
allocator(CreateArrayBufferAllocator(), &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<Context> 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->SetClassName(FIXED_ONE_BYTE_STRING(isolate(), "process"));
auto process_object =
process_template->GetFunction()->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<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);
先将
lib/internal/bootstrap
檔案夾下的兩個檔案讀進來,然後利用
GetBootstrapper
來執行
js
代碼分别得到了一個函數,一步步來看,先看看
GetBootstrapper
為什麼可以執行
js
代碼,檢視這個函數可以發現主要是因為
ExecuteString
MaybeLocal<v8::Script> script =
v8::Script::Compile(env->context(), source, &origin);
...
MaybeLocal<Value> result = script.ToLocalChecked()->Run(env->context());
這個主要利用了
v8
的能力,對
js
檔案進行了解析和執行,打開
loaders.js
看看其參數,需要五個,撿兩個最重要的來說,分别是
process
getBinding
,這裡面往後繼續看
LoadEnvironment
發現
process
對象就是剛剛生成的,而
getBinding
是函數
GetBinding
node_module* mod = get_builtin_module(*module_v);
Local<Object> exports;
if (mod != nullptr) {
exports = InitModule(env, mod, module);
} else if (!strcmp(*module_v, "constants")) {
exports = Object::New(env->isolate());
CHECK(exports->SetPrototype(env->context(),
Null(env->isolate())).FromJust());
DefineConstants(env->isolate(), exports);
} else if (!strcmp(*module_v, "natives")) { // NativeModule _source
exports = Object::New(env->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->nm_link) {
if (strcmp(mp->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->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 && (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] && 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