信号依据内核规定的投递规则,把事件通知给进程或线程。信号处理方式可以选择默认动作、安装处理函数,或者在允许时忽略;线程掩码则能阻塞符合条件的信号,让它们暂时保持待处理。当程序似乎漏掉重复通知,或者没有按预期停止时,需要分别检查处理方式、阻塞状态和待处理状态,不能把这些不同层面统称为“有没有收到信号”。
坏掉的样子
普通信号不是可靠事件计数器。同一种标准信号已经处于待处理状态时,随后多次发生可能合并。如果应用把每次 SIGUSR1 都理解为一个排队任务,就可能丢失这个计数关系。实时信号具有排队语义,但资源仍然有限,也需要明确的应用协议;不能仅因为名字里有实时,就假定它适合无限量承载业务消息。
信号名称不保证应用功能。某些守护进程约定 SIGHUP 表示重载,但它的默认动作是终止。SIGTERM 默认也会终止,只有应用实现了相应行为,它才成为优雅关停请求。发送成功不能证明配置已经重载,也不能证明在途工作已经排空。控制方必须观察目标实际响应,而不是把投递结果当作业务操作结果。
SIGKILL 与 SIGSTOP 不能捕获、忽略或阻塞。KILL 不给应用执行清理的机会,但也不应被描述为在每种内核状态下都保证瞬时释放资源。监督者仍需观察实际完成。使用符号名称也更清晰,不能假定所有平台上的信号编号都完全一致;信号数字与进程退出状态还可能由工具采用不同方式表示。
快速验证
把下面脚本保存为 signals.py,在一次性 Linux 环境中运行 python3 signals.py。原始记录使用 Linux 6.12.76 和 Python 3.11.2;本轮在 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,向自己发送五次,再确认它待处理。一次同步等待取走信号后,第二次零超时等待没有结果。先阻塞再发送,让这个对照具有确定性。它证明的是待处理普通信号的合并规则,而不是根据异步回调碰巧打印几次,去猜测信号是否丢失或程序调度是否足够快。
接着在阻塞状态下发送三次 SIGRTMIN,三次等待分别取到一个,之后才为空。这通过同步消费直接比较排队规则。实验没有测量队列容量、不同实时信号编号之间的优先级,也没有验证应用负载随信号传递。正文只依据实际覆盖的现象解释,避免把基本排队对照扩大成完整消息系统保证。
尝试忽略 KILL 和 STOP,均得到 EINVAL;尝试把它们加入阻塞掩码,实际掩码中也没有这些成员。这里区分处理方式修改被拒绝,与掩码请求里不可阻塞成员被忽略两种行为。两种操作都不会让应用获得控制这两个信号的能力,因此不能把函数返回方式相似或不同当作信号已经被阻塞的证明。
随后四个子进程案例分离投递和清理。默认 HUP 与默认 TERM 都终止子进程,不会生成 Python finally 中的标记。明确安装 TERM 处理函数的子进程通过 Python 退出,并写出标记。最后一个子进程具有相同 TERM 处理函数,但收到 KILL 后没有执行清理。处理一种信号,不会让它也能处理另一种不可捕获信号。
Python subprocess 用负的终止信号编号表示信号终止,主动处理 TERM 的案例则正常返回零。这是 Python 的进程状态表示,不是所有 shell 通用的退出码规则。脚本同时检查状态与标记,避免只凭其中一项推断清理发生。真实应用也应把控制动作、终止原因和工作完成结果分别观察。
把工作放到适合的执行上下文
原生信号处理函数中,只适合执行异步信号安全操作。普通日志、内存分配以及许多库调用,可能在程序被中途打断时死锁或破坏状态。常见设计是记录最小通知,再让正常执行流程完成协调关停;具体通知机制应匹配运行时和受支持的事件循环,而不是在信号处理函数里直接处理所有业务善后。
Python 处理函数通过解释器信号机制在主线程执行,不是在底层 C 处理函数里直接运行任意 Python。这解释了小型 TERM 案例为何可以抛出 SystemExit,却不能据此在 C handler 中做任意工作,也不能保证长时间运行的原生操作会及时让出控制,使 Python 清理立刻执行。运行时边界是解释实验结果的一部分。
控制服务时,应定义预期响应、观察机制和合适期限。重载要能证明新配置生效;关停要按策略停止接入、完成或放弃在途工作,并报告结束。升级信号会改变终止行为,却不能补回应用从未实现的清理逻辑。正确监督依赖实际结果,而不是仅依赖一次投递成功或一个听起来温和的信号名字。
来源
- signal(7),默认动作、掩码、普通信号合并与实时信号队列,已核验 2026-10-15。
- signal-safety(7),原生信号处理函数的异步信号安全限制,已核验 2026-10-15。
- Python 3.11 signal documentation,Python 处理函数执行、同步等待与信号掩码,已核验 2026-10-15。
- kill(2),信号投递权限与返回语义,已核验 2026-10-15。