我有一个由运行程序(它是主线程)组成的程序,该程序创建1个或多个子线程,这些子线程主要使用子进程来触发第三方应用程序。
我希望能够在收到SIGINT后正常终止所有线程,因此我在主线程中定义了一个处理程序,如下所示:
signal.signal(signal.SIGINT, handler)
尽管最初我收到了SIGINT,但它只会影响我的主线程,然后我将能够管理子线程终止。
不过,我实际上观察到的是,按下Ctrl + c也会影响我的子线程(我看到子线程中的子进程一旦按下Ctrl + c就会引发RC 512异常)。
有人可以建议仅主线程在不影响子线程的情况下检测到该信号吗?
答案 0 :(得分:0)
如果您使用subprocess.Popen()
创建子进程,并且不希望它们被SIGINT信号杀死,请使用preexec_fn
参数将SIGINT信号处置设置为在新二进制文件之前被忽略被执行:
child = subprocess.Popen(...,
preexec_fn = lambda: signal.signal(signal.SIGINT, signal.SIG_IGN))
其中...
是您当前参数的占位符。
如果您使用实际线程(线程或线程模块),Python的信号模块会设置所有内容,以便只有主线程/初始线程才能接收信号或设置信号处理程序。因此,Python中的信号实际上并不会影响适当的线程。
在subprocess.Popen()
情况下,子进程最初继承该进程的副本,包括信号处理程序。这意味着存在一个小窗口,子进程可以使用与父进程相同的代码来捕获信号。但是,由于它是一个独立的过程,因此仅可见其副作用。 (例如,如果信号处理程序调用{{1}},则仅子进程将退出。子进程中的信号处理程序无法更改父进程中的任何变量。)
为避免这种情况,父进程可以临时切换到其他信号处理程序,该信号处理程序仅在子进程创建期间记住是否捕获到信号:
sys.exit()
有了上述助手,我们的想法是,在创建子进程(使用子进程模块或某些os模块函数)之前,请调用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)
。子进程继承了SIGINT处理程序的副本。创建子进程后,可以通过调用sigint_divert()
恢复SIGINT处理。 (请注意,如果您在设置原始SIGINT处理程序后调用了sigint_restore()
,以便其传递不会引发IOError异常,则应在此处调用signal.siginterrupt(signal.SIGINT, False)
。)
这样,子进程中的信号处理程序就是转向信号处理程序,该信号处理程序仅设置一个全局标志,并且不执行其他任何操作。当然,您仍然想对sigint_restore(False)
使用preexec_fn =
参数,以便在子进程中执行实际的二进制文件时完全忽略SIGINT信号。
subprocess.Popen()
不仅恢复原始信号处理程序,而且如果转移的信号处理程序捕获到SIGINT信号,则可以通过直接调用原始信号处理程序来“重新引发”它。假设原始处理程序是您已经安装的处理程序;否则,您可以改用sigint_restore()
。
在非Windows操作系统上的Python 3.3及更高版本公开了信号掩码,该信号掩码可用于在一段时间内“阻止”信号。阻塞意味着信号的传递被推迟,直到被解除阻塞为止。不容忽视。这正是上述信号转移代码试图实现的目的。
信号不会排队,因此,如果一个信号已经挂起,则将忽略任何其他相同类型的信号。 (因此,每种类型的信号(例如SIGINT)只能同时处于待处理状态。)
这允许使用两个辅助功能的模式,
os.kill(os.getpid(), signal.SIGKILL)
,以便在创建线程或子进程之前调用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()
。在创建的线程或子进程中,默认情况下会阻止SIGINT信号。
也可以使用restore_signals(mask)
(阻塞直到发送一个信号为止)或signal.sigwait({signal.SIGINT})
来消耗已阻塞的SIGINT信号,如果一个信号处于待处理状态,则signal.sigtimedwait({signal.SIGINT}, 0)
立即返回该信号,否则使用None
。
当子进程管理自己的信号掩码和信号处理程序时,我们不能使它忽略SIGINT信号。
在Unix / POSIXy机器上,我们可以通过将子进程与控制终端分离,并在其自己的会话中运行来停止将SIGINT发送到子进程。
subprocess.Popen()
中需要进行两组更改:
在setsid
下执行命令或二进制文件:[ "setsid", "program", args.. ]
或"setsid sh -c 'command'"
,具体取决于您提供的二进制文件是列表还是字符串。< / p>
setsid
是一个命令行实用程序,可在新会话中运行带有指定参数的指定程序。新会话没有控制终端,这意味着如果用户按下 Ctrl + C ,它将不会收到SIGINT。
如果父级未将管道用于子流程的stdin
,stdout
或stderr
,则应将其显式打开给os.devnull
:< / p>
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信号。