Python: 如何防止子进程接收CTRL-C / Control-C / SIGINT信号?

45

我目前正在为一个在shell中运行的专用服务器编写包装器。该包装器通过subprocess生成服务器进程,并观察和响应其输出。

专用服务器必须明确地给出一个命令以正常关闭。因此,CTRL-C不能到达服务器进程。

如果我在Python中捕获KeyboardInterrupt异常或覆盖SIGINT处理程序,则服务器进程仍会接收CTRL-C并立即停止。

因此我的问题是: 如何防止子进程接收CTRL-C / Control-C / SIGINT信号?


我对解决方法很感兴趣,如果您能发布它就太好了! - Gleno
你使用的操作系统是什么? - ʇsәɹoɈ
专用服务器正在Linux系统(Debian)上运行。 - robert
6
有人知道在Windows上解决这个问题的方法吗? - Michael Herrmann
5个回答

43

在 #python IRC 频道(Freenode)中的某个人通过指出 subprocess.Popen(...)preexec_fn参数帮助了我:

如果将 preexec_fn 设置为可调用对象,则该对象将在子进程执行之前在子进程中被调用。(仅适用于Unix)

因此,以下代码解决了这个问题(仅适用于UNIX):

import subprocess
import signal

def preexec_function():
    # Ignore the SIGINT signal by setting the handler to the standard
    # signal handler SIG_IGN.
    signal.signal(signal.SIGINT, signal.SIG_IGN)

my_process = subprocess.Popen(
    ["my_executable"],
    preexec_fn = preexec_function
)

注意: 信号实际上并未被阻止到达子进程。相反,上面的preexec_fn重写了信号的默认处理程序,使信号被忽略。因此,如果子进程再次覆盖SIGINT处理程序,则此解决方案可能无法正常工作。

另一个要注意的点: 此解决方案适用于各种子进程,即不仅限于Python编写的子进程。例如,我正在为其编写包装器的专用服务器实际上是由Java编写的。


1
如果在一个使用多个线程的Python应用程序中使用Popen(preexec_fn=...),最终会导致死锁。我已经遇到过几次这个问题,并且在这里有相关的文档记录:https://docs.python.org/3/library/subprocess.html#subprocess.Popen - Adam Casey

26
结合其他答案的方法,可以解决问题 - 不会将发送给主应用程序的信号转发到子进程。
在Python 3.11及更高版本中,您可以使用Popenprocess_group参数:
from subprocess import Popen

Popen('do_not_want_signals', process_group=0)

在Python 3.10及更早版本中,您可以使用preexec_fn
import os
from subprocess import Popen

def preexec(): # Don't forward signals.
    os.setpgrp()

Popen('do_not_want_signals', preexec_fn=preexec)
# The above can be shortened to:
Popen('do_not_want_signals', preexec_fn=os.setpgrp)

14
+1 你不需要 preexec 函数,Popen(args, preexec_fn=os.setpgrp) 也可以。 - Peter Sutton
8
建议尝试使用 Popen(args, preexec_fn=os.setpgrp) 替代 preexec_nf。;-) - Marcus
1
在Python 3.11+中,应该使用process_group=0来替代preexec_fn=os.setpgrp,因为preexec_fn与线程不兼容。在早期的Python版本中,可以设置start_new_session=True来实现类似的结果,而不是使用preexec_fn=,尽管它的setsid()调用也会做更多的事情... - gps

9
您可以像这样做,使其在Windows和Unix中运行:
import subprocess
import sys

def pre_exec():
    # To ignore CTRL+C signal in the new process
    signal.signal(signal.SIGINT, signal.SIG_IGN)

if sys.platform.startswith('win'):
    #https://msdn.microsoft.com/en-us/library/windows/desktop/ms684863(v=vs.85).aspx
    #CREATE_NEW_PROCESS_GROUP=0x00000200 -> If this flag is specified, CTRL+C signals will be disabled
    my_sub_process=subprocess.Popen(["executable"], creationflags=0x00000200)
else:
    my_sub_process=subprocess.Popen(["executable"], preexec_fn = pre_exec)

2
当我使用你的creationflags时,在Windows上无法使用Ctrl+C杀死主进程。有什么想法吗? - Fuzzyma
@Fuzzyma 我找到了一个快速解决方法,使用 win32api 代替信号:win32api.SetConsoleCtrlHandler(exit_handler, True)。非常有效。 - Shameer Kashif
类似于另一个答案的注释,避免使用 preexec_fn= 并使用 3.11 的 process_group=0 或 3.2 的 start_new_session=True 标志,因为 preexec_fn= 不是线程安全的。 - gps

3

经过一个小时的尝试,以下方法对我有效:

process = subprocess.Popen(["someprocess"], creationflags=subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP)

这是针对Windows的解决方案。


1

在生成子进程之前,尝试将SIGINT设置为忽略(然后将其重置为默认行为)。

如果这样不起作用,您需要阅读作业控制并学习如何将进程放入自己的后台进程组中,以便^C甚至不会导致内核首先向其发送信号。(在Python中可能无法实现,除非编写C助手程序。)

另请参见这个旧问题


我尝试过这个,但它没有起作用(在生成子进程之前忽略SIGINT,即不是作业控制的事情)。我现在有一个解决方法,稍后今天或明天我会介绍。 - robert

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