为什么 Python 子进程无法正确捕获信号?

Why Python subprocesses won't properly capture signals?

让我们有一个应该捕获(并忽略)SIGTERM 信号的小程序:

# nosigterm.py:

import signal
import time

def ignore(signum, frame):
    print("Ignoring signal {}".format(signum))


if __name__ == '__main__':
  signal.signal(signal.SIGINT, ignore)
  signal.signal(signal.SIGTERM, ignore)

  while True:
    time.sleep(2)
    print("... in loop ...")

当从另一个 python 脚本作为子进程执行时,发送 SIGTERM 会终止该子进程,我觉得这很奇怪:

# parent_script.py:

import signal
import subprocess
import sys

args = [sys.executable, "nosigterm.py"]
prog = subprocess.Popen(args)
assert prog.poll() is None

prog.send_signal(signal.SIGTERM)
print("prog.poll(): {}".format(prog.poll()))
assert prog.poll() is None, "Program unexpectedly terminated after SIGTERM"

输出为:

$ python3 parent_script.py 
prog.poll(): None
Traceback (most recent call last):
  File "parent_script.py", line 13, in <module>
    assert prog.poll() is None, "Program unexpectedly terminated after SIGTERM"
AssertionError: Program unexpectedly terminated after SIGTERM

你知道为什么会这样吗?

请注意,如果 nosigterm.py 作为一个独立的 python 脚本执行(python3 nosigterm.py)并且 SIGTERM 由系统 kill 命令(在另一个终端)发送,它行为应有:

$ python3 nosigterm.py 
... in loop ...
... in loop ...
Ignoring signal 15
... in loop ...
... in loop ...
... in loop ...

我已经尝试了三个 python 版本(2.7、3.6 和 3.7)和两个 Linux 操作系统(CentOS 7 和 Debian 9),结果都是一样的。如果我将 nosigterm.py 替换为用 C 编写的捕获 SIGTERM 的二进制应用程序(通过 sigaction()),行为仍未改变,因此它必须以某种方式与父 python 进程相关。

另请注意,Popen 参数 restore_signals=True/Falsepreexec_fn=os.setsid/os.setpgrp 也没有进行任何更改。

如果有人能帮助我理解这一点,我将不胜感激。谢谢。

这是竞争条件。

您正在分叉并立即发送信号,因此 child 进程正在争先恐后地在它被杀死之前忽略它。

此外,您的 parent 脚本在检查脚本是否已终止时存在竞争条件。您向脚本发出信号并立即检查它是否已死,因此这是 child 在检查发生之前死亡的竞赛。

如果您在发送信号之前添加 time.sleep(1),您将确保 child 赢得比赛,因此您会得到您期望的行为。