如果从 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
。
这确实表明键盘中断可以阻止进程的清理?
我认为你不应该对此有问题。另外,您在上下文管理器中打开 Popen,因此一旦退出,缩进上下文管理器就应该自动为您正确退出 Popen 会话。
我相信它与 try 块中的finally语句相同,在键盘中断时它仍然会运行finally语句。我将在下面包含上下文管理器的文档链接,以便您可以阅读有关它的更多信息。
编辑:上下文管理器确实使用带有finally语句的try块来释放/退出。
https://docs.python.org/3/library/contextlib.html
编辑后续:
因此,如果 __init__ 未完全完成并且调用了键盘中断,则应通过垃圾收集来处理它,并且应自动调用 __del__ 函数。我在下面提供了链接。这应该会为你处理一切。
来自 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