为什么 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/False
或 preexec_fn=os.setsid/os.setpgrp
也没有进行任何更改。
如果有人能帮助我理解这一点,我将不胜感激。谢谢。
这是竞争条件。
您正在分叉并立即发送信号,因此 child 进程正在争先恐后地在它被杀死之前忽略它。
此外,您的 parent 脚本在检查脚本是否已终止时存在竞争条件。您向脚本发出信号并立即检查它是否已死,因此这是 child 在检查发生之前死亡的竞赛。
如果您在发送信号之前添加 time.sleep(1)
,您将确保 child 赢得比赛,因此您会得到您期望的行为。
让我们有一个应该捕获(并忽略)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/False
或 preexec_fn=os.setsid/os.setpgrp
也没有进行任何更改。
如果有人能帮助我理解这一点,我将不胜感激。谢谢。
这是竞争条件。
您正在分叉并立即发送信号,因此 child 进程正在争先恐后地在它被杀死之前忽略它。
此外,您的 parent 脚本在检查脚本是否已终止时存在竞争条件。您向脚本发出信号并立即检查它是否已死,因此这是 child 在检查发生之前死亡的竞赛。
如果您在发送信号之前添加 time.sleep(1)
,您将确保 child 赢得比赛,因此您会得到您期望的行为。