在之前遇到过一个 Node.js 使用文件描述符来读取文件,未释放文件描述符的坑,对此还对 Node.js 提过 PR ,让 Node.js 支持文件句柄的变量 GC 后,也同时销毁句柄,在 fs 的 FileHandleAPI 提供了未使用变量时进行尝试关闭文件描述符并提供警告,但是很多人并没有使用 FileHandleAPI ,而是更习惯于使用早期的 File System 的 API ,而除了显式的未释放的文件描述符,还有隐式的文件描述符。下面我则根据两个案例来进行讲解关于文件描述符的坑。
显式的文件描述符,我们通常会使用 fs.open() 方法来打开文件,然后通过 fs.read() 方法来读取文件。如下代码所示。
const fs = require("fs");
fs.open("test.txt", "r", (err, fd) => {
// 做一些操作
});
setInterval(() => {
// 一直循环
}, 1000);
在这里其实有两个误区:
针对误区 1 ,因为大多数人在没有完整阅读 Node.js 文档,由于 Node.js 又存在回收机制,所以很多人会认为 fd 变量被回收了,那么 fd 对应的文件描述符也会被自动回收。
针对误区 2 ,这个误区则是没有 C 语言基础的同学容易犯的错误,而是更习惯于 JavaScript 的用法,更少的考虑回收问题,所以不会去 close 。
针对上面的误区,如果没有 close 的情况,什么时候文件描述符会被回收呢?在这个情况下,只有 Node.js 进程销毁的时候才会进行文件描述符的回收。
对于显式的文件描述符,隐式的文件描述符更具有欺骗性,那么什么叫隐式的文件描述符呢?简单来说就是没有直接强制我们传递文件描述符或直接使用文件描述符却又使用到了文件描述符的场景,比如下面的代码。
const fs = require("fs");
// 创建一个文件读流
fs.createReadStream("test.txt");
setInterval(() => {
// 一直循环
}, 1000);
在这里由于没有像显式使用文件描述符,而是将文件描述符作为可选变量,像这种隐式使用文件描述符的方法更具有欺骗性。
在上面的案例中,相信很多人都有类似的使用可读流的用法并且没有关闭可读流,但哪怕你仅仅式创建了可读流都将会生成一个文件描述符,和上面的显示文件描述符一样,必须手动关闭才能释放文件描述符,并且着这个文件描述符的占用都是 Node.js 进程销毁的时候才会进行回收。在这种场景下,如果你和 C 或 C++交互,哪怕你使用的是读流,都可能导致当前的文件描述符占用,导致 C 或 C++无法正常读取文件。
如果说我们的文件描述符被大量泄漏了,那么到了一定的数值,整个 Node.js 进程服务将会出现一个假死的现象,比如一直卡住在读文件的方法上,无法进行下一步运行,那么这个时候我们就可以考虑是否泄漏了文件描述符。
对于文件描述符的泄漏,我们在 linux 下可以使用lsof
命令来查看你某个进程下的文件描述符情况。比如我们的 Node.js 进程是 12345 ,那么我们可以使用以下的命令来查看该进程下的文件描述符使用情况。
lsof -p 12345
然后我们根据文件的使用情况,查找代码的使用情况来对代码进行一个修改对文件描述符进行关闭,比如上面 fs.open 和 createReadStream 的例子,可以使用下面的代码来进行关闭。
const fs = require("fs");
fs.open("test.txt", "r", (err, fd) => {
// 使用完成后关闭文件描述符
fd.close();
});
const fd = fs.createReadStream("test.txt");
// 流使用完成后进行销毁
fd.destroy();
这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。
V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。
V2EX is a community of developers, designers and creative people.