实验先阻塞信号:五次 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
文章历史样本中,子进程软值为 32,编号 0 到 31 的表项已占用,得到 EMFILE;耗尽前建立的管道向仍能打开文件的控制进程报告。

在实际失败的进程中诊断描述符耗尽

进程使用文件描述符访问的不只是普通文件。网络套接字、管道、事件接口和复制出来的引用,都要占用描述符表中的入口。RLIMIT_NOFILE 约束新分配可以使用的编号,其软值比允许的最大编号大一。因此,即使机器仍有充足磁盘空间和内存,服务也可能因为自己的表项额度用尽而无法继续接受工作。排查时必须先确认耗尽的是哪一种资源。 检查真正失败的进程。EMFILE 属于进程边界,ENFILE 属于系统边界;预先打开的控制通道可在新分配失败时继续工作。 坏掉的样子 EMFILE 表示调用进程触及适用的描述符限制,ENFILE 则表示系统打开文件资源耗尽。这两种错误需要沿不同方向调查。提高机器级文件设置,不会自动改变失败进程的软值;扩大某个应用的限制,也不能修复系统级资源短缺。把日志里的错误名和实际限制对应起来,比看到“文件太多”就随意调整参数更有用。 耗尽还会破坏诊断。打开日志文件、枚举目录本身,也可能需要新的描述符。如果应用直到额度用光后才尝试读取 /proc/self/fd,检查动作就可能一起失败。已经存在的描述符通常仍可使用,因此预先建立的控制管道有价值:它让进程无需再分配资源,也能够报告当前错误,而不会把诊断失败误认为路径消失。 描述符数量持续增长,需要结合上下文解释。它可能反映正常并发增加、连接池逐渐填满,也可能是某些所有者忘记关闭引用。曲线本身不能证明泄漏。应同时比较描述符类型、工作负载,以及工作排空后的数量,再沿超出预期寿命的引用追踪所有权。只有把数量变化与资源生命周期关联起来,才能选择正确修复。 快速验证 把下面脚本保存为 exhaustion.py,在一次性 Linux 环境中运行 python3 exhaustion.py。此前记录的实测使用 Linux 6.12.76 和 Python 3.11.2。这里的描述符版本改用管道代替套接字,并为等待就绪设置截止时间。控制进程启动独立子进程,后者只把自身软上限降到三十二;继承的软硬值原本都必须允许这个设置: ...

十月 27, 2026 · 1 分钟 · XB Field Notes