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

use*_*483 4 python multithreading signals

我有一个程序,其中包含一个运行程序(主线程),该运行程序创建 1 个或多个子线程,这些子线程主要使用子进程来触发第 3 方应用程序。

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

signal.signal(signal.SIGINT, handler)
Run Code Online (Sandbox Code Playgroud)

我最初认为,一旦收到 SIGINT,它只会影响我的主线程,然后我将能够管理子线程终止。

然而,我实际观察到的是,按 control+c 也会影响我的子线程(我发现,一旦我按 control+c,子线程中的子进程就会引发 RC 512 异常)。

有人可以建议是否有可能只有主线程会检测到这个信号而不影响子线程?

Nom*_*mal 5

如果您用于subprocess.Popen()创建子进程,并且不希望它们被 SIGINT 信号杀死,请preexec_fn在执行新二进制文件之前使用参数将 SIGINT 信号配置设置为忽略:

child = subprocess.Popen(...,
                         preexec_fn = lambda: signal.signal(signal.SIGINT, signal.SIG_IGN))
Run Code Online (Sandbox Code Playgroud)

在哪里...是当前参数的占位符。

如果您使用实际的线程(线程或线程模块),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)
Run Code Online (Sandbox Code Playgroud)

使用上述帮助程序,其想法是在创建子进程之前(使用 subprocess 模块或某些 os 模块函数),您可以调用sigint_divert(). 子进程继承转移的 SIGINT 处理程序的副本。创建子进程后,您可以通过调用恢复 SIGINT 处理sigint_restore()。(请注意,如果您signal.siginterrupt(signal.SIGINT, False)在设置原始 SIGINT 处理程序后调用,以便其传递不会引发 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)
Run Code Online (Sandbox Code Playgroud)

这样就可以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+它将不会收到 SIGINT C

  • stdin如果父进程不使用子进程' 、stdout或 的管道stderr,则应将它们显式打开为os.devnull

    stdin=open(os.devnull, 'rb'),
    stdout=open(os.devnull, 'wb'),
    stderr=open(os.devnull, 'wb')

    这确保了子进程不会回退到控制终端下。Ctrl(当用户按下+时,控制终端向每个进程发送 SIGINT 信号C。)

如果父进程愿意,它可以使用 向子进程发送 SIGINT 信号os.kill(child.pid, signal.SIGINT)