深入出不來nodejs源碼-從fs.stat方法來看node架構


  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。這個可以很簡單的測試得到結果,我這里就不貼圖了,已經夠長了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM