TERM 选择组 G:A 采用默认处理并退出,B 忽略 TERM 而存活。C 属于其他组,未被选中。图中结果来自文章受控样本。

进程组选择信号接收者,不等于进程家族树

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

九月 24, 2027 · 1 分钟 · XB Field Notes
正常显式退出,以及主动退出的 INT 或 TERM 处理器,汇入统一的 EXIT 清理步骤。红色 KILL 路径在清理之前被终止横杠截断。

Shell 的 trap 把信号转成一次清理动作

trap 让 Shell 为某个信号或自身事件安排动作,适合删除临时文件、协调退出,但不会让中断变成原子操作,也无法保证任何终止方式都能完成清理。在 Bash 中,trap 是内建命令,EXIT 是 Shell 事件,而不是操作系统信号。可靠的设计需要明确处理哪些出口,并保留执行为什么结束的证据,不能仅凭注册了一次处理器就断言资源一定会被回收。 这是明确的退出清理策略,不能保证删除成功或立即关停。先保存最初的状态,并单独诊断清理结果。等待同步前台命令与通过 wait 等待异步命令的陷阱时机不同。本轮没有重新运行原文示例。 信号处理与退出清理分开 trap 'cleanup' INT TERM 会替换这些信号的默认动作。如果处理器只是返回,Shell 可能继续执行;安装处理器并不天然等于启动关停。一种明确的 Bash 写法是在信号处理器中主动退出,再让统一的退出处理器负责清理。正常执行到末尾、显式调用退出命令,也会经过这个出口,从而减少各业务分支之间重复的资源回收逻辑。 ...

一月 12, 2027 · 2 分钟 · XB Field Notes
实验先阻塞信号:五次 SIGUSR1 合并为一次待处理,三次 SIGRTMIN 保持三个排队实例。实时信号的队列资源仍然有限。

信号具有处理方式、掩码和投递规则

信号依据内核规定的投递规则,把事件通知给进程或线程。信号处理方式可以选择默认动作、安装处理函数,或者在允许时忽略;线程掩码则能阻塞符合条件的信号,让它们暂时保持待处理。当程序似乎漏掉重复通知,或者没有按预期停止时,需要分别检查处理方式、阻塞状态和待处理状态,不能把这些不同层面统称为“有没有收到信号”。 线程掩码、待处理状态和处理方式分别回答不同问题。应观察目标实际响应与清理;发送成功、熟悉的信号名称或某种返回码表示,都不能单独证明工作完成。 坏掉的样子 普通信号不是可靠事件计数器。同一种标准信号已经处于待处理状态时,随后多次发生可能合并。如果应用把每次 SIGUSR1 都理解为一个排队任务,就可能丢失这个计数关系。实时信号具有排队语义,但资源仍然有限,也需要明确的应用协议;不能仅因为名字里有实时,就假定它适合无限量承载业务消息。 信号名称不保证应用功能。某些守护进程约定 SIGHUP 表示重载,但它的默认动作是终止。SIGTERM 默认也会终止,只有应用实现了相应行为,它才成为优雅关停请求。发送成功不能证明配置已经重载,也不能证明在途工作已经排空。控制方必须观察目标实际响应,而不是把投递结果当作业务操作结果。 SIGKILL 与 SIGSTOP 不能捕获、忽略或阻塞。KILL 不给应用执行清理的机会,但也不应被描述为在每种内核状态下都保证瞬时释放资源。监督者仍需观察实际完成。使用符号名称也更清晰,不能假定所有平台上的信号编号都完全一致;信号数字与进程退出状态还可能由工具采用不同方式表示。 快速验证 把下面脚本保存为 signals.py,在一次性 Linux 环境中运行 python3 signals.py。原始记录使用 Linux 6.12.76 和 Python 3.11.2;2026-10-15 的复核记录了增加就绪超时的实验在 Linux、Python 3.12.14 上通过;此次未执行信号实验。脚本只向自身和自己创建的子进程发送信号,恢复原始掩码,并把清理标记限制在临时目录中: from pathlib import Path from tempfile import TemporaryDirectory import errno,os,select,signal,subprocess,sys watched={signal.SIGUSR1,signal.SIGRTMIN} old=signal.pthread_sigmask(signal.SIG_BLOCK,watched) try: for _ in range(5):os.kill(os.getpid(),signal.SIGUSR1) assert signal.SIGUSR1 in signal.sigpending() assert signal.sigwait({signal.SIGUSR1})==signal.SIGUSR1 assert signal.sigtimedwait({signal.SIGUSR1},0) is None for _ in range(3):os.kill(os.getpid(),signal.SIGRTMIN) for _ in range(3):assert signal.sigtimedwait({signal.SIGRTMIN},0).si_signo==signal.SIGRTMIN assert signal.sigtimedwait({signal.SIGRTMIN},0) is None print('five blocked standard signals coalesce into one; three blocked real-time signals remain queued') finally:signal.pthread_sigmask(signal.SIG_SETMASK,old) for sig in [signal.SIGKILL,signal.SIGSTOP]: try:signal.signal(sig,signal.SIG_IGN) except OSError as error:assert error.errno==errno.EINVAL else:raise AssertionError('uncatchable signal accepted a disposition change') old=signal.pthread_sigmask(signal.SIG_BLOCK,{signal.SIGKILL,signal.SIGSTOP}) assert not {signal.SIGKILL,signal.SIGSTOP}&signal.pthread_sigmask(signal.SIG_BLOCK,set()) signal.pthread_sigmask(signal.SIG_SETMASK,old) print('SIGKILL and SIGSTOP cannot be ignored or added to the blocked mask') worker=r''' from pathlib import Path import signal,sys if int(sys.argv[1]):signal.signal(signal.SIGTERM,lambda *_:sys.exit(0)) try: print('ready',flush=True) while True:signal.pause() finally:Path(sys.argv[2]).write_text('cleanup') ''' with TemporaryDirectory(prefix='xbcatm-signals-') as directory: for name,sig,handled,expected,cleaned in [ ('hup-default',signal.SIGHUP,False,-signal.SIGHUP,False), ('term-default',signal.SIGTERM,False,-signal.SIGTERM,False), ('term-handled',signal.SIGTERM,True,0,True), ('kill',signal.SIGKILL,True,-signal.SIGKILL,False)]: marker=Path(directory)/name process=subprocess.Popen([sys.executable,'-c',worker,str(int(handled)),str(marker)],stdout=subprocess.PIPE,stderr=subprocess.PIPE,text=True) try: assert select.select([process.stdout], [], [], 5)[0], 'child readiness timed out' assert process.stdout.readline().strip()=='ready' os.kill(process.pid,sig);output,error=process.communicate(timeout=5) assert process.returncode==expected,(name,process.returncode,error) assert marker.exists()==cleaned,name print(name,'returncode',process.returncode,'cleanup',cleaned) finally: if process.poll() is None:process.kill();process.wait() 脚本先阻塞 SIGUSR1,向自己发送五次,再确认它待处理。一次同步等待取走信号后,第二次零超时等待没有结果。先阻塞再发送,让这个对照具有确定性。它证明的是待处理普通信号的合并规则,而不是根据异步回调碰巧打印几次,去猜测信号是否丢失或程序调度是否足够快。 ...

十一月 23, 2026 · 2 分钟 · XB Field Notes