63.1 验证单个进程能使用的最大文件描述符个数

实验设计

以下实验都是在 wsl 中进行的。

$ cat /proc/sys/fs/file-max   # 整个系统中能使用的最大文件描述数量
9223372036854775807
$ cat /proc/sys/fs/nr_open    # 单个进程中能使用的最大文件描述符个数
1048576

编译运行以下测试代码(不要用 Release 模式、不要增加 -DNDEBUG;这份代码还假设了 0、1、2 号文件描述符已经被打开):

#include <cassert>
#include <cerrno>
#include <cstdio>
#include <cstdlib>
#include <fcntl.h>
#include <sys/resource.h>
#include <unordered_set>
#include <vector>

int main() {
  // check rlimit
  int ret;
  struct rlimit rlimit_res;
  if ((ret = getrlimit(RLIMIT_NOFILE, &rlimit_res)) == -1) {
    perror("getrlimit");
    exit(EXIT_FAILURE);
  }
  printf("soft limit of fd count: %lu\n", (unsigned long)rlimit_res.rlim_cur);
  printf("hard limit of fd count: %lu\n", (unsigned long)rlimit_res.rlim_max);

  // try to open as many files as we can
  std::vector<int> opened_files{0, 1, 2};
  while (true) {
    int fd = open("/dev/zero", O_RDONLY);
    if (fd == -1) {
      if (errno == EMFILE) {
        break;
      } else {
        perror("open");
        exit(EXIT_FAILURE);
      }
    }
    opened_files.push_back(fd);
  }
  // check if sorted
  for (size_t i = 1; i < opened_files.size(); ++i) {
    assert(opened_files[i - 1] < opened_files[i]);
  }
  // find missing fds
  {
    int maxfd = opened_files.back();
    std::unordered_set<int> st(opened_files.begin(), opened_files.end());
    for (int i = 0; i < maxfd; ++i) {
      if (!st.count(i)) {
        printf("missing fd %d\n", i);
      }
    }
  }
  printf("fd count: %zd\n", opened_files.size());
}

实验结果

直接通过 shell 进入 WSL:Debian 时,运行结果如下:

$ ./build/main
soft limit of fd count: 1024
hard limit of fd count: 1048576
fd count: 1024

有意思的是,通过 VS Code 连接到 WSL:Debian 时,运行结果如下:

$ ./build/main
soft limit of fd count: 1048576
hard limit of fd count: 1048576
missing fd 19
missing fd 20
missing fd 21
missing fd 22
missing fd 23
fd count: 1048571

我的 VS Code 版本:

Version: 1.92.2 (user setup)
Commit: fee1edb8d6d72a0ddff41e5f71a671c23ed924b9
Date: 2024-08-14T17:29:30.058Z
Electron: 30.1.2
ElectronBuildId: 9870757
Chromium: 124.0.6367.243
Node.js: 20.14.0
V8: 12.4.254.20-electron.0
OS: Windows_NT x64 10.0.22631

猜测是 VS Code 根据自己的用途提高了软限制(软限制是可以在 0 和硬限制之间做调整的,而非特权进程只能降低硬限制、不能提高硬限制),并且还占用了一些 fd。

VS Code 连接 WSL 后终端的文件描述符会被部分占用

每多打开一个终端,在终端运行的程序可用的文件描述符的数量就减少 1,而且被占用的文件描述符在终端启动之后就固定了,不再变化。即使其他终端被关闭,已启动的终端中被占用的文件描述符也不会被释放回来,这一点通过再次运行程序可以验证出来。但是其他终端被关闭会使得新打开的终端中被 VS Code 占用的文件描述符被释放一部分。

这个 fd 被占用的现象很像是在 fork() 出终端时就决定的。

只打开一个终端的输出:

soft limit of fd count: 1048576
hard limit of fd count: 1048576
missing fd 19
missing fd 20
missing fd 21
missing fd 22
fd count: 1048572

打开两个终端,第二个终端的输出:

soft limit of fd count: 1048576
hard limit of fd count: 1048576
missing fd 19
missing fd 20
missing fd 21
missing fd 22
missing fd 23
fd count: 1048571

打开三个终端,第三个终端的输出:

soft limit of fd count: 1048576
hard limit of fd count: 1048576
missing fd 19
missing fd 20
missing fd 21
missing fd 22
missing fd 23
missing fd 24
fd count: 1048570

这些被占用的 fd 到底是什么呢

修改以上程序检查缺失 fd 的代码如下,主要就是每次除了打印之外,再将 fd 记录到列表中:

  // find missing fds
  std::vector<int> missing_fds;
  {
    int maxfd = opened_files.back();
    std::unordered_set<int> st(opened_files.begin(), opened_files.end());
    for (int i = 0; i < maxfd; ++i) {
      if (!st.count(i)) {
        printf("missing fd %d\n", i);
        missing_fds.push_back(i);
      }
    }
  }

在 main() 函数的末尾加上以下代码,这段代码让程序在退出之前暂停,我们就根据提示在另外一个终端中运行命令:

  if (!missing_fds.empty()) {
    printf("You can run the following commands:\n");
    printf("======================\n");
    long pid = getpid();
    bool first = true;
    for (int fd : missing_fds) {
      if (first) {
        printf("file /proc/%ld/fd/%d", pid, fd);
        first = false;
      } else {
        printf(" \\\n     /proc/%ld/fd/%d", pid, fd);
      }
    }
    printf("\n");
    printf("======================\n");
    printf("<Press enter to exit>\n");
    getchar();
  }

示例输出:

soft limit of fd count: 1048576
hard limit of fd count: 1048576
missing fd 19
missing fd 20
missing fd 21
missing fd 22
missing fd 23
missing fd 24
missing fd 25
fd count: 1048569
You can run the following commands:
======================
file /proc/27624/fd/19 \
     /proc/27624/fd/20 \
     /proc/27624/fd/21 \
     /proc/27624/fd/22 \
     /proc/27624/fd/23 \
     /proc/27624/fd/24 \
     /proc/27624/fd/25
======================
<Press enter to exit>

在另外一个终端运行给出的命令,结果为:

/proc/23853/fd/19: symbolic link to /dev/urandom
/proc/23853/fd/20: symbolic link to /home/USER/.vscode-server/data/logs/20240901T131205/ptyhost.log
/proc/23853/fd/21: symbolic link to /home/USER/.vscode-server/data/logs/20240901T131205/remoteagent.log
/proc/23853/fd/22: symbolic link to /home/USER/.vscode-server/data/logs/20240901T131205/network.log
/proc/23853/fd/23: symbolic link to /dev/ptmx
/proc/23853/fd/24: symbolic link to /dev/ptmx
/proc/23853/fd/25: symbolic link to /dev/ptmx

每次打开一个新的终端,多占用的文件描述符都是指向 /dev/ptmx 的打开项的,第一个终端除了三个日志之外,只有 /dev/urandom 文件。第二个终端开始新增的都是 /dev/ptmx 文件。

在 ssh 连接的远程服务器中也有类似的现象,只是没有涉及 /dev/urandom 文件,而且每次新终端都新增了 /dev/pts/ptmx 文件(包括第一个)。根据 https://askubuntu.com/a/1501918/ ,/dev/ptmx 就像是 /dev/pts/ptmx 的符号链接(实际上它们都是字符设备),但是前者大家都能读写,用户为 root,组为 tty;后者大家都不能读写,用户和组都是 tty。

按照我的理解,/dev/ptmx 是伪终端主设备的名称,shell 使用的应该是伪终端从设备(0、1、2 号文件描述符),不需要主设备?在 https://github.com/microsoft/vscode/issues/182212 中也有人反馈 macOS 上 VS Code 打开虚拟终端占用了大量文件描述符的问题。提了个新的 issue: https://github.com/microsoft/vscode/issues/227299

Important

为什么不直接在发现 fd 的时候就第一时间调用 system() 运行 file 命令,而要在另外一个终端运行命令呢?因为这个时候进程的文件描述符都被占满了,不能再打开新文件了,system() 会报以下错误:

sh: error while loading shared libraries: libc.so.6: cannot open shared object file: Error 24