技术指南

Linux 无法创建进程:区分用户限制、cgroup 与 PID 空间

从 EAGAIN、真实 UID 与计数增量出发,区分 RLIMIT_NPROC、cgroup PID 限制、系统线程上限和 PID 命名空间。

服务报 `fork: Resource temporarily unavailable`、线程创建失败或 systemd 无法启动新进程时,先保存真实 errno,再区分四个边界:真实用户 ID 的 `RLIMIT_NPROC`、服务或父级 cgroup 的 `pids.max`、主机的 `threads-max`,以及 PID 命名空间的可分配编号。它们都可能让 `fork()` 返回 `EAGAIN`,但作用域和修复方法不同;进程列表很长或内存紧张都不能单独定案。

从错误与计数增量选择证据链

同窗观察优先边界需要确认
目标 unit 的 `pids.current` 接近有效上限,`pids.events:max` 增长服务或祖先 cgroup PID 限制ControlGroup、TasksCurrent/EffectiveTasksMax、当前与祖先 pids 文件
同一真实 UID 的线程数达到该进程 `Max processes` 软限制RLIMIT_NPROC失败进程的真实 UID、`/proc/PID/limits` 与该 UID 的任务快照
不相关服务同时无法创建任务,系统任务数逼近 `threads-max`主机级线程上限全机任务数、threads-max、内存与内核日志
只在容器内失败,外部主机仍正常,PID 视图不同PID namespace 或容器/cgroup 边界NSpid、容器 init、父级 cgroup 与编排限制
`fork()` 返回 `ENOMEM` 而不是 `EAGAIN`内存或已失去 init 的 PID namespace内存/OOM 证据与 namespace 生命周期,不套用本页的限额结论

先固定失败进程、真实 UID 与命名空间

把 example.service 换成真实 unit;从未受影响的管理会话执行。旧 systemd 可能没有 EffectiveTasksMax,容器内的 PID 和 cgroup 视图也可能与宿主不同
unit=example.service
pid=$(systemctl show "$unit" -p MainPID --value)
test "$pid" -gt 0

date --utc --iso-8601=seconds
systemctl show "$unit" \
  -p ActiveState -p SubState -p Result -p MainPID -p ControlGroup \
  -p TasksCurrent -p TasksMax -p EffectiveTasksMax
awk '/^(Name|State|Tgid|Pid|PPid|Uid|Threads|NSpid):/ {print}' \
  "/proc/$pid/status"
sed -n '/Max processes/p' "/proc/$pid/limits"
readlink "/proc/$pid/ns/pid"

`RLIMIT_NPROC` 按调用者的真实 UID 统计现存进程,更准确地说在 Linux 上统计线程;`/proc/PID/status` 的 `Uid` 第一列是 real UID,`Threads` 是该进程的线程数。它不是单个 service 的 worker 上限,也不对 real UID 0 或具备特定资源管理能力的进程同样执行。先锁定失败进程身份,不能拿当前 shell 的 `ulimit` 代表 systemd 服务。

先查 service,再沿祖先 cgroup 向上

适用于统一 cgroup v2。读取开始与结束两份快照并比较 pids.events 的 max 增量;当前 cgroup 未触顶时,祖先 slice 仍可能是实际限制者
cg=$(systemctl show "$unit" -p ControlGroup --value)
test -n "$cg"
while :; do
  base="/sys/fs/cgroup${cg}"
  printf '\n[cgroup %s]\n' "$cg"
  for name in pids.current pids.peak pids.max pids.events pids.events.local; do
    if test -r "$base/$name"; then
      printf '%s: ' "$name"
      tr '\n' ' ' <"$base/$name"
      printf '\n'
    fi
  done
  test "$cg" = / && break
  cg=${cg%/*}
  test -n "$cg" || cg=/
done

cgroup v2 的 `pids.current` 统计当前 cgroup 及后代任务,`pids.max` 是硬边界,`pids.peak` 是历史峰值。创建新任务若会违反本层或祖先策略,`fork()`/`clone()` 返回 `EAGAIN`;`pids.events` 的 `max` 是累计触顶次数。单个历史非零值不能证明本次事故,必须保存失败窗口的增量,并同时检查祖先。

RLIMIT_NPROC 要按真实 UID 汇总

任务数在采样中会变化,结果只代表该时间点。命令清单可能暴露租户、账号和进程名,使用 0600 证据目录并在分享前脱敏
uid=$(awk '/^Uid:/ {print $2}' "/proc/$pid/status")
printf 'real_uid=%s\n' "$uid"
prlimit --pid "$pid" --nproc
ps -eLo ruid=,pid=,tid=,ppid=,stat=,comm= \
  | awk -v uid="$uid" '$1 == uid {print}' \
  >"uid-${uid}-tasks.txt"
wc -l <"uid-${uid}-tasks.txt"
ps -eo ruid=,pid=,ppid=,nlwp=,stat=,comm= --sort=-nlwp | head -n 40

`/proc/PID/limits` 和 `prlimit --pid` 显示目标进程真正继承的软、硬限制。shell 的 limits 会由子进程继承并跨 `execve()` 保留,但 systemd 服务来自服务管理器及 unit 配置,不一定继承登录会话的 PAM 设置。多个服务共用一个 UID 时,`RLIMIT_NPROC` 会把它们合并计数;若目标是隔离单个 unit,通常应优先修复工作模型并使用明确的 cgroup `TasksMax=`。

系统级上限只在广泛故障时升级调查

从未受影响的会话读取;task_snapshot 是动态近似值。pid_max 是 PID 分配回绕值,不应把最高可见 PID 或一次接近回绕直接写成耗尽
printf 'threads_max='; cat /proc/sys/kernel/threads-max
printf 'pid_max='; cat /proc/sys/kernel/pid_max
printf 'namespace_last_pid='; cat /proc/sys/kernel/ns_last_pid
printf 'task_snapshot='; ps -eLo tid= | wc -l
journalctl -k --since '-15 minutes' --no-pager \
  | grep -Ei 'fork|clone|resource temporarily unavailable|cannot allocate memory|oom'

内核的 `threads-max` 控制可由 `fork()` 创建的系统级线程总量,并根据可用内存初始化;`pid_max` 控制 PID 分配回绕值。全机任务数逼近 threads-max 且多个不相关服务同窗失败,才支持主机级假设。PID 分配还受命名空间和正在占用的编号影响,不能用 `max(PID)` 直接计算剩余容量。

找到增长者,也检查没有被回收的子进程

现场解释修复方向
单个进程 NLWP 持续增长线程池、库或失败重试没有收敛限制入口,保存线程栈/应用指标并修复生命周期
同一父进程下短命子进程不断增加worker/命令生成失控停止生成源,建立有界并发和退避
大量 Z 状态且 PPID 相同父进程没有 wait/reap 已退出子进程修复父进程回收;僵尸本身不能靠普通 kill 清除
多个 unit 共用 UID,合计撞到 nproc账号级限制与服务边界错位拆分服务身份或改用 unit cgroup 限制
父级 slice pids.max 触顶子 unit 单独看仍有余量按整个 slice 的容量与故障域修复,不只改子 unit

先停止增长,再恢复最小生产能力

  1. 从现有管理会话暂停创建任务的定时器、队列消费者、部署或无界重试;不要先启动更多诊断代理。
  2. 保存目标 PID、真实 UID、unit/cgroup 层级、limits、pids 计数与增长者,再优雅停止明确失控的 unit;状态型服务先核对一致性和队列租约。
  3. 只恢复最小 worker 数并观察新任务是否能创建、`pids.events:max` 是否停止增长、业务积压是否下降。
  4. 只有正常工作集确实超过合理边界且宿主内存、PID 与故障预算允许时,才单独调整 `TasksMax=` 或账号限制;保存旧值和撤销时间。
  5. 若连恢复命令都无法执行,使用已保留的控制台或供应商带外通道。重启是最后的可用性动作,不是根因证明;重启前先评估未落盘数据。

用同一负载验证限制与业务都恢复

  • 真实请求、后台任务和服务启动恢复,应用不再记录 EAGAIN/线程创建错误,队列没有继续扩大。
  • 目标 unit 及祖先的 pids.current 回到已定义余量,观察窗内 pids.events:max 没有新增。
  • 同一真实 UID 的任务数低于有效 RLIMIT_NPROC,线程或子进程增长曲线已经收敛。
  • 系统任务数、内存和关键邻居保持稳定,没有把故障转移到父级 slice 或其他服务。
  • 若改过边界,恢复旧值也能稳定运行,或保留新值的容量依据、监控阈值、回滚条件和负责人。

监控增长率,不维护一组无限大数值

为关键 unit 保存 TasksCurrent、EffectiveTasksMax、pids.current/peak 和 pids.events 增量,同时记录每个服务的 worker、线程、子进程、队列和真实业务吞吐。容器编排、systemd、线程库、进程模型或账号拆分变化后重建基线。全局提高 `threads-max` 或 `pid_max` 会扩大内核内存和故障半径,不能替代有界并发、正确回收和 per-unit 隔离。

本文命令在 2026-08-19 按 Linux man-pages、Linux 内核、util-linux 与 systemd 当前上游资料完成语义和语法审阅,没有制造 fork bomb、线程泄漏、PID/cgroup 耗尽,也没有修改运行系统的 limits、TasksMax 或 sysctl。发行版补丁、cgroup v1、容器的 PID/cgroup namespace、PAM、服务能力和 systemd 版本会改变可见边界;执行前确认目标进程身份、独立控制台、证据权限和状态型服务恢复要求。

返回知识库