在Deepin系统上打开终端敲下ps -ef,你会看到成百上千个进程,它们之间通过父子关系构成一棵进程树,树根是PID为1的进程。在Deepin中,这个角色由systemd担任。但你是否想过一个问题:如果某个父进程先退出了,它那些还没结束的子进程由谁来接管?这些失去父进程的进程通常被称为孤儿进程,它们的归宿并不是被直接清理掉,而是被重新“收养”。本文就来详细拆解Deepin系统下孤儿进程的收养机制。

什么是孤儿进程,为什么需要收养
孤儿进程的定义很简单:父进程先于子进程终止,此时子进程还在运行,就成了孤儿。与之相对的另一个概念是僵尸进程,即子进程已经退出但父进程还没有调用wait()或waitpid()回收其退出状态,进程描述符仍然占用内核资源。这两个概念经常被混为一谈,但它们的处理方式完全不同:孤儿进程本身无害,只是需要一个新父亲;僵尸进程则需要有人调用wait来收尸。
为什么孤儿进程必须有新父亲?因为Linux的进程退出机制要求,任何一个进程终止时,内核必须通知它的父进程,把退出码和资源使用情况交给对方。如果父进程已经不存在,这个通知就无处投递,子进程退出后的状态也将永远无人认领,最终变成无人回收的资源泄漏。所以内核在父进程退出时,会主动遍历它的所有子进程,为每一个活着的子进程寻找一个“养父”,保证进程树上每个节点都有父节点,树的完整性是进程管理的基石。
内核如何完成收养:从find_new_reaper说起
内核中负责收养的核心逻辑位于forget_original_parent()函数中,当父进程退出时,内核会调用它来重新安排子进程的去向。这个函数内部首先尝试find_new_reaper(),寻找收养者的优先级是这样的:第一优先找同一线程组中的其他线程作为养父;如果找不到,就检查最近的祖先进程中是否有标记了PR_SET_CHILD_SUBREAPER属性的进程,这个属性可以通过prctl()设置,表示“我的后代进程变成孤儿时,优先由我收养”,这是3.4版本之后引入的内核特性;如果还没有,最终兜底的就是init进程,也就是PID为1的进程。
在Deepin系统里,PID 1是systemd。也就是说,绝大多数情况下,孤儿进程最终都会被systemd收养。systemd作为养父有一个重要承诺:它会周期性地调用wait()清理被收养子进程的退出状态,所以被systemd收养的进程不会变成僵尸。这也是为什么孤儿进程通常被认为是无害的——它们活着时正常工作,退出时也有人负责善后。
我们用一段C代码在Deepin上演示这个过程:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void) {
pid_t child = fork();
if (child == 0) {
// 子进程:睡眠30秒,期间父进程先退出,自己变成孤儿
printf("子进程 PID=%d, 原父进程 PID=%d\n", getpid(), getppid());
sleep(30);
printf("子进程仍存活, 现在的父进程 PID=%d\n", getppid());
_exit(0);
}
// 父进程立即退出,制造孤儿
printf("父进程 PID=%d 即将退出\n", getpid());
return 0;
}编译运行后,子进程第一次打印的父进程PID是原父进程,而30秒后第二次打印时会发现getppid()的值变成了1(或者在Deepin的用户会话中,可能是用户级systemd实例的PID)。这直观地展示了收养过程。你还可以另开一个终端,用ps -o pid,ppid,cmd -p 子进程PID观察PPID的变化。
Deepin桌面环境下的特殊情况:用户级systemd
Deepin基于Debian,使用systemd作为初始化系统,但桌面会话中还有一个容易被忽略的细节:用户登录后,systemd会为每个用户启动一个用户级实例,进程名通常为systemd --user。在较新的systemd版本中,通过用户会话启动的进程如果成为孤儿,往往会被用户级systemd收养,而不是直接挂到PID 1下面。所以在Deepin终端里做上面的实验时,看到的PPID可能不是1,而是用户级systemd的PID,这不是异常,恰恰是PR_SET_CHILD_SUBREAPER机制在起作用。
此外,Deepin桌面环境本身(dde-session 等组件)也大量使用了子进程收养相关机制。例如deepin-terminal、dde-desktop等进程退出时,它们的子进程是否被正确接管,直接关系到桌面会话的稳定性。systemd的单元配置中也提供了相关工具,比如设置KillMode=process时只有主进程被杀死,子进程会被init收养继续运行,这在调试服务行为时要特别留意,否则可能出现服务重启了但旧子进程还残留的情况。
排查这类问题时,可以使用以下命令组合:
# 查看进程树,直观看到父子关系 ps -ejH # 或者使用pstree,按树形展示 pstree -p | grep -i systemd # 查找僵尸进程 ps -eo pid,ppid,stat,cmd | awk '$3 ~ /Z/' # 查看某个进程的收养情况 cat /proc/某PID/status | grep -i ppid
孤儿进程常见误区与实践建议
第一个常见误区是把孤儿进程当成问题去“消灭”。实际上,设计良好的守护进程正是故意利用孤儿机制实现的:经典做法是连按两次fork(),让中间的父进程立即退出,使孙进程成为孤儿被init收养,从而脱离终端会话、避免持有控制终端。很多系统服务的历史实现都基于这个技巧。所以看到PPID为1的进程,不要条件反射地认为出了故障。
第二个误区是以为被收养的进程就绝对不会产生问题。虽然systemd会负责回收,但如果你的程序中父进程频繁产生大量短命子进程又频繁退出,会导致收养操作反复发生,增加内核开销;更糟的情况是父进程没有正确清理子进程就退出,把本应由自己处理的逻辑丢给了systemd。建议在编写多进程程序时,父进程在退出前主动wait()所有子进程,或者使用SIGCHLD信号处理器批量回收:
#include <signal.h>
#include <sys/wait.h>
#include <unistd.h>
void sigchld_handler(int sig) {
// 循环waitpid回收所有已退出的子进程
while (waitpid(-1, NULL, WNOHANG) > 0)
;
}
int main(void) {
struct sigaction sa;
sa.sa_handler = sigchld_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;
sigaction(SIGCHLD, &sa, NULL);
// 后续fork的子进程退出时会自动被回收
pause();
return 0;
}第三个建议是善用现代内核工具。5.3之后的内核提供了pidfd机制,配合pidfd_open()和pidfd_send_signal(),可以安全地对进程发信号而不怕PID被复用;systemd则提供了systemd-run --scope来把进程纳入cgroup管理,即使进程成为孤儿,其资源统计和清理仍然有迹可循。在Deepin上排查进程问题时,结合systemctl status查看cgroup归属,往往比单纯看PPID更能说明进程的真正管理者是谁。
总结一下,Deepin系统中孤儿进程的收养由内核的forget_original_parent()驱动,优先在同线程组和设置了SUBREAPER属性的祖先中寻找养父,最终兜底到PID 1的systemd。理解这套机制,不仅能帮你正确诊断进程树异常,也能让你在编写守护进程和多进程程序时做出更稳妥的设计决策。