node的源碼分析還挺多的,不過像我這樣愣頭完全平鋪源碼做解析的貌似還沒有,所以開個先例,從一個API來了解node的調用鏈。
首先上一張整體的圖,網上翻到的,自己懶得畫:
這里的層次結構十分的清晰,從上到下如果翻譯成語言層面,依次是JS、C++、windows(UNIX)的系統API。
最高層也就是我們自己寫的JS代碼,node會首先通過V8引擎進行編譯解析成C++,隨后將其分發給libuv,libuv根據操作系統的類型來分別調用底層的系統API。
下面通過fs.stat這個API來一步步探索整個過程。
JS => require('fs')
這個方法的調用從開發者的角度講,只需要兩行代碼:
const fs = require('fs'); fs.stat(path, [options], callback);
其中第一步,是獲取內置模塊fs,第二步,就是調用對應的方法。
其實兩個可以合一起講了,弄懂了模塊來源,對應的api也就簡單了。
在前面幾章,只是很模糊和淺顯的講了一個注冊內置模塊的過程,其實在node的目錄,有一個本地的JS庫,簡單的處理了參數:
// node/lib/fs.js fs.stat = function(path, callback) { callback = makeStatsCallback(callback); path = getPathFromURL(path); validatePath(path); const req = new FSReqWrap(); req.oncomplete = callback; // const binding = process.binding('fs'); binding.stat(pathModule.toNamespacedPath(path), req); };
這是方法的源碼,需要注意的只有最后一行,通過binding.stat來調用下層的C++代碼,而這個binding是來源於process對象。
在之前內置模塊初探的時候,我提到過一個代碼包裝,就是對於require的JS文件的外層有一個簡單的wrap:
NativeModule.wrapper = [ '(function (exports, require, module, process) {', '\n});' ]; NativeModule.wrap = function(script) { return NativeModule.wrapper[0] + script + NativeModule.wrapper[1]; }; source = NativeModule.wrap(source);
這里的script對應的就是JS文件字符串,實際上最后生成的其實是一個自調用匿名函數。
node => process.binding
隱去了V8引擎編譯JS代碼的過程(主要這一步很惡心,暫時不想講),直接進入C++模塊。
這個方法在內置模塊引入時也提到過,就是GetBinding方法:
static void GetBinding(const FunctionCallbackInfo<Value>& args) { // ... // 找到對應的模塊節點 node_module* mod = get_builtin_module(*module_v); Local<Object> exports; if (mod != nullptr) { // 初始化並返回一個對象 exports = InitModule(env, mod, module); } // ... }
需要關注的代碼只有get_builtin_module和InitModule兩個。
在前面的某一章我講過,node初始化會通過NODE_BUILTIN_MODULES宏將所有內置模塊的相關信息整理成一個鏈表,通過一個靜態指針進行引用。
所以,這里就通過那個指針,找到對應名字的內置模塊,代碼如下:
node_module* get_builtin_module(const char* name) { // modlist_builtin就是那個靜態指針 return FindModule(modlist_builtin, name, NM_F_BUILTIN); } inline struct node_module* FindModule(struct node_module* list,const char* name,int flag) { struct node_module* mp; // 遍歷鏈表找到符合的模塊信息 for (mp = list; mp != nullptr; mp = mp->nm_link) { if (strcmp(mp->nm_modname, name) == 0) break; } // 沒找到的話mp就是nullptr CHECK(mp == nullptr || (mp->nm_flags & flag) != 0); return mp; }
這里傳入的字符串是fs,而每一個模塊信息節點的nm_modname代表模塊名,所以直接進行字符串匹配就行了。
返回后只是第一步,第二步就開始真正的加載了:
static Local<Object> InitModule(Environment* env, node_module* mod, Local<String> module) { // 生成一個新對象作為fs Local<Object> exports = Object::New(env->isolate()); // ... mod->nm_context_register_func(exports, unused, env->context(), mod->nm_priv); return exports; }
這里調用的是模塊內部的一個方法,從名字來看也很直白,即帶有上下文的模塊注冊函數。
在前面生成模塊鏈表的方法,有這么一段注釋:
// This is used to load built-in modules. Instead of using // __attribute__((constructor)), we call the _register_<modname> // function for each built-in modules explicitly in // node::RegisterBuiltinModules(). This is only forward declaration. // The definitions are in each module's implementation when calling // the NODE_BUILTIN_MODULE_CONTEXT_AWARE. #define V(modname) void _register_##modname(); NODE_BUILTIN_MODULES(V) #undef V
從最后面一行可以看出,注冊方法時來源於另外一個宏,如下:
#define NODE_BUILTIN_MODULE_CONTEXT_AWARE(modname, regfunc) \ NODE_MODULE_CONTEXT_AWARE_CPP(modname, regfunc, nullptr, NM_F_BUILTIN)
這個宏會在每一個單獨的模塊C++文件的末尾調用,形式大同小異,以fs模塊為例:
NODE_BUILTIN_MODULE_CONTEXT_AWARE(fs, node::fs::Initialize)
這里的第一個參數fs是模塊名,而第二個是初始化方法,一般來說負責初始化一個對象,然后給對象添加一些方法。
當然,以fs為例,看一下初始化的內容:
void Initialize(Local<Object> target, Local<Value> unused, Local<Context> context, void* priv) { Environment* env = Environment::GetCurrent(context); // ...大量SetMethod env->SetMethod(target, "mkdir", MKDir); env->SetMethod(target, "readdir", ReadDir); env->SetMethod(target, "stat", Stat); env->SetMethod(target, "lstat", LStat); env->SetMethod(target, "fstat", FStat); env->SetMethod(target, "stat", Stat); // ...還有大量代碼 }
可見,初始化就是給傳入的對象設置一些屬性,屬性名就是那些熟悉的api了。
這里只看stat,本地方法對應Stat,簡化后如下:
static void Stat(const FunctionCallbackInfo<Value>& args) { Environment* env = Environment::GetCurrent(args); // 參數檢測 options是可選的 const int argc = args.Length(); CHECK_GE(argc, 2); // 第一個參數必定是路徑 BufferValue path(env->isolate(), args[0]); CHECK_NE(*path, nullptr); // 這玩意不管 FSReqBase* req_wrap_async = GetReqWrap(env, args[1]); if (req_wrap_async != nullptr) { // stat(path, req) // 注意倒數第二個參數!!! AsyncCall(env, req_wrap_async, args, "stat", UTF8, AfterStat, uv_fs_stat, *path); } else { // stat(path, undefined, ctx) // ... // 注意倒數第二個參數!!! int err = SyncCall(env, args[2], &req_wrap_sync, "stat", uv_fs_stat, *path); // ... } } // AsyncCall => AsyncDestCall template <typename Func, typename... Args> inline FSReqBase* AsyncDestCall(/*很多參數*/, Func fn, Args... fn_args) { // ... int err = fn(env->event_loop(), req_wrap->req(), fn_args..., after); // ... } template <typename Func, typename... Args> inline int SyncCall(/*很多參數*/, Func fn, Args... args) { // ... int err = fn(env->event_loop(), &(req_wrap->req), args..., nullptr); // ... }
省略了很多很多(大家都不想看)的代碼,濃縮出了核心的調用,就是uv_fs_stat。
這里的if、else主要是區別同步和異步調用,那個after就是代表有沒有callback,簡單了解下就OK了。
libuv => uv_fs_stat
至此,正式進入第三階段,libuv層級。
這個框架的代碼十分清爽,給你們看一下:
int uv_fs_stat(uv_loop_t* loop, uv_fs_t* req, const char* path, uv_fs_cb cb) { int err; // 初始化一些信息 INIT(UV_FS_STAT); // 處理路徑參數 err = fs__capture_path(req, path, NULL, cb != NULL); if (err) { return uv_translate_sys_error(err); } // 實際操作 POST; }
完全不用省略任何代碼,每一步都很清晰,INIT宏的參數是一個枚舉,該枚舉類包含所有文件操作的枚舉值。
這里首先是初始化stat相關的一些信息,如下:
#define INIT(subtype) \ do { \ if (req == NULL) \ return UV_EINVAL; \ uv_fs_req_init(loop, req, subtype, cb); \ } \ while (0) INLINE static void uv_fs_req_init(uv_loop_t* loop, uv_fs_t* req, uv_fs_type fs_type, const uv_fs_cb cb) { uv__once_init(); UV_REQ_INIT(req, UV_FS); req->loop = loop; req->flags = 0; // 只有這一步是類型相關的 req->fs_type = fs_type; req->result = 0; req->ptr = NULL; req->path = NULL; req->cb = cb; memset(&req->fs, 0, sizeof(req->fs)); }
因為代碼比較簡單直白,所以就懶得省略了。
這里的宏是一個公共宏,所有文件操作相關的調用都要經過這個宏來進行初始化。在參數上,loop(事件輪詢)、req(文件操作的相關對象)、cb(回調函數)都基本上不會變,所以實際上唯一區別操作類型的只有subtype。
第二步是對路徑的處理,我覺得應該不會有人想知道內容是什么。
所以直接進入最后一步,POST。這個框架也真是可以的,所有的文件操作都通過三件套批量處理了。
這個宏如下:
#define POST \ do { \ if (cb != NULL) { \ uv__req_register(loop, req); \ uv__work_submit(loop, &req->work_req, uv__fs_work, uv__fs_done); \ return 0; \ } else { \ uv__fs_work(&req->work_req); \ return req->result; \ } \ } \ while (0)
cb來源於node調用中的最后一個參數,同步情況下傳的是一個Undefined,並不需要一個回調函數。
對於開發者來說同步異步可能只是書寫流程的小變化,但是對於libuv來說卻不太一樣,因為框架本身同時掌控着事件輪詢,在異步情況下,這里的處理需要單獨開一個線程進行處理,隨后通過觀察者模式通知異步調用結束,需要執行回調函數。
另外一個不同點是,同步調用直接返回一個結果,異步調用會包裝結果作為回調函數的參數然后進行調用,通過上面的if、else結構也能看出來。
windowsAPI
這里的處理分同步和異步。
先看同步:
static void uv__fs_work(struct uv__work* w) { uv_fs_t* req; // ... #define XX(uc, lc) case UV_FS_##uc: fs__##lc(req); break; // 枚舉值為UV_FS_STAT switch (req->fs_type) { // ... XX(CLOSE, close) XX(READ, read) XX(WRITE, write) XX(FSTAT, fstat) // ... default: assert(!"bad uv_fs_type"); } }
這個地方,上面的那個枚舉值終於起了作用,省略了一些無關代碼,最終的結果通過宏,指向了一個叫fs__fstat函數。
static void fs__fstat(uv_fs_t* req) { int fd = req->file.fd; HANDLE handle; VERIFY_FD(fd, req); // 保證可以獲取到對應的文件句柄 handle = uv__get_osfhandle(fd); // 錯誤處理 if (handle == INVALID_HANDLE_VALUE) { SET_REQ_WIN32_ERROR(req, ERROR_INVALID_HANDLE); return; } // 這里進行變量賦值 if (fs__stat_handle(handle, &req->statbuf, 0) != 0) { SET_REQ_WIN32_ERROR(req, GetLastError()); return; } req->ptr = &req->statbuf; // 返回0 req->result = 0; }
這里有兩個方法需要注意:
1、uv__get_osfhandle 獲取文件句柄
2、fs__stat_handle 獲取文件信息
源碼如下:
INLINE static HANDLE uv__get_osfhandle(int fd) { HANDLE handle; UV_BEGIN_DISABLE_CRT_ASSERT(); // windowsAPI 根據文件描述符獲取文件句柄 handle = (HANDLE) _get_osfhandle(fd); UV_END_DISABLE_CRT_ASSERT(); return handle; } INLINE static int fs__stat_handle(HANDLE handle, uv_stat_t* statbuf, int do_lstat) { // ... // windowsAPI nt_status = pNtQueryInformationFile(handle, &io_status, &file_info, sizeof file_info, FileAllInformation); /* Buffer overflow (a warning status code) is expected here. */ if (NT_ERROR(nt_status)) { SetLastError(pRtlNtStatusToDosError(nt_status)); return -1; } // windowsAPI nt_status = pNtQueryVolumeInformationFile(handle, &io_status, &volume_info, sizeof volume_info, FileFsVolumeInformation); // ...文件信息對象的處理 }
可以看出,最后的底層調用了windows的API來獲取對應的文件句柄,然后繼續獲取對應句柄的文件信息,將信息處理后弄到req->ptr上,而node中對於同步處理的結果代碼如下:
Local<Value> arr = node::FillGlobalStatsArray(env, static_cast<const uv_stat_t*>(req_wrap_sync.req.ptr)); args.GetReturnValue().Set(arr);
這里的req_wrap_sync.req.ptr就是上面通過windowAPI獲取到的文件信息內容。
異步情況如下:
void uv__work_submit(uv_loop_t* loop, struct uv__work* w, void (*work)(struct uv__work* w), void (*done)(struct uv__work* w, int status)) { uv_once(&once, init_once); w->loop = loop; w->work = work; w->done = done; post(&w->wq); }
先看那個奇怪的post:
static void post(QUEUE* q) { // 上鎖 uv_mutex_lock(&mutex); // 關於QUEUE的分析可見https://www.jianshu.com/p/6373de1e117d // 知道是個隊列就行了 QUEUE_INSERT_TAIL(&wq, q); if (idle_threads > 0) uv_cond_signal(&cond); // 解鎖 uv_mutex_unlock(&mutex); }
由於異步涉及到事件輪詢,所以代碼實質上要稍微復雜一點,但是總體來說並不需要關心那么多。
這里有一個空閑線程的判斷,不管,直接看那個處理方法:
void uv_cond_signal(uv_cond_t* cond) { if (HAVE_CONDVAR_API()) uv_cond_condvar_signal(cond); else // 初始化一個狀態變量防止線程的競爭情況 // 反正也是個windowsAPI uv_cond_fallback_signal(cond); } static void uv_cond_condvar_signal(uv_cond_t* cond) { // windowsAPI pWakeConditionVariable(&cond->cond_var); }
你會發現,這只是防止線程競態而需要生成一個狀態變量。
其實這個地方已經涉及到libuv中事件輪詢的控制了,每次loop會從handle中取一個req,然后執行work,然后通知node完成,可以執行回調函數done了。
暫時不需要知道那么多,在uv__work_submit方法中,對應的賦值是這4個參數:
uv__work_submit(loop, &req->work_req, uv__fs_work, uv__fs_done);
其中第三個參數就是剛才同步獲取文件信息的方法,而第四個就是在獲取完畢會回調函數的調用:
static void uv__fs_done(struct uv__work* w, int status) { uv_fs_t* req; req = container_of(w, uv_fs_t, work_req); uv__req_unregister(req->loop, req); if (status == UV_ECANCELED) { assert(req->result == 0); req->result = UV_ECANCELED; } // 執行回調 req->cb(req); }
異步調用因為在回調函數帶了結果,所以返回值不能跟同步一樣,最后的處理有些許不一樣:
template <typename Func, typename... Args> inline FSReqBase* AsyncDestCall(/*很多參數*/) { // ... if (err < 0) { // ... } else { req_wrap->SetReturnValue(args); } // 返回另外的值 return req_wrap; } void FSReqWrap::SetReturnValue(const FunctionCallbackInfo<Value>& args) { // 設成undefined args.GetReturnValue().SetUndefined(); }
簡單講,fs.statSync返回一個Stat對象,而fs.stat返回undefined。這個可以很簡單的測試得到結果,我這里就不貼圖了,已經夠長了。