如果您使用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
sigint_diverted = False
sigint_original = None
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)。
Python 3.3 及之后的非 Windows 操作系统提供了信号掩码,可用于阻止信号一段时间。阻止意味着信号的传递被推迟,直到解除阻止;而不是被忽略。这正是以上信号分流代码试图实现的内容。
信号不会排队,因此如果一个信号已经挂起,那么同一类型的任何进一步信号都会被忽略。(所以,同一类型的信号,比如 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信号。
还可以使用
signal.sigwait({signal.SIGINT})
(该方法会阻塞直到信号被传递)或
signal.sigtimedwait({signal.SIGINT}, 0)
(如果有待处理的信号,则立即返回该信号,否则返回
None
)来消耗被阻止的SIGINT信号。
当子进程管理自己的信号掩码和信号处理程序时,我们无法让它忽略SIGINT信号。
但是,在Unix/POSIXy机器上,我们可以通过将子进程与控制终端分离并在其自己的会话中运行来阻止发送SIGINT信号给子进程。
在
subprocess.Popen()
中需要进行两组更改:
1. 在
setsid
下执行命令或二进制文件:要么是
[ "setsid", "program", args.. ]
,要么是
"setsid sh -c 'command'"
,具体取决于您是否将要执行的二进制文件作为列表或字符串提供。
setsid
是一个命令行实用程序,它在新会话中使用指定的参数运行指定的程序。新会话没有控制终端,这意味着如果用户按下
Ctrl+
C,它将不会收到SIGINT。
2. 如果父进程未使用管道来处理子进程的
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信号。
signal.signal(signal.SIGINT, signal_handler)
对你行不通。相关信息:在线链接。 - stovfl