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