python subprocess.call 没有正确处理信号

python subprocess.call doesn't handle signal correctly

(我使用的是 Python 3.4.2) 我有一个脚本 test.py,它处理 SIGTERM 等。但是,当它被其他脚本调用时,信号处理不正确。

这是test.py:

#! /path/to/python3
import time
import signal
import sys

def handleSIG(signal, frame):
    for i in range(10):
        print(i)
    sys.exit()

for sig in [signal.SIGTERM, signal.SIGINT, signal.SIGQUIT, signal.SIGHUP]:
    signal.signal(sig, handleSIG)

time.sleep(30)

如果我只调用 "test.py" 并执行 "Ctrl+C",那么它会将 0,1,...,9 打印到控制台。但是,如果我在另一个脚本中使用 subprocess.call 调用 test.py,则只会打印 0。例如,这是另一个调用 test.py:

的脚本
import subprocess

cmd = '/path/to/test.py'
subprocess.call(cmd)

奇怪的是,使用 subproces.Popen() 可以消除此错误。

如果 wait 被中断,python 3.3 subprocess.call 实现会向其子进程发送一个 SIGKILL,这是由您的 Ctrl -C(SIGINT -> KeyboardInterrupt 异常)。

因此,您看到处理终端的 SIGINT(发送到整个进程组)的子进程与父进程的 SIGKILL 之间的竞争。

来自 python 3.3 来源,为简洁起见进行了编辑:

def call(*popenargs, timeout=None, **kwargs):
    with Popen(*popenargs, **kwargs) as p:
        try:
            return p.wait(timeout=timeout)
        except:
            p.kill()
            p.wait()
            raise

将此与 python 2 实现进行对比:

def call(*popenargs, **kwargs):
    return Popen(*popenargs, **kwargs).wait()

多么令人不快的惊喜。看起来这种行为是在 3.3 中引入的,当时 waitcall 接口被扩展以适应超时。我觉得这不正确,我 filed a bug.

更新: 此 Python-3 回归将通过 PR #5026. For additional background and discussion, see bpo-25942 and (rejected) PR #4283 在 Python 3.7 中得到修复。 =15=]


我最近 运行 自己陷入了这个问题。 @pilcrow的解释是正确的

仅使用 Python 2 实现 (Popen(*popenargs, **kwargs).wait()) 的 OP 解决方案(在评论中)对我来说不够,因为我不是 100% 确定 child 将在所有情况下响应 SIGINT。我仍然希望它在超时后被杀死。

我简单地选择 re-waiting 作为 child(有超时)。

def nice_call(*popenargs, timeout=None, **kwargs):
    """
    Like subprocess.call(), but give the child process time to
    clean up and communicate if a KeyboardInterrupt is raised.
    """
    with Popen(*popenargs, **kwargs) as p:
        try:
            return p.wait(timeout=timeout)
        except KeyboardInterrupt:
            if not timeout:
                timeout = 0.5
            # Wait again, now that the child has received SIGINT, too.
            p.wait(timeout=timeout)
            raise
        except:
            p.kill()
            p.wait()
            raise

从技术上讲,这意味着我有可能将 child 的生命周期延长到原来的 timeout 之外,但这比不正确的清理行为要好。