Python 子进程和键盘中断 - 它会导致子进程没有变量吗?

问题描述 投票:0回答:2

如果从 Python 启动子进程:

from subprocess import Popen

with Popen(['cat']) as p:
    pass

是否有可能进程已启动,但由于用户按 CTRL+C 引起的键盘中断,

as p
位从未运行,但进程确实已启动?所以Python中不会有变量对进程做任何事情,这意味着用Python代码终止它是不可能的,所以它会一直运行到程序结束?

环顾Python源代码,我发现我认为进程在

__init__
调用中开始,位于https://github.com/python/cpython/blob/3.11/Lib/subprocess.py#L807,然后在 POSIX 系统上最终在 https://github.com/python/cpython/blob/3.11/Lib/subprocess.py#L1782C1-L1783C39 调用
os.posix_spawn
。如果在
os.posix_spawn
完成之后但在其返回值尚未分配给变量之前出现键盘中断,会发生什么情况?

模拟这个:

class FakeProcess():
    def __init__(self):
        # We create the process here at the OS-level,
        # but just after, the user presses CTRL+C
        raise KeyboardInterrupt()

    def __enter__(self):
        return self

    def __exit__(self, _, __, ___):
        # Never gets here
        print("Got to exit")

p = None
try:
    with FakeProcess() as p:
        pass
finally:
    print('p:', p)

这会打印

p: None
,并且打印
Got to exit

这确实表明键盘中断可以阻止进程的清理?

python subprocess signals sigint
2个回答
1
投票

我认为你不应该对此有问题。另外,您在上下文管理器中打开 Popen,因此一旦退出,缩进上下文管理器就应该自动为您正确退出 Popen 会话。

我相信它与 try 块中的finally语句相同,在键盘中断时它仍然会运行finally语句。我将在下面包含上下文管理器的文档链接,以便您可以阅读有关它的更多信息。

编辑:上下文管理器确实使用带有finally语句的try块来释放/退出。

https://docs.python.org/3/library/contextlib.html

编辑后续:

因此,如果 __init__ 未完全完成并且调用了键盘中断,则应通过垃圾收集来处理它,并且应自动调用 __del__ 函数。我在下面提供了链接。这应该会为你处理一切。

https://github.com/python/cpython/blob/e3a11e12ab912f0614a90ade7acd34dda7e7f15e/Lib/subprocess.py#L1120C7-L1120C7


0
投票

来自 https://docs.python.org/3/library/signal.html#note-on-signal-handlers-and-exception 键盘中断是由 Python 的默认 SIGINT 处理程序引起的。具体来说,它建议何时可以提出:

如果信号处理程序引发异常,该异常将传播到主线程,并且可能在任何字节码指令之后引发。最值得注意的是,键盘中断可能会在执行过程中的任何时刻出现。

因此它可以在任何 Python 字节码指令之间引发,但不能在任何 Python 字节码指令期间引发。因此,这一切都归结为“调用函数”(在本例中为

os.posix_spawn
)和“将其结果分配给变量”一条或多条指令。

而且是多个。从 https://pl.python.org/docs/lib/bytecodes.html 有 STORE_* 指令,它们与调用函数是分开的。

https://docs.python.org/3/library/signal.html#note-on-signal-handlers-and-exception还指出

大多数 Python 代码(包括标准库)都无法针对此问题变得稳健,因此键盘中断(或信号处理程序产生的任何其他异常)在极少数情况下可能会使程序处于意外状态。

这暗示了,我认为此类问题在 Python 中是普遍存在的,尽管在实践中可能很少见。

但是https://docs.python.org/3/library/signal.html#note-on-signal-handlers-and-exception也给出了避免这种情况的方法:

复杂或需要高可靠性的应用程序应避免从信号处理程序中引发异常。他们还应该避免捕获 KeyboardInterrupt 作为优雅关闭的方法。相反,他们应该安装自己的 SIGINT 处理程序。

如果您确实需要/想要的话,您可以这样做。从https://stackoverflow.com/a/76919499/1319998获取答案,它本身基于https://stackoverflow.com/a/71330357/1319998,你基本上可以推迟SIGINT/键盘中断

import signal
from contextlib import contextmanager

@contextmanager
def defer_signal(signum):
    # Based on https://stackoverflow.com/a/71330357/1319998

    original_handler = None
    defer_handle_args = None

    def defer_handle(*args):
        nonlocal defer_handle_args
        defer_handle_args = args

    # Do nothing if
    # - we don't have a registered handler in Python to defer
    # - or the handler is not callable, so either SIG_DFL where the system
    #   takes some default action, or SIG_IGN to ignore the signal
    # - or we're not in the main thread that doesn't get signals anyway
    original_handler = signal.getsignal(signum)
    if (
            original_handler is None
            or not callable(original_handler)
            or threading.current_thread() is not threading.main_thread()
    ):
        yield
        return

    try:
        signal.signal(signum, defer_handle)
        yield
    finally:
        signal.signal(signum, original_handler)
        if defer_handle_args is not None:
            original_handler(*defer_handle_args)

创建一个上下文管理器,以更有力地保证您不会因创建过程中的 SIGINT 而获得某种僵尸进程:

@contextmanager
def PopenDeferringSIGINTDuringConstruction(*args, **kwargs):
    # Very much like Popen, but defers SIGINT during its __init__, which is when
    # the process starts at the OS level. This avoids what is essentially a
    # zombie process - the process running but Python having no knowledge of it
    #
    # It doesn't guarentee that p will make it to client code, but should
    # guarentee that the subprocesses __init__ method is not interrupted by a
    # KeyboardInterrupt. And if __init__ raises an exception, then it
    # __del__ method also shouldn't get interrupted

    with defer_signal(signal.SIGINT):
        p = Popen(*args, **kwargs)

    with p:
        yield p

可用作:

with PopenDeferringSIGINTDuringConstruction(['cat']) as p:
    pass
© www.soinside.com 2019 - 2024. All rights reserved.