Python-防止子线程受到SIGINT信号的影响

时间:2018-10-11 15:12:01

标签: python multithreading signals

我有一个由运行程序(它是主线程)组成的程序,该程序创建1个或多个子线程,这些子线程主要使用子进程来触发第三方应用程序。

我希望能够在收到SIGINT后正常终止所有线程,因此我在主线程中定义了一个处理程序,如下所示:

signal.signal(signal.SIGINT, handler)

尽管最初我收到了SIGINT,但它只会影响我的主线程,然后我将能够管理子线程终止。

不过,我实际上观察到的是,按下Ctrl + c也会影响我的子线程(我看到子线程中的子进程一旦按下Ctrl + c就会引发RC 512异常)。

有人可以建议仅主线程在不影响子线程的情况下检测到该信号吗?

1 个答案:

答案 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。

  • 如果父级未将管道用于子流程的stdinstdoutstderr,则应将其显式打开给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信号。