如果您使用subprocess.Popen() 创建子进程,并且您不希望它们被 SIGINT 信号杀死,请使用 preexec_fn 参数在执行新二进制文件之前将 SIGINT 信号处置设置为忽略:
child = subprocess.Popen(...,
preexec_fn = lambda: signal.signal(signal.SIGINT, signal.SIG_IGN))
其中... 是您当前参数的占位符。
如果您使用实际线程(线程或线程模块),Python 的信号模块会设置所有内容,以便只有主/初始线程可以接收信号或设置信号处理程序。因此,正确的线程并不会真正受到 Python 中的信号的影响。
在subprocess.Popen() 的情况下,子进程最初继承了该进程的副本,包括信号处理程序。这意味着有一个小窗口,在此期间子进程可能会使用与父进程相同的代码来捕获信号;但是,因为它是一个单独的过程,所以只有它的副作用是可见的。 (比如信号处理器调用sys.exit(),只有子进程会退出,子进程中的信号处理器不能改变父进程中的任何变量。)
为避免这种情况,父进程可以在子进程创建期间临时切换到不同的信号处理程序,该处理程序仅记住是否捕获了信号:
import signal
# Global variables for sigint diversion
sigint_diverted = False # True if caught while diverted
sigint_original = None # Original signal handler
def sigint_divert_handler():
global sigint_diverted
sigint_diverted = True
def sigint_divert(interrupts=False):
"""Temporarily postpone SIGINT signal delivery."""
global sigint_diverted
global sigint_original
sigint_diverted = False
sigint_original = signal.signal(signal.SIGINT, sigint_divert_handler)
signal.siginterrupt(signal.SIGINT, interrupts)
def sigint_restore(interrupts=True):
"""Restore SIGINT signal delivery to original handler."""
global sigint_diverted
global sigint_original
original = sigint_original
sigint_original = None
if original is not None:
signal.signal(signal.SIGINT, original)
signal.siginterrupt(signal.SIGINT, interrupts)
diverted = sigint_diverted
sigint_diverted = False
if diverted and original is not None:
original(signal.SIGINT)
使用上面的助手,想法是在创建子进程之前(使用 subprocess 模块,或某些 os 模块函数),调用sigint_divert()。子进程继承了转移的 SIGINT 处理程序的副本。创建子进程后,您可以通过调用 sigint_restore() 来恢复 SIGINT 处理。 (请注意,如果您在设置原始 SIGINT 处理程序后调用了signal.siginterrupt(signal.SIGINT, False),以便它的传递不会引发 IOError 异常,您应该在此处调用sigint_restore(False)。)
这样子进程中的信号处理程序就是被转移的信号处理程序,它只设置一个全局标志,其他什么都不做。当然,你还是想用preexec_fn =参数给subprocess.Popen(),这样子进程执行实际二进制时,SIGINT信号就完全被忽略了。
sigint_restore() 不仅恢复了原始信号处理程序,而且如果被转移的信号处理程序捕获到一个 SIGINT 信号,它会通过直接调用原始信号处理程序来“重新引发”。这假设原始处理程序是您已经安装的处理程序;否则,您可以改用os.kill(os.getpid(), signal.SIGKILL)。
非 Windows 操作系统上的 Python 3.3 及更高版本公开了信号掩码,可用于在一段时间内“阻止”信号。阻塞意味着信号的传递被推迟,直到解除阻塞;不被忽视。这正是上面的信号转移代码试图完成的。
信号没有排队,所以如果一个信号已经挂起,任何其他相同类型的信号都会被忽略。 (因此,每种类型只有一个信号,比如 SIGINT,可以同时处于挂起状态。)
这允许一个模式使用两个辅助函数,
def block_signals(sigset = { signal.SIGINT }):
mask = signal.pthread_sigmask(signal.SIG_BLOCK, {})
signal.pthread_sigmask(signal.SIG_BLOCK, sigset)
return mask
def restore_signals(mask):
signal.pthread_sigmask(signal.SIG_SETMASK, mask)
所以在创建线程或子进程之前调用mask = block_signals(),之后调用restore_signals(mask)。在创建的线程或子进程中,SIGINT信号默认被阻塞。
阻塞的 SIGINT 信号也可以使用 signal.sigwait({signal.SIGINT})(阻塞直到一个被传递)或 signal.sigtimedwait({signal.SIGINT}, 0) 使用,如果一个未决的信号立即返回,否则 None。
当子进程管理自己的信号掩码和信号处理程序时,我们不能让它忽略 SIGINT 信号。
在 Unix/POSIXy 机器上,我们可以阻止 SIGINT 被发送到子进程,但是,通过将子进程与控制终端分离,并在自己的会话中运行它。
subprocess.Popen() 中需要进行两组更改:
-
在setsid 下执行命令或二进制文件:[ "setsid", "program", args.. ] 或"setsid sh -c 'command'",具体取决于您提供要作为列表还是作为字符串执行的二进制文件。
setsid 是一个命令行实用程序,它在新会话中使用指定的参数运行指定的程序。新会话没有控制终端,这意味着如果用户按下 Ctrl+C,它将不会收到 SIGINT。
-
如果父进程不使用管道用于子进程'stdin、stdout 或stderr,则应显式将它们打开到os.devnull:
stdin=open(os.devnull, 'rb'),
stdout=open(os.devnull, 'wb'),
stderr=open(os.devnull, 'wb')
这确保子进程不会退回到控制终端下。 (当用户按下Ctrl+C时,控制终端向每个进程发送SIGINT信号。)
如果父进程愿意,它可以使用os.kill(child.pid, signal.SIGINT)向子进程发送一个SIGINT信号。