Python的subprocess.call无法正确处理信号

3
(我正在使用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)

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


Popen 也不会等待进程完成,所以不确定它如何工作。 - Padraic Cunningham
@PadraicCunningham 我从命令行运行了test2.py,并使用Ctrl-C终止了它。对于Popen,我使用的是"subprocess.Popen(cmd).wait()",因此它会等待cmd执行完成。 - Erika L
顺便提一下,从subprocess.call()的实现来看,子进程的信号处理特性将被阻止是有道理的。如果根本没有打印任何东西,我会理解的,但奇怪的是它打印了一些东西(在我的情况下是0),但并非全部。 - Erika L
你使用的是什么操作系统? - Padraic Cunningham
Linux(通过Windows下的PuTTy登录) - Erika L
我猜在父进程等待子进程完成(subprocess.call())的情况下,当父进程关闭时,它会发送 SIGKILL 信号给子进程。它打印0的原因可能是在发送 SIGKILL 之前发送了一些东西(你可以打印出来看看),这给了子进程一个非常短的清理机会... - zvone
2个回答

7
Python 3.3版本的subprocess.call实现在其wait被中断时向其子进程发送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接口时引入了这种行为。我认为这是不正确的,我已经报告了一个错误。


是的。我认为原帖作者已经知道如何直接使用 Popen 。至于自定义SIGINT处理程序,您想要它执行什么操作?原帖作者可能希望使用Ctrl-C向整个进程组发送SIGINT,并重要的是,中断 wait - pilcrow
@pilcrow 非常感谢您的解释。我已经用 subprocess.Popen(cmd).wait() 替换了 subprocess.call(cmd),并解决了这个问题。是的,我的目标是使用 Ctrl-C 来杀死父进程,并使所有子进程优雅地退出。 - Erika L
@pilcrow:什么也不做(signal(SIGINT, lambda *a: None)),如果子进程在SIGINT上退出(SIGINT已发送到整个进程组),则不需要中断.wait()。但是,如果您可以消除所有.call()调用(包括第三方代码),那么Popen().wait()可能更可取。 - jfs
啊,也许在信号处理程序中使用os._exit()会更好。这样可以产生预期的行为-我们可能也希望父进程终止。无论哪种方式,都是一种非常特殊的解决不愉快情况的方法。 - pilcrow
我不认为我表达清楚了 - OP可能确实希望等待被中断。但是,是的,这将进入推测的领域。 - pilcrow
显示剩余2条评论

2
更新:这个Python 3回归问题将在Python 3.7中得到解决,通过PR#5026。有关更多背景和讨论,请参见bpo-25942和(被拒绝的)PR#4283

我最近也遇到了这个问题。@pilcrow给出的解释是正确的。

OP(在评论中)提供的解决方案(Popen(* popenargs,** kwargs).wait())对我来说不够,因为我不能百分之百确定子进程在所有情况下都会响应SIGINT。我仍然希望它能在超时后被杀死。

我最终选择了简单地重新等待子进程(带有超时)。

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

从技术上讲,这意味着我可能会延长孩子的生命周期超出原始的timeout,但这比错误的清理行为要好。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接