做 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 默认值会随内存大小计算出来,通常是内存页数的一个比例。想临时调大可以直接写:
bash复制sysctl -w fs.file-max=524288
永久生效则写到 /etc/sysctl.conf 或 /etc/sysctl.d/ 下的独立文件里,执行 sysctl -p 加载。
另一个参数 fs.nr_open 是单个进程能打开 fd 数目的硬上限。它实际上决定了 RLIMIT_NOFILE 的 ceiling,默认通常为 1048576。也就是说,即使你把 /etc/security/limits.conf 里的 nofile 改成 2000000,如果没提前调大 nr_open,这个值也起不来。实际调优时这两个参数需要成对考虑,特别是一些代理类服务想单进程扛百万连接时,fs.nr_open 必须跟着放大。
2.2 用户级资源限制:ulimit 与 limits.conf
系统级之上,Linux 还会按用户做资源限制。你在 shell 里执行:
bash复制ulimit -n
看到的是当前 shell 及其子进程能使用的最大 fd 数量。这个值分软限制(soft)和硬限制(hard)。软限制是进程实际跑起来时的上限,硬限制是普通用户能通过 ulimit 命令调整的边界。普通用户只能把软限制调高到不超过硬限制,想突破硬限制必须得有 root 权限。
更改方法主流是编辑 /etc/security/limits.conf 或者放在 /etc/security/limits.d/ 下的配置文件:
bash复制* soft nofile 65535
* hard nofile 65535
* soft nproc 65535
* hard nproc 65535
第一列 * 表示对所有用户生效;如果只想限制某个用户,就写用户名。nproc 对应的就是进程数限制,后面我会专门展开。需要注意这些配置是给通过 PAM 登录的用户会话生效的,不是说你改完立刻对系统里所有常驻进程生效,而 sshd、systemd 登录服务必须确认对应用户的 PAM 配置里加载了 pam_limits.so,否则 limits.conf 里的规则很可能被静默忽略。
2.3 进程自己的闸门:systemd 与 nginx/java
真正跑业务的高并发进程往往不在交互式 shell 里,而是由 systemd 管理,或由 nginx、Java 这类应用自己读取配置。这段最容易被坑:你明明把 limits.conf 改成了 1048576,重启服务后却发现进程仍然只有 1024 的上限,这大概率是因为 systemd unit 文件里有自己的限制,并且会覆盖 limits.conf。
systemd service 里控制 fd 和进程数的常见配置项是 LimitNOFILE、LimitNPROC 和 TasksMax:
ini复制[Service]
LimitNOFILE=1048576
LimitNPROC=65535
TasksMax=infinity
改完 unit 文件后要执行 systemctl daemon-reload 并重启服务。像 nginx 这类软件自身还会在配置里再设一道闸门,比如 worker_rlimit_nofile 65535;,但 nginx 的这个指令其实是在调 worker 进程的 setrlimit,如果系统硬限制比你设得低,配置不会生效,所以排查顺序应该从内核到 systemd 到软件配置一路看下来。
3. 进程数限制:最容易被人搞混的两个东西
3.1 系统总进程数:kernel.pid_max 与 threads-max
很多人在查“进程数限制”时,先看的是 kernel.pid_max:
bash复制cat /proc/sys/kernel/pid_max
这个参数决定系统可以分配的最大 PID 编号。注意它是“编号”,并不完全等同于同时存在的进程总数。进程退出后 PID 可以被复用,所以理论上系统可以不断创建进程。不过一旦 PID 编号耗尽,内核就无法再分配新的进程,表现就是 fork: Cannot allocate memory 或者 Resource temporarily unavailable。
实际上,对线程密集的应用,内核线程和用户态线程也都需要占用 PID,所以 32 位系统常见的进程数瓶颈 32768 在今天的服务器上已经捉襟见肘。64 位内核的默认值通常会大得多,但某些云镜像、容器化内核也可能保持一个偏小的值。手动调大时直接写进 sysctl 即可:
bash复制sysctl -w kernel.pid_max=4194304
/proc/sys/kernel/threads-max 则控制系统范围内线程总数上限,这个值往往和内存大小相关,因为每个线程在内核里都要对应一块 task_struct 空间。
3.2 单用户的进程数上限:RLIMIT_NPROC
除了系统总量,Linux 还会按用户名做进程/线程数限制,对应的资源项是 RLIMIT_NPROC,配置入口就是前面提到的 limits.conf 里的 nproc。
这个限制很多人理解是“每个进程能产生多少子进程”,其实不准确。它的准确含义是:某个真实用户 ID 下所有进程加线程的总数不能超过该值。也就是说,你跑了一个 Java 应用,它内部起了 500 个线程,这 500 个都算在这个用户头上。
举个例子,你用普通用户跑测试脚本,同时起了大量 PHP-FPM worker,脚本里再开多线程,结果很可能连 SSH 登录都会报 Resource temporarily unavailable,因为你的用户总线程数已经顶到 nproc 上限。登录都进不去的时候,唯一能做的就是从宿主机或另一个管理员用户进去清理进程。
注意:
nproc的上限并不是对 root 用户管理得那么死,但在生产系统上,不能用“反正我是 root”赌这个问题。更常见的是服务运行在一个独立用户下,配置写错就真的会影响业务。
查看一个运行中进程具体受到什么限制,可以读它的 limits 文件:
bash复制cat /proc/<pid>/limits
这里面会明确列出 Max open files、Max processes、Max threads 等项的软硬限制,比任何命令都直观。
3.3 容器和 cgroup 额外加的那一层
现在的服务大量跑在 Docker 或 Kubernetes 里,这就让问题变得更有意思:即使宿主机 pid_max 还有富余,容器内仍然可能报 fork 失败。原因在于容器还有 cgroup 的 pids 控制器在限制。
查看容器内的 pids 限制:
bash复制cat /sys/fs/cgroup/pids/pids.max
Docker 启动时可以用 --pids-limit 来控制容器内进程总数。Kubernetes 里虽然不直接暴露这个参数,但不少运行时会有默认的 pids cgroup 限制。你排查的顺序一定要从外到内:先看宿主机的 pid_max,再看 systemd 的 TasksMax,再看容器 cgroup 限制,最后看应用所在用户的 nproc。
这也是我强烈建议把“进程数限制”当作一个多层问题来对待的原因。单看任何一层都有盲区,很多值班同学在容器里调了半天 limit,没想到真正卡住的是宿主机 cgroup 或 systemd TasksMax。
4. 一个线上排障实录:Too many open files
4.1 现象与第一波判断
某次线上 Java 网关突然开始频繁报错,核心日志是 java.io.IOException: Too many open files。这个报错说明进程在调用 open() 或 accept() 时,已经撞上了 RLIMIT_NOFILE 的软限制。第一波不要着急改参数,先看进程到底开了多少 fd:
bash复制ls /proc/<pid>/fd | wc -l
如果这个数字已经很接近进程的 nofile 上限,基本可以确认是 fd 耗尽。再看这些 fd 都归属什么类型:
bash复制lsof -p <pid> | awk '{print $5}' | sort | uniq -c | sort -rn
现场排查时我看到有几万个 TCP 连接处于 CLOSE_WAIT 状态。CLOSE_WAIT 意味着对端已经关闭连接,但本地服务没调用 close(),这些 socket 会一直占着 fd。根源通常是业务代码读取完数据后没有正确关闭响应,或者连接池里的空闲连接没有回收。
4.2 压测验证与参数调整过程
临时恢复服务可以先调高这个进程的软限制。以 root 用户执行:
bash复制prlimit --pid <pid> --nofile=1048576:1048576
或者用普通方式,通过 ulimit 修改后重启进程:
bash复制ulimit -n 1048576
但注意不能再高了,因为前面说的 fs.nr_open 默认值就是 1048576。如果单进程需要超过这个值,先调大 nr_open:
bash复制sysctl -w fs.nr_open=2097152
然后再重新启动服务。这类调整如果确认要长期使用,就同步写进 limits.d 和 sysctl.conf,避免机器重启后回退。调整完用压测工具模拟真实流量过来,对比调整前后的 fd 曲线。
这里给一个实用估算思路:不要凭空设一个很大的数。先看峰值并发连接数,再统计单连接平均占用的 fd 数。通常一个连接就是 1 个 fd,但如果你开了日志、配置中心、数据库连接池、监控上报,可能一条业务请求会牵扯 5~10 个 fd。行业里比较稳妥的做法是按“预估峰值连接数 x 单连接 fd 数 x 1.5 冗余”来设计。比如峰值 3 万并发,单请求平均 5 个 fd,那么 nofile 至少给到 225000。
4.3 排查命令速查与预防手段
这部分命令非常值得保存一份。我整理了常用的排查命令:
| 目标 | 命令 |
|---|---|
| 查看当前系统已用/最大文件句柄 | cat /proc/sys/fs/file-nr |
| 查看进程 fd 数量 | ls /proc/<pid>/fd | wc -l |
| 查看进程详细限制 | cat /proc/<pid>/limits |
| 列出进程打开的文件/网络连接 | lsof -p <pid> |
| 查看系统 PID 编号上限 | cat /proc/sys/kernel/pid_max |
| 查看系统线程总数上限 | cat /proc/sys/kernel/threads-max |
| 查看某个用户的进程/线程总数 | ps -eLf | awk '{print $1}' | sort | uniq -c |
| 查看容器 pids cgroup 限制 | cat /sys/fs/cgroup/pids/pids.max |
长期预防上,一个是做 fd 使用率监控,达到 70% 就要警惕,超过 85% 建议触发告警;另一个是在代码层面注意连接泄漏和资源释放。尤其是 Python、Java、Go 这些带 GC 或带运行时管理的语言,进程退出前不一定立刻回收 OS 层 fd,单纯依赖语言自带的垃圾回收并不够,连接池、HTTP client 都要显式 close。
4.4 顺便说一句:不是所有“fork 失败”都是内存不够
很多人看到 fork: Cannot allocate memory 第一反应是内存不足,但实际经常是进程数限制或线程数限制触发。一次我排查 Redis 持久化失败,日志提示无法 fork 子进程,free 看内存仍然有大量剩余。后来发现是这个容器里的进程数已经到了 cgroup 限制,Redis 想 fork 一个子进程做 RDB 快照,结果申请不到新的进程名额。
排查思路很简单:dmesg 里如果没有 OOM 记录,就往 pid_max、nproc、cgroup pids 方向走。把两类问题分清,能省下很多无效折腾。
5. 常用调优配置与典型误区
5.1 一套可落地的服务器调优模板
这里给一套生产环境比较常见的基础调优配置,你可以根据自己的内存和业务规模调整。先改 /etc/sysctl.d/99-file-limit.conf:
ini复制fs.file-max = 2097152
fs.nr_open = 2097152
kernel.pid_max = 4194304
kernel.threads-max = 4194304
执行:
bash复制sysctl --system
然后是用户级限制,在 /etc/security/limits.d/90-app.conf 写入:
bash复制* soft nofile 1048576
* hard nofile 1048576
* soft nproc 65535
* hard nproc 65535
systemd 管理的服务可以单独覆盖。比如 nginx 服务,在 /etc/systemd/system/nginx.service.d/override.conf 里写:
ini复制[Service]
LimitNOFILE=1048576
LimitNPROC=65535
TasksMax=infinity
然后:
bash复制systemctl daemon-reload
systemctl restart nginx
注意不要把 LimitNOFILE 随便设成 infinity,更好的做法是给一个贴合业务的明确数字。infinity 在一些 systemd 版本里会被解析成很大又不可控的值,容易掩盖问题而不是解决问题。
5.2 为什么改了 limits.conf 却不生效
这个是我被问过最多的问题,没有之一。改完 limits.conf 不生效,最常见的原因有这几个:
- 服务是 systemd 托管的,unit 文件里的
LimitNOFILE覆盖了 limits.conf; - 修改后没有重新登录,因为 limits.conf 只在 PAM 会话建立时加载,旧的 shell 还持有旧值;
- 目标进程不是通过 PAM 登录启动的,比如某些通过脚本 nohup 启动的守护进程,可能根本不读 limits.conf;
*通配符并不覆盖 root 用户,如果需要限制 root,得单独写 root 条目;- 你尝试在普通用户下把软限制调到高于硬限制,这会被内核拒绝,必须同时调高硬限制。
判断是否生效,最佳实践不是看 ulimit -n 的输出,而是直接读目标进程的 /proc/<pid>/limits。那个文件实时反映了内核里真正生效的值。
5.3 面试和实操中常见的几个细节问题
如果你是准备面试或带新人,下面几个细节很适合检验是否真的理解了这套机制。
fork() 之后子进程的 fd 会全部继承,那么父进程关闭这个 fd 子进程是否一定关闭?不是,同一个打开文件描述被引用计数管理,要等所有引用它的 fd 都关闭后才会真正释放。
为什么程序一启动就会有一个 socket 出现在 fd 列表里?可能是服务监听端口,也可能是和配置中心或日志系统建立的连接。想看真相直接查 lsof -i 和进程网络状态。
nproc 限制一个用户最多 65535 个进程/线程,但系统 pid_max 是 4194304,这冲突吗?不冲突,一个是用户维度,一个是全局维度。就像一条高速公路总容量和单车道限速是两个维度。
这些细节搞清楚了,你再看线上报错就不会再盲目执行 ulimit -n 65535 然后发现什么都没变了。
我自己的体会是,Linux 资源限制这条链路本质上是一个层层嵌套的门禁:内核放行、用户放行、systemd 放行、应用自己再放行,任何一层的门没开够,业务都进不去。排查时不要只盯着一处,带上 /proc 下的实时数据逐层看,通常五分钟内就能定位到底卡在哪一层。这个思路比死记命令更重要。
