ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Linux进程间关系详解:从PPID、进程组到会话与僵尸进程

Linux进程间关系详解:从PPID、进程组到会话与僵尸进程 Linux下一个特别容易让人忽视、但每次出故障都跳出来刷存在感的概念就是进程间关系。和同事讨论问题时我经常发现一个现象很多人对ps、top这类命令背得滚瓜烂熟但一提到PPID、PGID、SID这几个字段就开始含糊甚至有人把“进程间关系”和“进程间通信”混为一谈。进程间关系解决的其实不是“进程之间怎么传数据”而是“谁是谁的爹、谁归哪个组管、谁绑定在哪个终端上”。搞清楚这些系统卡死、服务莫名其妙退出、脚本误杀进程这类问题排查起来会顺手很多。这篇文章就围绕 Linux 进程间的父子关系、进程组、会话、控制终端这几个核心点把底层机制和落地实战一起聊透。1. 进程间关系到底是什么先把它和 IPC 分清1.1 进程关系不是通信关系而是组织关系很多朋友刚接触这个概念时会下意识把“进程间关系”理解为共享内存、消息队列、Socket 那套东西。但其实那是另一个大主题叫 IPCInter-Process Communication也就是进程间通信。而 Linux 的进程间关系说的是进程之间的“组织架构”和“血缘关系”。做运维和开发平时最常碰到的进程间关系可以拆成四层父子关系一个进程通过fork()创建出子进程子进程的PPID就是父进程的PID。这是最直接的亲情关系。兄弟关系同一个父进程 fork 出来的多个子进程互为兄弟它们之间有独立的数据空间但共享一些文件描述符之类的资源。进程组关系一组由同一终端或同一作业产生的进程可以被归到同一个进程组。发信号时可以对整个组操作不必一个个找 PID。会话关系会话是进程组的上一级组织。一次用户登录、一个终端窗口启动的整套进程通常在同一个会话里。控制终端、前后台作业都与会话密切相关。你可以把这种关系理解成一家公司父进程是部门经理子进程是下属进程组是一个部门会话是整家公司而控制终端就是公司对外的那扇门。部门里的人可以不断变化但公司框架还在门关了整家公司可能都要停摆。1.2 先看一段真实的排障经历我之前处理过一起线上故障服务是一个 Java 网关前面还挂着 Nginx。正常情况下两个进程各自独立通过端口通信。可某天有人发布脚本时加了一段“清理所有残留进程”的逻辑直接用pkill -f java结果把同一台机器上部署的另一个兄弟项目的 Java 进程也一起干掉了。那个兄弟项目其实是同一个父进程拉起的一组进程组内的兄弟进程。还有一次一个定时任务通过 SSH 执行远程脚本脚本里用nohup启动了一个后台任务但没重定向标准输入。运维第二天发现远程服务还在线可后台任务已经挂着不动了日志里全是“cannot access tty”之类的报错。说到底是因为后台进程还留在原来的会话和控制终端下面终端一变它就懵了。这两次故障给我一个很深的印象理解进程间关系不是纯粹为了面试背概念而是排查线上问题时绕不开的基本功。后面遇到什么诡异现象我都会先用ps -o pid,ppid,pgid,sid,tty看一眼进程全貌再谈怎么处理。2. 血缘起点fork 与 exec 如何定义父与子2.1 fork 是进程诞生的起点Linux 里除了内核手动启动的第一个进程也就是 PID 为 1 的 init/systemd 进程之外其他所有进程几乎都是通过fork()出来的。fork()的含义很形象做一个“细胞分裂”当前进程在调用处分裂出一份几乎一样的子进程。写个最简单的 Python 示例import os print(before fork, pid , os.getpid()) pid os.fork() if pid 0: print(child process, pid , os.getpid(), , ppid , os.getppid()) else: print(parent process, pid , os.getpid(), , child pid , pid)运行后你会看到fork()在父进程里返回子进程的 PID在子进程里返回 0。两次输出里子进程的getppid()就是父进程的getpid()。这就是父子关系最底层的来源。一个容易被忽略的点是fork()之后父子进程不是简单复制了对方的内存而是暂时共享物理内存页并且采用写时复制机制。也就是说只有某一方真正去修改数据时内核才会复制一份独立页面这样能大幅减少 fork 的开销。但它也带来一个常见误区不要指望父子进程通过普通变量通信因为它们修改的是各自独立的逻辑地址视图一个进程改了变量另一个看不到。2.2 exec 相当于换了个“灵魂”fork()只是复制进程复制出来的子进程一开始执行的代码和父进程一模一样。但实际用命令行敲一条ls -l的时候shell 不可能把一堆ls的代码复制进去再跑它要做的是让子进程去执行另外一个全新的程序这就是exec系列系统调用干的事。在 Linux 中execve()会把当前进程的代码段、数据段、堆、栈整个替换成新程序的映像但从进程关系角度看这个进程的 PID、PPID、PGID、SID 全都不变。也就是说子进程只是“换了灵魂身份证没换”。所以真实的命令启动过程通常是用户输入命令 - bash fork 出一个子进程 - 子进程调用 execve 执行 /usr/bin/ls - 新程序覆盖子进程映像 - ls 进程的 PPID 依旧是 bash这也是为什么你在pstree里能看到 bash 下挂着许多命令进程而不是直接看到“两个 bash”。有个运维细节值得留意因为exec不会改变进程关系字段所以如果你用SID、PGID来识别进程身份即使进程执行的程序已经换了好几个它仍然属于原来的进程组和会话。攻击者或故障进程利用这个特性可以把自己“洗白”但从排障角度说这反而方便我们通过进程组把整个链路捞出来。2.3 子进程退出后父进程有义务“收尸”子进程终止后并不会立刻从系统里完全消失它会保留一个“僵尸”状态直到父进程调用wait()或waitpid()获取它的退出状态。如果父进程一直不回收僵尸进程就会一直占据进程表里的一个槽位虽然它不占用 CPU 和内存但数量多了会导致系统无法创建新进程。下面第三大部分会细讲僵尸进程这里先记住一个点父进程对子进程的“收尸”是必须履行的义务。写多进程程序时不管子进程是被正常exit()还是收到信号终止父进程都应该调用wait()去处理。很多线上僵尸进程堆积的问题归根结底是程序的异常分支没做好回收。3. 进程组与会话从血缘到“组织编制”3.1 进程组究竟是怎么形成的单靠父子关系还不足以解释终端上各种进程的行为。比如我们执行ping baidu.com | grep time 这条命令会启动两个进程ping和grep。它们可能是同一个 shell fork 出来的兄弟进程但 shell 还要把它们归到同一个进程组以便一起前后台切换、一起接收终端信号。这个进程组 ID 一般用组内第一个进程的 PID 表示。查看进程组最直接的方法是ps加几个输出列ps -eo pid,ppid,pgid,sid,tpgid,tty,stat,cmd你可能当场看到类似这样的结果PID PPID PGID SID TPGID TT STAT CMD 1000 999 1000 1000 1000 pts/0 Ss bash 1100 1000 1100 1000 1100 pts/0 S sleep 30 1200 1000 1200 1000 1100 pts/0 S ping baidu.com在这里PGID是进程组 IDSID是会话 ID。凡是在同一个终端上通过同一命令行启动的一批进程通常具备相同的PGID而整个登录会话下的所有进程往往共享同一个SID。引号里提醒一句看到TPGID前台进程组 ID时别忽略它。它代表当前该终端上真正抢占前台的那个进程组 ID。前后台切换就是通过修改这个TPGID来实现的。后台进程组不会收到来自键盘的中断信号比如 CtrlC 只发给前台进程组这就是为什么后台任务常常对 CtrlC “无感”。3.2 会话用户登录到退出的全过程容器会话可以简单理解为一个或多个进程组的集合。通常用户通过 SSH 登录或者打开一个终端窗口后内核会为这次登录创建一个新的会话会话 ID 等于会话首进程的 PID。比如 SSH 登录后会话首进程通常是 bash。一个会话里可以有多个进程组一个前台进程组拥有当前控制终端。若干后台进程组不占用终端输入。如果你在交互式 shell 里敲几下jobs、fg、bg其实就是在会话内部切换前后台进程组。会话的存在让系统能够清楚知道用户退出登录时哪些进程应该收到 SIGHUP挂断信号。默认情况下会话首进程退出后内核会向控制终端对应的前台进程组发送 SIGHUP那些没有处理挂断信号的进程就会退出。很多同学在用ssh host sh start.sh时遇到过问题明明脚本里启动了后台服务可 SSH 一断开服务就跟着没了。原因就在这里后台进程仍然留在当前会话里会话关闭时被 SIGHUP 带走。想要让它活下去要么nohup忽略 SIGHUP要么setsid让它彻底开创新会话。3.3 从“家人”到“单位同事”谁也跑不出 PGID 和 SID用大白话总结一下父子关系、进程组和会话三者父子关系解决“谁生了我”排障时看到进程的PPID就能找到它是在哪个进程手上被 fork 出来的。进程组解决“我跟谁一起干活”同一批协作的进程比如管道左右两边的命令归到同一个组里方便统一调度。会话解决“我们在哪个项目组、属于哪次登录”控制终端断开时按会话为单位发信号避免把别的登录会话下的服务也一起误杀。实际写脚本时这个分层非常有用。比如你拉起了一个微服务注册中心进程下挂着一堆子进程你要停止整个服务树如果只杀父进程 PID子进程会变成孤儿如果杀会话里所有进程又可能误伤同终端下的其他工作。所以一般建议按PGID精确操作而不是按SID全端清除。4. 用命令看清关系这些常用操作你真的会用吗4.1 pstree一张族谱看清父子进程排查进程关系时我最喜欢先跑一条pstree -ap直接输出进程树pstree -ap它会以树状形式展示进程父子层级还自动加上 PID。如果只是想看某个进程下面的子孙树可以接 PIDpstree -ap 1234这种方式特别适合定位那种“父进程还活着但子进程们已经乱成一锅粥”的场景。比如 Nginx 的 master 进程下面挂着 worker 进程用pstree一眼就能区分 master 和 worker 的关系比ps -ef然后一件件去对PPID直观多了。不过pstree也有局限它默认只展示父子关系不直接展示进程组和会话归属。所以它是“血缘关系”的首选工具但遇到“为什么同一终端下的两个进程 CtrlC 只能杀掉一个”这类问题时还是得靠ps的PGID、SID列。4.2 ps 里的排障“四件套”PPID、PGID、SID、TPGID排查进程问题时我几乎每次都会写入一个固定格式ps -eo pid,ppid,pgid,sid,tpgid,tty,stat,comm四件套字段含义如下字段含义典型用途PPID父进程 PID找祖先、判断孤儿进程状态PGID进程组 ID按组统一终止、定位一个作业的完整进程集合SID会话 ID查看进程属于哪次登录/哪个终端会话TPGID前台进程组 ID判断当前终端真正的“前台作业”是哪个举个例子。你想知道某个进程到底归哪个终端管跑ps -o tty,sid,cmd -p 1234如果TTY字段是?说明这个进程没有控制终端通常是 daemon 化或者setsid过的进程。如果是pts/0说明它还在某个 SSH 终端会话下一旦终端断开它就有被挂断信号带走的可能。4.3 利用 PGID/SID 做批量操作避免误伤线上遇到需要按关系批量处理进程时我建议先查出目标进程的PGID再对组内所有进程操作。比如某个 Java 服务进程 PID 是 5678要整组终止PGID$(ps -o pgid -p 5678 | tr -d ) kill -- -$PGIDkill -- -PGID的语法是向整个进程组发送信号前面两个减号是为了避免-PGID被当成命令行选项。这种操作比pkill -f精确得多因为它严格限定在进程组范围内不会因为匹配关键词而误杀其他无关进程。再比如想找出某个父进程下所有子进程pgrep -P 1234pgrep -P专门按父进程查找输出的是所有直系子进程的 PID。如果只想看孙进程还需配合pstree或者递归查找。一个排障心得碰到“一个进程被杀后过几秒又自动复活”的情况先别急着kill -9先看它的PPID。如果父进程是一个守护进程或 systemd 服务父进程可能在不断拉起重启逻辑。光杀子进程治标不治本要处理父进程或停掉服务单元才能真正终止它。5. 三种常见异常进程僵尸、孤儿与失控的后台任务5.1 僵尸进程父进程不“收尸”的后果先看一个典型场景用脚本不停创建子进程但不等待回收#!/bin/bash while true; do sleep 1000 done这个脚本会不断 fork 出后台 sleep 进程。如果父进程一直不去wait那么已经结束的 sleep 子进程就会变成僵尸也就是ps状态列里的Zzombie。检查系统里的僵尸进程数ps -eo stat,ppid,pid,cmd | awk $1 ~ /^Z/看到僵尸后很多人第一反应是kill -9强杀。但我必须强调僵尸进程已经死了kill 根本没用它不响应任何普通信号因为内核已经完成了对它的资源回收只是在进程表里保留一个记录等父进程来取。处理僵尸的正路是处理它的父进程。如果父进程能正常处理子进程退出事件它会在下一次wait()时把僵尸清掉如果父进程本身逻辑有问题永远不会wait()那只能终止父进程让僵尸子进程被 init/systemd 收养由 PID 1 统一回收。对于 systemd 管理的服务systemctl restart服务通常就能连带清理僵尸。值得留意的是进程表里存在少量僵尸不一定会立刻出问题但如果僵尸数量持续增长就会占满 PID 上限导致新进程fork()失败报Resource temporarily unavailable。监控里看到这个错误第一反应就该查是不是有大量僵尸。5.2 孤儿进程父进程先走谁来接管和僵尸相反孤儿进程是“父进程还健在时子进程还在运行但父进程突然退出了”。此时子进程不会立刻死亡而是会被 PID 为 1 的 init/systemd 进程收养PPID变成 1。从孤儿进程的形成可以看到Linux 的进程模型其实很“有担当”不会让一个无处挂靠的进程变成无主进程而是统一交给系统初始化进程来管理。对于线上服务来说如果能接受“变成孤儿也继续跑”那这个行为其实是容错机制。但有经验的人都会告诉你不要依赖“变孤儿”来保活服务。因为孤儿进程往往还保留着原来的会话 ID如果它绑过某个已关闭终端终端断开后可能会收到 SIGHUP或者在之后访问终端设备时报错。理想的保活方案是用nohup、setsid或者交给 systemd 管理让进程主动脱离会话而不是默默变成孤儿。5.3 失控的后台任务为什么 SSH 断开服务就没了前面提过的经典场景这里展开说假设你在远程机器上执行ssh host sleep 1000 按理说 sleep 已经在后台运行了SSH 断开后 sleep 通常也会消失。原因就是 sleep 仍属于当前 SSH 会话会话退出后控制终端消失内核会给相关进程组发 SIGHUP。反过来如果你用下面方式启动ssh host nohup sleep 1000 /dev/null 21 或者ssh host setsid sleep 1000 /dev/null 21 /dev/null 那么 sleep 就能在 SSH 断开后继续存活因为nohup让它忽略 SIGHUPsetsid更是直接让它创建一个新会话。很多人在容器或 CI 环境里也会踩类似坑脚本里用启动一个后台进程脚本执行完退出后台进程跟着被清理。这时可以考虑用disown把后台作业从 shell 的作业表里移除或干脆用setsid创建新会话避免跟随父 shell 一起被信号处理。6. 把进程关系玩明白几个实战技巧和面试加分点6.1 让脚本可以准确“杀掉自己启动的整棵进程树”写部署脚本时最怕的就是只杀了父进程子进程变成孤儿继续占用端口、占用文件锁。之前遇到一个发布脚本结束服务只 kill 了主进程 PID结果后续重新发布时端口还是被占着。查了半天发现是主进程 fork 出的子进程还活着。可靠的做法是启动时就记录进程组 ID停止时按进程组清理# 启动阶段 setsid /path/to/server echo $! /var/run/server.pid # 停止阶段 PID$(cat /var/run/server.pid) PGID$(ps -o pgid -p $PID | tr -d ) kill -- -$PGID注意这里启动用setsid而不是直接因为直接有时会让进程留在当前 shell 的作业表里停止时容易误伤当前终端本身。setsid让进程开创新会话后续对它做进程组操作时风险更小。6.2 nohup、setsid、disown、systemd 到底怎么选后台化一个进程有很多工具但它们的原理和适用场景差别很大方法实际作用适用场景把任务放到当前 shell 后台临时测试不脱离终端nohup cmd 忽略 SIGHUP进程仍在原会话想抵挡终端断开但没有创建新会话setsid cmd创建新会话彻底脱离原终端让进程成为独立会话首进程disown从 shell 作业表中移除交互式 shell 中让退出时不发 HUPsystemd service完整托管进程生命周期生产环境服务最推荐其中nohup和setsid经常让人混淆。简单说nohup只是“不听挂断电话”但进程还站在原来的会话里setsid相当于直接“辞职搬走了”进了新公司。一般临时跑长任务nohup够用真正要稳定托管生产服务建议直接交给 systemd让系统负责进程生命周期、日志收集、异常重启而不是靠 nohup 硬撑。我见过不少团队生产服务还在用nohup java -jar xxx.jar 这种土办法。不是不能跑但维护成本很高进程想重启还得手动找 PID日志也没有统一收集出问题不容易追溯。如果换成 systemd unit只要写一个 service 文件KillModecontrol-group天然支持按 cgroup 停止整个服务进程树比按 PGID 还要更稳一层。6.3 面试里讲清楚这几点基本就能加分进程间关系在 Linux 面试里几乎是必考题高频问题集中在父进程和子进程的关系核心答fork()创建子进程子进程的 PPID 是父进程 PID父进程退出后子进程被 PID 1 收养。僵尸进程怎么处理先说明kill -9杀不掉僵尸再答清思路通过处理父进程或让 init 收养来回收。为什么 SSH 断开后 nohup 能让服务继续跑抓住 SIGHUP、会话、进程组这几个关键词解释 nohup 只是忽略挂断信号setsid 是彻底脱离会话。如何一次杀整棵进程树先按ps -o pgid拿到进程组 ID再kill -- -PGID或者直接通过 systemd cgroup 停止。面试官要听的不是命令拼凑而是你能解释命令背后的原理。比如同样一句kill -- -123你能说出来它是对进程组发信号那么别人就知道你真的理解进程组是什么。比如调试一个后台任务时如果你发现ps -o sid,cmd -p 123里SID和一个已退出进程的 PID 相同说明这个进程所在会话的组长已经退出。此时进程组变成孤儿进程组内核可能会向这个孤儿进程组发送 SIGHUP 和 SIGCONT目的就是让没有父进程看管的进程能及时察觉终端状态变化。这类细节普通文档很少写但实际排查时会遇到知道后处理问题会从容很多。最后再分享一个我日常定位问题的顺序先跑pstree -ap看进程的“族谱”再用ps -eo pid,ppid,pgid,sid,tpgid,tty,stat,cmd看进程的组织归属最后根据状态字段判断它是运行中、睡眠、僵尸还是不可中断。这套流程几乎覆盖了 80% 的进程关系类故障。把这些关系理清之后很多看似诡异的线上问题最后都能用“进程组”“会话”“信号”这几个词解释清楚。
返回列表