进程组选择信号接收者,不等于进程家族树
进程组是内核维护的一组进程,通过进程组编号 PGID 标识,可用于向一个作业投递信号等操作。父子关系与组成员关系是两个维度:有亲缘关系的进程可以属于不同组,组长也不必始终存活,其他成员仍可继续属于原组。理解这些关系,才能判断一个控制动作到底覆盖哪些进程,以及某个进程退出意味着什么。 坏掉的样子 向单个进程发信号,不会自动向它的所有子进程发信号。向进程组发信号,选择的是当前组成员,也不是整棵后代树。创建新会话或新组的子进程,可能已经位于目标范围之外;反过来,多个兄弟进程可以刻意共享一个组,即使任何一个都不是另一个的父进程。进程树图不能直接替代组成员清单。 组投递也不保证所有接收者作出相同响应。一个成员可能采用默认动作,另一个安装处理函数,还有成员可能忽略或阻塞允许处理的信号。因此,组操作不会自动形成原子停止,也不能证明组内全部工作完成。每个进程的信号处理方式和应用关停协议仍然决定最终结果,需要分别观察投递、响应与退出。 组长退出不等于整个组退出。只要还有成员,PGID 就可能继续有效,即使组长已经被父进程回收。只观察组长状态的监督程序,可能在成员仍运行时报告结束。组身份应与实际生命周期管理结合,不能被当作一批启动工作永久不变的标识,也不能单凭领导进程消失就推断所有资源都已释放。 快速验证 把脚本保存为 groups.py,在具有 Python 3.11 或更新版本的一次性 Linux 环境中运行 python3 groups.py。实测使用 Linux 6.12.76 和 Python 3.11.2。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 和会话编号,而不是仅根据请求参数推测安排已经生效,这让实验身份与操作目标可以直接核对。 ...