进程组是内核维护的一组进程,通过进程组编号 PGID 标识,可用于向一个作业投递信号等操作。父子关系与组成员关系是两个维度:有亲缘关系的进程可以属于不同组,组长也不必始终存活,其他成员仍可继续属于原组。理解这些关系,才能判断一个控制动作到底覆盖哪些进程,以及某个进程退出意味着什么。

零号信号可以在不投递信号的情况下检查存在性与权限,但不是存活或完成确认。僵尸进程仍可能存在,检查之后目标状态也可能变化。因此,应保留本例通过等待回收确认完成的步骤,不能用一次成功的权限探测替代退出结果,更不能据此判断业务工作已经安全结束。

坏掉的样子

向单个进程发信号,不会自动向它的所有子进程发信号。向进程组发信号,选择的是当前组成员,也不是整棵后代树。创建新会话或新组的子进程,可能已经位于目标范围之外;反过来,多个兄弟进程可以刻意共享一个组,即使任何一个都不是另一个的父进程。进程树图不能直接替代组成员清单。

组投递也不保证所有接收者作出相同响应。一个成员可能采用默认动作,另一个安装处理函数,还有成员可能忽略或阻塞允许处理的信号。因此,组操作不会自动形成原子停止,也不能证明组内全部工作完成。每个进程的信号处理方式和应用关停协议仍然决定最终结果,需要分别观察投递、响应与退出。

组长退出不等于整个组退出。只要还有成员,PGID 就可能继续有效,即使组长已经被父进程回收。只观察组长状态的监督程序,可能在成员仍运行时报告结束。组身份应与实际生命周期管理结合,不能被当作一批启动工作永久不变的标识,也不能单凭领导进程消失就推断所有资源都已释放。

快速验证

把脚本保存为 groups.py,在具有 Python 3.11 或更新版本的一次性 Linux 环境中运行 python3 groups.py。本轮在 Linux 6.18.44 和 Python 3.12.14 上重新执行了该样本。process_group 选项在执行程序前安排进程组,start_new_session 请求新会话。所有收到信号的进程,都由此样本创建:

import os,signal,subprocess,sys

worker=r'''
import os,signal,sys
if sys.argv[1]=='ignore-term':signal.signal(signal.SIGTERM,signal.SIG_IGN)
signal.signal(signal.SIGUSR1,lambda *_:print('USR1',flush=True))
print(os.getpid(),os.getpgrp(),os.getsid(0),flush=True)
while True:signal.pause()
'''
children=[]
def launch(role,**settings):
    process=subprocess.Popen([sys.executable,'-c',worker,role],stdout=subprocess.PIPE,stderr=subprocess.PIPE,text=True,**settings)
    children.append(process)
    identity=tuple(map(int,process.stdout.readline().split()))
    assert len(identity)==3 and identity[0]==process.pid
    return process,identity
try:
    leader,first=launch('default',process_group=0)
    member,second=launch('ignore-term',process_group=leader.pid)
    outsider,third=launch('default',start_new_session=True)
    group=leader.pid
    assert first[1]==second[1]==group and first[2]==second[2]==os.getsid(0)
    assert third==(outsider.pid,outsider.pid,outsider.pid)
    assert group!=os.getpgrp() and third[1]!=group
    print('two sibling children share a group; another sibling starts a separate session and group')
    os.killpg(group,signal.SIGTERM)
    assert leader.wait(timeout=5)==-signal.SIGTERM
    assert member.poll() is None and outsider.poll() is None
    assert os.getpgid(member.pid)==group
    print('group TERM terminates the default recipient, leaves the ignoring member alive, and excludes the other group')
    os.killpg(group,signal.SIGUSR1)
    assert member.stdout.readline().strip()=='USR1'
    print('the group remains addressable after its leader has exited and been reaped')
    os.killpg(group,signal.SIGKILL)
    assert member.wait(timeout=5)==-signal.SIGKILL
    assert outsider.poll() is None
    outsider.terminate();assert outsider.wait(timeout=5)==-signal.SIGTERM
    print('cleanup reaps all three children; signaling a group did not signal the whole family tree')
finally:
    for process in children:
        if process.poll() is None:process.kill();process.wait()

第一个子进程新建进程组,PGID 等于自己的 PID。第二个子进程在控制进程所在会话中加入该组。第三个子进程创建独立会话,同时成为会话首领和进程组长。脚本读取真实 PID、PGID 和会话编号,而不是仅根据请求参数推测安排已经生效,这让实验身份与操作目标可以直接核对。

三个进程都是同一控制进程启动的兄弟。前两个共享信号目标,第三个不在其中,因此能明确展示父子关系与组成员关系的区别。脚本还确认目标 PGID 不等于控制进程自己的组,避免组操作意外命中发起实验的进程。这里控制的是自己创建并已确认身份的对象,没有使用猜测的外部进程编号。

向共享组发送 TERM,采用默认处理方式的第一个子进程终止。第二个刻意忽略 TERM,继续存活;组外子进程同样仍运行。控制进程等待并回收第一个子进程,再确认存活成员仍以已退出组长的 PID 作为 PGID。这个结果直接说明,领导进程的生命周期与组中最后一个成员的生命周期并不相同。

之后向该组发送 USR1,存活成员安装的处理函数打印确认消息,证明组长退出后进程组仍可寻址。对于这次请求,样本检查具体处理函数的响应,没有只依据发送调用成功就推断应用已经执行动作。信号可以作为触发机制,但需要业务确认时,应另外设计可观察结果。

最后,KILL 终止剩余组成员,独立会话里的子进程仍然存活。控制进程单独终止第三个子进程,并回收三者,断言失败时也会清理。这验证了选择性组投递和不同处理方式,不需要控制终端,也没有向宿主机无关进程发送信号。实验完成是通过等待实际退出确认,而不是发完信号就结束。

分清会话、终端与监督关系

setpgid 修改成员关系时,受会话和进程状态限制。setsid 为符合条件的调用者创建新会话和进程组,现有进程组长不能直接调用成功。创建会话不会改变父进程是谁。重新归属父进程、关联终端与选择进程组,是运行管理中相关的概念,却不是一个可以相互替换的动作,不能把它们都笼统叫作脱离父进程。

启用作业控制的交互 shell,通常把管道放在独立组中,并设置终端的前台 PGID。终端驱动把配置的控制字符信号投递给前台组。非交互 shell 并不普遍为每条管道创建独立组,因此需要检查真实启动模式,而不能把交互终端的行为直接套到所有脚本、服务和容器入口上。

信号权限仍按接收进程判断。Linux 文档中的组发送成功,只表示至少向一个符合条件的进程投递,并不是每位成员都已确认。需要确定完成时,应使用合适的服务管理器或明确的子进程管理协议,并观察退出或应用确认。组目标有助于控制作业,却不能替代追踪真实成员及其最终结果。

来源

  • setpgid(2),进程组成员、继承与会话限制,已核验 2026-10-15。
  • setsid(2),创建会话和进程组以及组长限制,已核验 2026-10-15。
  • kill(2),面向进程组的投递、权限检查与成功语义,已核验 2026-10-15。
  • credentials(7),进程编号、进程组、会话与控制终端,已核验 2026-10-15。