做 Linux 运维和后台开发这些年,有两类报错几乎所有人都躲不过:一类是连接池打满时报的 Too many open files,另一类是 SSH 登录或启动进程时突然冒出来的 Resource temporarily unavailable。前者十有八九是文件描述符不够用了,后者则往往和进程数限制有关,但很多人并不知道这两套体系到底怎么叠加、怎么排查、怎么调优。
这篇文章想把文件描述符和进程数限制这两件事彻底讲透。我会先从内核视角解释它们的本质,再逐层拆解系统、用户、进程三个维度的限制是怎么生效的,最后结合线上排障和面试常问的场景,给出可以直接拿去用的排查命令和调优方案。适合刚接触 Linux 的新手、正在被高并发服务折磨的开发者,以及想系统梳理这类知识点的运维同学。
1. 先搞清楚:文件描述符到底是什么
1.1 从“打开文件”到拿到一张数字门票
文件描述符的官方定义是一个非负整数,但你在实际定位问题时可以把想得更简单一点:它是内核返回给进程的一张“数字门票”。进程每次调用 open()、socket()、pipe() 这类系统调用,内核就会在进程自己的资源表里登记一项,然后返回一个整数给应用层。之后你再对这个文件或连接做读写、关闭,基本都是拿着这个整数去找内核办事。
这张表在 Linux 内核里叫做 fd table,每个进程独立维护。你可能会好奇为什么标准输入、标准输出、标准错误偏偏占用了 0、1、2 这三个编号,其实没有什么复杂魔法:这是 POSIX 的约定,shell 启动进程时默认把这三个描述符指向终端或重定向目标,所以从 3 开始才是你程序里第一个真正主动打开的资源。
除了普通文件,socket 连接、管道、epoll 实例、eventfd、inotify 实例、timerfd,方方面面只要是“可以读写、可以被多路复用机制监听”的东西,最终都会占用一个文件描述符。这也是为什么高并发场景下文件描述符非常容易被耗尽:一个 TCP 连接就是一个 fd,一万个并发连接就是一万个 fd。
1.2 fd 的分配和复制机制
内核给新打开的资源分配 fd 时,遵循的是“最小可用编号”规则,也就是说如果你先打开了 fd 3,又打开了 fd 4,关闭 3 后再打开新文件,新资源很可能还是拿到 3。这个规则对应用层而言没有多大影响,但理解它有助于看懂 strace 输出,也方便解释 fork 后的继承关系。
fork() 之后,子进程会从父进程那里继承完整的 fd table 拷贝。这里的“继承”不是简单的复制指针,而是让父子进程指向同一个内核打开文件描述(open file description),所以共享偏移量。网络服务里常见的做法是父进程先监听端口,fork 出多个 worker 后大家都持有同一个 listen fd,内核负责把新连接分发给某一个 worker 的 accept。如果你要写多进程模型,这套继承机制必须心里有数,否则很容易出现“明明关掉了 fd,连接却还被另一个进程握着”的诡异现象。
提示:从
/proc/<pid>/fd/能看到某个进程当前打开的所有 fd。这个目录里的每一项都是一个符号链接,通过readlink可以看到它指向的真实文件、socket、管道或设备。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 限制是怎么层层叠加的:从内核到进程
2.1 系统级参数:fs.file-max 与 fs.nr_open
Linux 对文件描述符的限制并不是只存在于 ulimit 这一层,最底层还有两个内核参数:fs.file-max 和 fs.nr_open。
fs.file-max 是整个系统范围内可以打开的文件描述符总数。可以直接查看当前值和最大值:
bash复制cat /proc/sys/fs/file-max
cat /proc/sys/fs/file-nr
file-nr 会输出三个数字,第一个是已分配的文件句柄数量,第二个是已分配但未使用的句柄数,第三个是最大句柄数。多数发行版里 fs.file-max 默认值会随内存大小计算出来,通常是内存页数的一个比
