jsernews 1.0.1

<~>

ts 164 days ago.

原文链接:https://blog.safia.rocks/post/169466425525/node-module-deep-dive-fs

是时候深入另一个 Node 模块了!

我得到了一些很好的反馈,人们期待在这一系列代码解析中,深入 Node 代码库的 C++ 部分。我同意。说实话,我之前避免了这一部分,主要是因为对我自己的 C++ 知识以及系统级软件的理解的不安全感。但是你知道吗,我将所有这些放在一边,并深入 Node 代码库的 C++ 部分,因为我是一位勇敢而无畏的开发人员。

我这样说是为了澄清,不要把我说的任何东西视为绝对事实视,如果你对我误解的部分代码有所了解,在 Twitter 上告诉我。

无论如何,让我们来看看有趣的东西。

我一直在思考 fs 模块。 fs 模块是 Node 标准库的一部分,它允许开发人员与文件系统进行交互。您可以执行诸如读取文件,写入文件以及检查文件状态等。如果您正在使用 JavaScript 构建桌面应用程序或与后端服务器中的文件进行交互,这非常方便。

我使用最多的 fs 函数之一是 exists 函数,它检查文件是否存在。实际上这个函数最近已被弃用,更支持使用 fs.statfs.access。因此,我认为深入了解 fs.access 如何在 Node 中工作会很有趣。这里是你如何在应用程序中使用 fs.access

> const fs = require('fs');
undefined
> fs.access('/etc/passwd', (error) => error ? console.log('This file does not exist.') : console.log('This file exists.'));
undefined
> This file exists.

整洁-O!所以我们可以传递文件名和携带一个 error 参数的回调。如果 error 存在,我们不能访问该文件,但如果它不存在,那么我们可以访问。 那么让我们来看一下代码库中的 fs 模块,看看发生了什么。fs.access 函数的代码如下所示。

fs.access = function(path, mode, callback) {
  if (typeof mode === 'function') {
    callback = mode;
    mode = fs.F_OK;
  } else if (typeof callback !== 'function') {
    throw new errors.TypeError('ERR_INVALID_CALLBACK');
  }

  if (handleError((path = getPathFromURL(path)), callback))
    return;

  if (typeof path !== 'string' && !(path instanceof Buffer)) {
    throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'path',
                               ['string', 'Buffer', 'URL']);
  }

  if (!nullCheck(path, callback))
    return;

  mode = mode | 0;
  var req = new FSReqWrap();
  req.oncomplete = makeCallback(callback);
  binding.access(pathModule.toNamespacedPath(path), mode, req);
};

就像我之前提到的那样,它需要一个路径和一个回调函数。它还需要一个模式参数,您可以在这里阅读更多信息。函数的前几行大部分是标准验证和安全检查。我会避免在这里解释它们,因为我认为它们是不言自明的。

一旦我们到达函数的最后几行,代码就会变得更加有趣。

var req = new FSReqWrap();
req.oncomplete = makeCallback(callback);
binding.access(pathModule.toNamespacedPath(path), mode, req);

我从来没有见过这个 FSReqWrap 对象。我假设它是 Node 文件系统中用于处理异步请求的一些底层 API。我试图找出这个对象是在哪里定义的。它的 require 语句看起来像这样。

const { FSReqWrap } = binding;

所以它看起来像是从 binding 中提取 FSReqWrap 对象。但是什么是 binding

const binding = process.binding('fs');

嗯。所以它似乎是使用 fs 参数调用 process.binding 的结果。我看到这些 process.binding 调用遍布在代码库当中,但很大程度上避免去挖掘它们。但今天不是!一个快速的 Google 搜索到了这个 StackOverflow 问题,这证实了我的怀疑,即 process.binding 是 C++ 代码如何暴露给代码库的 JavaScript 部分。所以我挖掘了 Node 代码库,试图找到 fs 的 C/C++ 代码驻留在哪里。我发现 fs 实际上有两个不同的 C 源文件,一个与 Unix 关联,另一个与 Windows 关联。

所以我试着去查看关于 Unix 的 fs C 源文件中是否有类似 access 函数的定义。单词 access 在代码中被引用四次。

这里两次。

#define X(type, action)                                                       \
  case UV_FS_ ## type:                                                        \
    r = action;                                                               \
    break;

    switch (req->fs_type) {
    X(ACCESS, access(req->path, req->flags));

这里两次。

int uv_fs_access(uv_loop_t* loop,
                 uv_fs_t* req,
                 const char* path,
                 int flags,
                 uv_fs_cb cb) {
  INIT(ACCESS);
  PATH;
  req->flags = flags;
  POST;
}

现在你知道我对整个“代码库的 C 这一部分让我紧张”的意思。

我感觉 uv_fs_access 更容易研究一些。 我不知道这个 X 函数宏定义是怎么回事,我也不认为自己处于一种禅宗般的状态中去解决问题。

好吧!所以 uv_fs_access 函数似乎是将 ACCESS 常量传递给 INIT 函数宏,看起来有点像这样。

#define INIT(subtype)                                                         \
  do {                                                                        \
    if (req == NULL)                                                          \
      return -EINVAL;                                                         \
    req->type = UV_FS;                                                        \
    if (cb != NULL)                                                           \
      uv__req_init(loop, req, UV_FS);                                         \
    req->fs_type = UV_FS_ ## subtype;                                         \
    req->result = 0;                                                          \
    req->ptr = NULL;                                                          \
    req->loop = loop;                                                         \
    req->path = NULL;                                                         \
    req->new_path = NULL;                                                     \
    req->cb = cb;                                                             \
  }                                                                           \
  while (0)

所以 INIT 函数宏似乎正在初始化一些 req 结构中的字段。通过查看以 req 为参数的函数的参数类型声明,我认为 req 是一个指向 uv_fs_t Object的指针。我发现一些文档比较简洁地表明 uv_fs_t 是文件系统请求类型。我想这就是我需要知道的一切!

注:为什么这段代码写在 do {} while(0) 而不是一系列函数调用中。有谁知道这可能是什么原因?

后期补充:我做了一些 Google 搜索,并找到了回答此问题的 StackOverflow 帖子

好的。所以一旦这个文件系统请求对象被初始化,access 函数就会调用 PATH 宏来执行以下操作。

#define PATH                                                                  \
  do {                                                                        \
    assert(path != NULL);                                                     \
    if (cb == NULL) {                                                         \
      req->path = path;                                                       \
    } else {                                                                  \
      req->path = uv__strdup(path);                                           \
      if (req->path == NULL) {                                                \
        uv__req_unregister(loop, req);                                        \
        return -ENOMEM;                                                       \
      }                                                                       \
    }                                                                         \
  }                                                                           \
  while (0)

嗯。所以这似乎在检查文件系统请求的路径是否为一个有效的路径。如果路径无效,它似乎取消注册与请求相关的循环。我不明白这段代码的很多细节,但是我的直觉是它对文件系统请求进行了验证。

uv_fs_access 下一个调用是 POST 宏,它具有与其关联的以下代码。

#define POST                                                                  \
  do {                                                                        \
    if (cb != NULL) {                                                         \
      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)

所以看起来像 POST 宏实际上调用了与文件系统请求相关的操作循环,然后执行适当的回调。

此时,我开始有点迷失了。我碰巧与同行代码阅读爱好者 Julia Evans 一起出席 StarCon。我们坐在一起,讨论 uv__fs_work 函数中的一些代码,看起来像这样。

static void uv__fs_work(struct uv__work* w) {
  int retry_on_eintr;
  uv_fs_t* req;
  ssize_t r;

  req = container_of(w, uv_fs_t, work_req);
  retry_on_eintr = !(req->fs_type == UV_FS_CLOSE);

  do {
    errno = 0;

#define X(type, action)                                                       \
  case UV_FS_ ## type:                                                        \
    r = action;                                                               \
    break;

    switch (req->fs_type) {
    X(ACCESS, access(req->path, req->flags));
    X(CHMOD, chmod(req->path, req->mode));
    X(CHOWN, chown(req->path, req->uid, req->gid));
    X(CLOSE, close(req->file));
    X(COPYFILE, uv__fs_copyfile(req));
    X(FCHMOD, fchmod(req->file, req->mode));
    X(FCHOWN, fchown(req->file, req->uid, req->gid));
    X(FDATASYNC, uv__fs_fdatasync(req));
    X(FSTAT, uv__fs_fstat(req->file, &req->statbuf));
    X(FSYNC, uv__fs_fsync(req));
    X(FTRUNCATE, ftruncate(req->file, req->off));
    X(FUTIME, uv__fs_futime(req));
    X(LSTAT, uv__fs_lstat(req->path, &req->statbuf));
    X(LINK, link(req->path, req->new_path));
    X(MKDIR, mkdir(req->path, req->mode));
    X(MKDTEMP, uv__fs_mkdtemp(req));
    X(OPEN, uv__fs_open(req));
    X(READ, uv__fs_buf_iter(req, uv__fs_read));
    X(SCANDIR, uv__fs_scandir(req));
    X(READLINK, uv__fs_readlink(req));
    X(REALPATH, uv__fs_realpath(req));
    X(RENAME, rename(req->path, req->new_path));
    X(RMDIR, rmdir(req->path));
    X(SENDFILE, uv__fs_sendfile(req));
    X(STAT, uv__fs_stat(req->path, &req->statbuf));
    X(SYMLINK, symlink(req->path, req->new_path));
    X(UNLINK, unlink(req->path));
    X(UTIME, uv__fs_utime(req));
    X(WRITE, uv__fs_buf_iter(req, uv__fs_write));
    default: abort();
    }
#undef X
  } while (r == -1 && errno == EINTR && retry_on_eintr);

  if (r == -1)
    req->result = -errno;
  else
    req->result = r;

  if (r == 0 && (req->fs_type == UV_FS_STAT ||
                 req->fs_type == UV_FS_FSTAT ||
                 req->fs_type == UV_FS_LSTAT)) {
    req->ptr = &req->statbuf;
  }
}

好!我知道这看起来有点可怕。相信我,当我第一次看到它的时候也让我害怕。 Julia和我意识到的其中一件事是代码的这一点。

#define X(type, action)                                                       \
  case UV_FS_ ## type:                                                        \
    r = action;                                                               \
    break;

    switch (req->fs_type) {
    X(ACCESS, access(req->path, req->flags));
    X(CHMOD, chmod(req->path, req->mode));
    X(CHOWN, chown(req->path, req->uid, req->gid));
    X(CLOSE, close(req->file));
    X(COPYFILE, uv__fs_copyfile(req));
    X(FCHMOD, fchmod(req->file, req->mode));
    X(FCHOWN, fchown(req->file, req->uid, req->gid));
    X(FDATASYNC, uv__fs_fdatasync(req));
    X(FSTAT, uv__fs_fstat(req->file, &req->statbuf));
    X(FSYNC, uv__fs_fsync(req));
    X(FTRUNCATE, ftruncate(req->file, req->off));
    X(FUTIME, uv__fs_futime(req));
    X(LSTAT, uv__fs_lstat(req->path, &req->statbuf));
    X(LINK, link(req->path, req->new_path));
    X(MKDIR, mkdir(req->path, req->mode));
    X(MKDTEMP, uv__fs_mkdtemp(req));
    X(OPEN, uv__fs_open(req));
    X(READ, uv__fs_buf_iter(req, uv__fs_read));
    X(SCANDIR, uv__fs_scandir(req));
    X(READLINK, uv__fs_readlink(req));
    X(REALPATH, uv__fs_realpath(req));
    X(RENAME, rename(req->path, req->new_path));
    X(RMDIR, rmdir(req->path));
    X(SENDFILE, uv__fs_sendfile(req));
    X(STAT, uv__fs_stat(req->path, &req->statbuf));
    X(SYMLINK, symlink(req->path, req->new_path));
    X(UNLINK, unlink(req->path));
    X(UTIME, uv__fs_utime(req));
    X(WRITE, uv__fs_buf_iter(req, uv__fs_write));
    default: abort();
    }
#undef X

实际上是一个巨大的 switch 语句。看起来神秘的 X 宏定义实际上是 case 语句的语法糖,看起来像这样。

 case UV_FS_ ## type:                                                        \
    r = action;                                                               \
    break;

因此,例如这个宏函数调用 X(ACCESS,access(req->path,req ->flags)) 实际上与以下扩展的 case 语句相对应。

case UV_FS_ACCESS:
    r = access(req->path, req->flags)
    break;

所以我们的 case 语句实质上最终调用 access 函数并将其响应设置为 raccess 又是什么?Julia 帮助我认识到 access 是在 unistd.h 中定义的系统库的一部分。所以这就是 Node 实际与系统特定的 API 进行交互的地方。

一旦它将结果存储在 r 中,函数就会执行下面的代码。

if (r == -1)
  req->result = -errno;
else
  req->result = r;

if (r == 0 && (req->fs_type == UV_FS_STAT ||
               req->fs_type == UV_FS_FSTAT ||
               req->fs_type == UV_FS_LSTAT)) {
  req->ptr = &req->statbuf;
}

那么基本上这样做是检查从调用系统的特定 APIS 得到的结果是否有效,并将其存储回前面提到的文件系统请求对象中。有意思!

这就是这个代码读取。我阅读了代码库的 C 部分,得益于 Julia 的帮助。如果您有任何疑问或想澄清我可能误解的内容,请告诉我。