Linux 中 PID 是如何分配的?

17次阅读

Linux 内核用位图找最小空闲 PID:从 1 开始扫描首个为 0 的比特位,置 1 后返回编号;位图每页管 32768 个 PID,对应默认 pid_max 值。

Linux 中 PID 是如何分配的?

内核用位图找最小空闲 PID

Linux 分配 PID 的核心动作是:从 1 开始,扫描位图(bitmap)中第一个值为 0 的比特位,将其置 1,并返回对应编号。这个“最小可用号”策略保证了 PID 尽可能紧凑,也便于用户快速识别“刚起的进程”。位图按页组织,每页 4096 字节 → 可管理 32768 个 PID(4096 × 8),正好匹配默认 /proc/sys/kernel/pid_max 值。

常见错误现象:fork() 失败报 Resource temporarily unavailable,不一定是内存不足,很可能是位图里真没空位了——比如 pid_max=32768 时系统已有 32767 个活进程(PID 1systemd 占着),再 fork 就会失败。

  • last_pid 是全局变量,记录上次成功分配的 PID,下次从 last_pid + 1 开始找,避免每次都从头扫
  • RESERVED_PIDS(通常为 300)不参与动态分配,留给内核线程和关键守护进程
  • 位图本身不存进程信息,只管“编号是否被占”,真正关联进程靠 struct pidtask_struct 的双向指针

为什么 PID 1 总是 systemd,而 PID 2 不稳定?

PID 1 是硬 编码 保留的:内核启动后直接调用 kernel_thread() 创建第一个用户态进程,强制指定 PID 为 1,后续所有进程都由它派生。现代发行版几乎都用 systemd 占据该位置;若换成 sysvinitopenrc,PID 1 还是它,只是二进制不同。

PID 2 则没有这种保障:它是内核线程 kthreadd 的 PID,但该线程在 rest_init() 中创建,时机紧邻 PID 1,一旦启动顺序微调(如 init 进程初始化稍慢),PID 2 就可能被其他早期内核线程抢走。所以脚本里写死 kill -9 2 是危险操作。

  • 不要依赖 PID 2 指向特定线程,查 ps -o pid,comm -p 2 才能确认当前是什么
  • PID 0 固定属于 swapper(调度器空闲进程),永远不可见、不可 kill
  • 容器内看到的 PID 1 是命名空间局部 PID,其全局 PID 一定是另一个大数,可通过 /proc/1/statusNSpid 字段验证

pid_max 调大就能无限开进程?别信

调高 pid_max(比如 sysctl -w kernel.pid_max=4194304)只是放宽了编号池上限,并不等于系统能真的运行 400 万个进程。每个 PID 对应一个 task_struct 结构体,占用约 5–10 KB 内存;PID 池扩大四倍,仅这部分内存就多吃掉上 GB。更现实的瓶颈往往是 ulimit -u(单用户 nproc 限制)或 RLIMIT_AS(地址空间)。

典型误判场景:监控显示 ps -eLf | wc -l 输出 3000,sysctl kernel.pid_max 是 32768,就以为还有 29000 个 PID 可用——其实大量 PID 已被僵尸进程(Z 状态)占着未回收,ps aux | awk '$8 ~ /Z/ {count++} END{print count+0}' 才是真实“卡住”的数量。

  • 调整 pid_max 后必须重启部分服务(如 systemd 的子进程不会自动感知新上限)
  • 容器环境要同时调大宿主机 pid_max 和容器内 pid cgroup 限额,否则 docker run --pids-limit 会优先触发
  • 高频 fork/exit 场景(如短命 CGI 进程)建议用 pid_max=65536 + echo 1 > /proc/sys/kernel/panic_on_oom 避免位图扫描拖慢分配

alloc_pid() 函数里最易忽略的三件事

看内核源码 kernel/pid.calloc_pid(),表面只是遍历命名空间层级分配数字,但有三个细节常被跳过:

  • 它先分配 struct pid 内存(来自 per-namespace 的 pid_cachep slab),再逐层调用 idr_alloc_cyclic() 填数字——如果某层命名空间的 IDR 树已满,整个分配就失败,不是重试而是直接 return ERR_PTR(-EAGAIN)
  • CLONE_NEWPID 创建新命名空间时,子空间的 level 比父空间 +1,且 numbers[] 数组长度 = level + 1,意味着嵌套 10 层容器后,每个进程要维护 11 个 PID 值(1 个全局 + 10 个局部)
  • 分配成功后,task_struct->pid 赋的是局部 PID(即当前命名空间视角的编号),而信号发送、kill() 系统调用底层用的却是全局 PID——这个转换在 find_vpid() 里完成,不是零成本

真正复杂的从来不是“怎么分”,而是“分完之后怎么让所有子系统(调度、cgroup、ptrace、信号)都认这个号”。PID 管理是 Linux 内核里少有的横跨 namespace、memory、scheduling 三大子系统的胶水逻辑,动它之前,先确认你改的到底是编号池,还是整个进程身份体系。

text=ZqhQzanResources