
字节笔记本
2026年10月7日 · 约 5 分钟读完
监听文件过多先看句柄上限
启动命令走的是 expo 的 ios。监听文件时失败,错误是 EMFILE,打开的文件过多,错误号是负二十四。运行时是 Node v18.20.2。栈落在 metro-file-map 的 NodeWatcher,行号 82。
临时提高用来验证
这是进程能打开的文件数到了上限。监听要给大量文件各留一个句柄。上限不够,监听直接失败,服务起不来。

先在同一个终端执行 ulimit -n,看当前上限。临时提到 10240,再启动一次。这一步只影响当前终端里之后的进程。新开的终端不会带着这个数字。
临时值能启动,就说明判断正确。不能启动,再把同一个终端的上限再提高一次,确认不是 10240 仍不够。不要在还没看 ulimit -n 的时候就去删依赖。
对话接着把两行写进 sysctl 配置:每个上限都是 65536,一个是系统文件数,一个是每进程文件数。写入用追加。数字是对话给的,不是唯一合法值。机器上若已有更大的值,不要改小。
对话还把 limit maxfiles 65536 65536 追加进 /etc/launchd.conf,并在 shell 配置里写上 ulimit -n 65536。然后要求重开终端,再执行原来的启动命令。
较新的系统不再读取 /etc/launchd.conf。把那一行追加进去,重启后也不会因此提高上限。先以当前终端的 ulimit 能否让启动成功为准。永久生效要看这台系统实际读取哪一份配置,不要把这个旧文件当成已经生效。
shell 配置里的 ulimit 只在交互终端里执行。从别的方式拉起的进程读不到这份配置。你在哪个终端里启动,就在哪个终端里看 ulimit -n。
对话里的写入把 echo 也放进了提权。负责写入的是后面的 tee,提权加在 tee 上即可。这不改变写进去的两行内容。
仍然失败时,对话的下一步是清掉打包缓存再启动,或者删掉依赖目录后重新安装。这两步解决的是缓存和依赖损坏。它们不提高句柄上限。EMFILE 还在时,先确认 ulimit,再做这两步。
清缓存的命令是 npm start 并带上 reset-cache。重新安装是删掉 node_modules 再 npm install。删依赖很慢,而且不改变系统上限。上限仍是默认值时,装完还会在监听时失败。
错误文本里的 watch 表示失败发生在监听,不是发生在编译某一行业务代码。不要对着业务源码找语法错误。栈已经指向监听器。
永久配置要看系统读不读
本机路径和局域网地址会出现在启动日志里。向外记录时删掉用户目录和内网地址。留下错误名、错误号、运行时版本和栈上的文件名即可。

10240 是临时试验值。65536 是对话写进配置的值。试验值能启动,说明当时的文件数落在默认上限和 10240 之间。配置值更大,是为了留给以后更多的文件。
重开终端后先再次执行 ulimit -n,看到的应是 shell 配置里的数字。若仍是很小的默认值,说明 shell 配置没被这个终端读取,或者系统在启动时把上限压了回去。这时去改业务代码没有帮助。
监听的文件包括依赖目录里的大量文件。上限提高到足够之后,启动应能越过 NodeWatcher 这一行。下一则错误若是别的内容,再按那则错误看,不要继续加文件数。
对话没有修改监听范围,也没有更换监听实现。它给的就是把上限调高,以及缓存和依赖两条备用。不要把没出现的配置文件名写成这次的步骤。
负二十四是这次的错误号。和 EMFILE 一起出现。只看到错误号、没有 EMFILE 字样时,仍以完整错误文本为准,不要用别的错误号去套这组命令。
v18.20.2 是对话里的运行时。别的版本同样会在句柄不够时给出 EMFILE。版本用来对照这次日志,不是说只有这个版本才会监听失败。
先临时提高、能启动、再考虑永久配置。永久配置写错文件,只会让你以为已经改过,下次打开终端又失败。每次失败都先看当前进程的上限。
启动脚本最终调用的是 expo 的 ios。上限要加在真正拉起监听的那个进程的父环境里。在另一个窗口改了 ulimit,原来的窗口仍是旧上限。
追加写入不会删掉文件里已有的行。重复执行会把同样的两行再写一遍。确认文件里只有一份,避免以后改数字时改错行。
缓存清理和句柄上限可以都做,但顺序不要反。先让 ulimit -n 的数字变大并成功启动一次。若启动已成功,就不必为了这则错误去删依赖。
这则错误在依赖特别多时更容易出现。删掉不需要的依赖能减少监听数量,那是另一个方向。对话没有走那一步。当前步骤仍是提高上限。
验收看两件事:当前终端的 ulimit -n 已经大于失败时的值,并且启动不再打印 EMFILE。只改了配置文件、终端里的数字没变,验收不算通过。



