信号具有处理方式、掩码和投递规则
信号依据内核规定的投递规则,把事件通知给进程或线程。信号处理方式可以选择默认动作、安装处理函数,或者在允许时忽略;线程掩码则能阻塞符合条件的信号,让它们暂时保持待处理。当程序似乎漏掉重复通知,或者没有按预期停止时,需要分别检查处理方式、阻塞状态和待处理状态,不能把这些不同层面统称为“有没有收到信号”。 坏掉的样子 普通信号不是可靠事件计数器。同一种标准信号已经处于待处理状态时,随后多次发生可能合并。如果应用把每次 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,向自己发送五次,再确认它待处理。一次同步等待取走信号后,第二次零超时等待没有结果。先阻塞再发送,让这个对照具有确定性。它证明的是待处理普通信号的合并规则,而不是根据异步回调碰巧打印几次,去猜测信号是否丢失或程序调度是否足够快。 ...