登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  linux

Linux PID namespace 中 init 进程怎样回收子进程

来源:17golang原创

时间:2026-10-14 14:41:37 206浏览 收藏

Linux PID namespace 中 init 进程怎样回收子进程

在 PID namespace 里,最先创建的进程会拿到命名空间内的 PID 1。它不只是一个普通业务进程:当其他进程的父进程退出后,内核会把孤儿进程重新挂到合适的 init 或 subreaper 上;如果这个命名空间里的 init 进程不及时调用 wait 或 waitpid,退出的子进程就可能长期停留为僵尸进程。

实用结论是:把 namespace init 当作一个小型进程管理器设计。它至少要安装必要的信号处理逻辑,持续回收自己接管的子进程,并把“PID 1 退出会导致整个命名空间失效”当作生命周期约束,而不是普通服务重启细节。

PID namespace 中的 init 负责接管孤儿并回收已退出子进程;真正执行回收的是 wait 系列系统调用。PID 1 一旦退出,内核会终止该命名空间内的其他进程,不能再把它当作可热替换的普通父进程。

PID namespace 的 1 号进程到底特殊在哪里

新 PID namespace 的进程号从 1 开始。进程在自身命名空间和祖先命名空间中可能拥有不同的 PID,祖先命名空间可以观察子命名空间中的进程,而子命名空间不能反向看到祖先中的进程。因此,容器内看到的 PID 1 和宿主机看到的同一进程,通常不是同一个数字。

根 PID namespace、子 PID namespace、namespace init 和业务进程之间的静态关系
图1:PID namespace 层级与进程可见性的静态说明图,不是截图或运行证据。

“init”这个名字表示职责,而不要求进程必须是 systemd、BusyBox init 或某个固定实现。一个很小的 C、Go 或 shell 包装进程也可以成为命名空间内的 PID 1,但它需要主动承担信号转发、退出状态和子进程回收等工作。

孤儿进程为什么会交给 init

假设业务父进程先退出,而它创建的子进程仍在运行。这个子进程不能保留一个已经不存在的父进程,内核会把它重新父子关系挂到父进程所在 PID namespace 的 init 进程;如果更近的祖先设置了 PR_SET_CHILD_SUBREAPER,则可能由这个 subreaper 接管。

这里有一个容易弄错的边界:接管者取决于“父进程所在的命名空间”,不一定就是子进程当前视角里最靠近的容器 PID 1。跨 namespace 创建和 setns、unshare 会让进程的父子关系与命名空间树不完全重合,所以排查时要同时看父进程关系和 namespace 层级。

被接管并不等于已经回收。进程退出后,内核仍要保留退出码、资源使用等少量信息,直到父进程调用 wait 系列系统调用读取这些信息;这个等待动作完成后,僵尸进程对应的任务表项才会被释放。

PID 1 怎样用 SIGCHLD 和 waitpid 回收

常见做法是让 SIGCHLD 处理器只设置一个轻量标记,把真正的 waitpid 循环放到主循环或事件循环中。这样可以避免在信号处理器里塞入日志、内存分配和复杂业务逻辑。

#include 
#include 
#include 
#include 

static volatile sig_atomic_t child_event = 0;

static void on_sigchld(int signo) {
    (void)signo;
    // 信号处理器只记账,避免在异步上下文中做复杂工作。
    child_event = 1;
}

static void reap_children(void) {
    int status;
    for (;;) {
        // -1 表示回收当前进程有权等待的任意子进程;WNOHANG 避免阻塞主循环。
        pid_t pid = waitpid(-1, &status, WNOHANG);
        if (pid > 0) {
            // 这里可以记录 pid、退出码或信号,但不要把业务逻辑塞进信号处理器。
            continue;
        }
        if (pid == 0) {
            // 当前没有更多已经退出的子进程,下一次 SIGCHLD 再继续处理。
            break;
        }
        if (errno == EINTR) {
            // 被其他信号打断时重试,避免漏掉待回收的子进程。
            continue;
        }
        // ECHILD 表示已经没有可等待的子进程,其他错误应交给生命周期策略处理。
        break;
    }
}

int main(void) {
    struct sigaction action = {0};
    action.sa_handler = on_sigchld;
    sigemptyset(&action.sa_mask);
    // 建立 SIGCHLD 处理逻辑后,PID 1 才能稳定感知子进程退出。
    sigaction(SIGCHLD, &action, NULL);

    for (;;) {
        // 真实服务可在这里处理信号、管道或 socket 事件。
        pause();
        if (child_event) {
            child_event = 0;
            reap_children();
        }
    }
}

这段代码只是回收骨架,不负责启动容器,也没有假设任何具体业务协议。核心是 waitpid(-1, ..., WNOHANG) 要循环调用:一次信号到达时可能对应多个已经退出的子进程,单次调用会留下未回收的僵尸。

生产实现还应明确三件事:退出码是否需要传给上层、收到 SIGTERM 后是否先停止拉起新子进程、以及主循环被其他信号打断时怎样继续回收。若直接把 SIGCHLD 设置为忽略或使用 SA_NOCLDWAIT,内核可以避免保留僵尸,但应用也会失去读取子进程退出状态的机会,不能无条件替代显式 waitpid。

普通父进程回收和命名空间回收不是一回事

普通服务通常只需要等待自己直接创建的子进程;namespace init 还要准备接管那些原父进程已经退出的后代。于是它的回收范围不是“我启动的那几个 PID”,而是“当前有权等待的全部子进程”。这也是推荐使用 waitpid(-1, ...) 循环,而不是维护一份可能过期的 PID 白名单的原因。

/proc 的视图也要一起考虑。procfs 显示哪些进程,取决于挂载它的进程所在 PID namespace。创建新的 PID namespace 后,通常还要在对应的 mount namespace 中挂载新的 procfs,容器内的诊断工具才能看到与容器 PID 号一致的列表;否则你可能误以为 PID 1 没有子进程,实际只是看到了另一个层级的视图。

可以用下面的命令表达一个“创建 PID namespace 并把 procfs 挂载到新视图”的思路。它是操作示意,不代表本文在本机执行过。

# --pid 创建新的 PID namespace,--fork 让后续 shell 成为其中的 PID 1。
# --mount-proc 让 procfs 按新的 PID 视图呈现,便于观察容器内进程关系。
unshare --pid --fork --mount-proc /bin/sh

# 在命名空间内查看进程树;输出只用于观察,不替代 waitpid 回收职责。
ps -o pid,ppid,stat,comm

PID 1 退出时为什么会连带终止整个 namespace

Linux 把 namespace init 视为命名空间正确运行所必需的进程。如果它退出,内核会向该 PID namespace 中的进程发送 SIGKILL;之后再尝试向这个已经没有 init 的命名空间创建进程,会失败并返回 ENOMEM。因此,PID 1 的主循环不能只关心业务子进程,还要定义“收到终止信号后如何有序结束”和“所有关键子进程结束后自己何时退出”。

PID 1 通过 waitpid 回收孤儿和退出子进程并连接到命名空间生命周期边界
图2:PID 1、SIGCHLD、waitpid 与命名空间退出边界的静态关系图,不是截图或运行证据。

从容器运行时的角度看,这个约束解释了两个常见设计:一是让一个专门的 init wrapper 作为容器入口,再由它启动业务程序;二是让 wrapper 明确转发终止信号,并在回收全部子进程后返回合理退出码。wrapper 不是为了“伪造”一个 init,而是为了补上单进程应用经常缺少的子进程管理职责。

一张表记住回收边界

场景内核关系PID 1 应做什么
直接子进程退出保留退出信息,等待父进程读取处理 SIGCHLD,并循环 waitpid
原父进程提前退出孤儿重新挂到合适的 init 或 subreaper不要只等待自己启动的固定 PID
容器内 procfs 视图不匹配看到的 PID 列表来自另一个挂载层级在正确的 mount namespace 中挂载 procfs
namespace init 退出命名空间内进程被 SIGKILL,后续创建受限把信号转发、清理和退出策略放在生命周期设计里

常见问题

PID 1 必须使用 systemd 吗?

不必须。关键是它要实现与场景匹配的信号处理、子进程回收和退出策略。极简容器可以使用专门的 init wrapper,完整系统才可能选择 systemd。

为什么只调用一次 waitpid 还会有僵尸?

一次 SIGCHLD 可能对应多个子进程退出。单次 waitpid 只取走一个退出状态,应该在返回正数时持续循环,直到返回 0 或确认没有可等待的子进程。

子 namespace 里的 PID 1 能看到宿主机进程吗?

不能按子 namespace 的视角反向观察祖先 namespace。祖先可以观察子 namespace 中的进程,但同一进程在不同层级使用的 PID 可能不同。

本文依据的 Linux 资料在哪里?

Linux PID namespace 的生命周期、PID 1、孤儿接管和 procfs 视图可参考:https://man7.org/linux/man-pages/man7/pid_namespaces.7.html。本文示例只用于解释结构与职责,图像也是静态说明图,不是运行截图。

把 namespace init 设计成“会回收子进程、能处理信号、知道何时退出”的小型进程管理器,PID namespace 的核心边界就清楚了:孤儿由合适的 init 接管,退出状态由 wait 系列消费,而 PID 1 的退出会结束整个命名空间的可用生命周期。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>