Python-防止子线程受到SIGINT信号的影响 [英] Python - prevent child threads from being affected from SIGINT signal

查看:80
本文介绍了Python-防止子线程受到SIGINT信号的影响的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I have a program that consist of a runner (which is the main thread) that creates 1 or more child threads which mainly use subprocesses to trigger 3rd party apps.

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

I wanted to be able to gracefully terminate all threads once a SIGINT is received, hence I defined a handler in my main thread as following:

signal.signal(signal.SIGINT, handler)

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

I initially though that once a SIGINT is received then it will affect only my main thread and then I'll be able to manage the child threads to terminate,.

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

However what I'm actually observing is that pressing control+c is also affecting my child threads (I'm seeing that the subprocess within the child thread is raising exception with RC 512 once I press control+c).

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

Can someone advise if it is possible that only the main thread will detect this signal without affecting the child threads?

推荐答案

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

If you use subprocess.Popen() to create child processes, and you do not want them to be killed by a SIGINT signal, use the preexec_fn argument to set the SIGINT signal disposition to ignored before the new binary is executed:

child = subprocess.Popen(...,
                         preexec_fn = lambda: signal.signal(signal.SIGINT, signal.SIG_IGN))

其中...是您当前参数的占位符.

where ... is a placeholder for your current parameters.

如果您使用实际的线程(线程或线程模块),Python的信号模块会设置所有内容,以便只有主线程/初始线程才能接收信号或设置信号处理程序.因此,正确的线程实际上并不受Python中信号的影响.

If you use actual threads (either threads or threading module), Python's signal module sets everything up so that only the main/initial thread can receive signals or set signal handlers. So, proper threads are not really affected by signals in Python.

subprocess.Popen()情况下,子进程最初继承该进程的副本,包括信号处理程序.这意味着存在一个小窗口,子进程可以使用与父进程相同的代码来捕获信号.但是,由于它是一个独立的过程,因此仅可见其副作用. (例如,如果信号处理程序调用sys.exit(),则仅子进程将退出.子进程中的信号处理程序无法更改父进程中的任何变量.)

In the subprocess.Popen() case, the child process initially inherits a copy of the process, including signal handlers. This means that there is a small window during which the child process may catch a signal, using the same code as the parent process; but, because it is a separate process, only its side effects are visible. (For example, if the signal handler calls sys.exit(), only the child process will exit. The signal handler in the child process cannot change any variables in the parent process.)

为避免这种情况,父进程可以临时切换到其他信号处理程序,该信号处理程序仅在子进程创建期间记住是否捕获到信号:

To avoid this, the parent process can temporarily switch to a different signal handler, that only remembers if a signal is caught, for the duration of the subprocess creation:

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)

使用上述帮助器,其想法是在创建子进程(使用子进程模块或某些os模块函数)之前,请调用sigint_divert().子进程继承了转移的SIGINT处理程序的副本.创建子进程后,可通过调用sigint_restore()恢复SIGINT处理. (请注意,如果您在设置原始SIGINT处理程序后调用了signal.siginterrupt(signal.SIGINT, False),以便其传递不会引发IOError异常,则应在此处调用sigint_restore(False).)

With the above helpers, the idea is that before creating a child process (using subprocess module, or some of the os module functions), you call sigint_divert(). The child process inherits a copy of the diverted SIGINT handler. After creating the child process, you restore SIGINT handling by calling sigint_restore(). (Note that if you called signal.siginterrupt(signal.SIGINT, False) after setting your original SIGINT handler, so that its delivery won't raise IOError exceptions, you should call here sigint_restore(False) instead.)

这样,子进程中的信号处理程序就是转向信号处理程序,该信号处理程序仅设置一个全局标志,并且不执行其他任何操作.当然,您仍然想对subprocess.Popen()使用preexec_fn =参数,以便在子进程中执行实际的二进制文件时完全忽略SIGINT信号.

This way, the signal handler in the child process is the diverted signal handler, which only sets a global flag and does nothing else. Of course, you still want to use the preexec_fn = parameter to subprocess.Popen(), so that SIGINT signal is ignored completely when the actual binary is executed in the child process.

sigint_restore()不仅恢复原始信号处理程序,而且如果转移的信号处理程序捕获到SIGINT信号,则可以通过直接调用原始信号处理程序来重新引发"它.假设原始处理程序是您已经安装的处理程序;否则,您可以改用os.kill(os.getpid(), signal.SIGKILL).

The sigint_restore() not only restores the original signal handler, but if the diverted signal handler caught a SIGINT signal, it is "re-raised" by calling the original signal handler directly. This assumes that the original handler is one you've already installed; otherwise, you could use os.kill(os.getpid(), signal.SIGKILL) instead.

在非Windows操作系统上的Python 3.3及更高版本公开了信号掩码,该信号掩码可用于在一段时间内阻止"信号.阻塞意味着信号的传递被推迟,直到被解除阻塞为止.不容忽视.这正是上述信号转移代码试图实现的目的.

Python 3.3 and later on non-Windows OSes exposes signal masks, which can be used to "block" signals for a duration. Blocking means that the delivery of the signal is postponed, until unblocked; not ignored. This is exactly what the above signal diversion code tries to accomplish.

信号不会排队,因此,如果一个信号已经挂起,则将忽略任何其他相同类型的信号. (因此,每种类型的信号(例如SIGINT)只能同时处于待处理状态.)

The signals are not queued, so if one signal is already pending, any further signals of that same type are ignored. (So, only one signal of each type, say SIGINT, can be pending at the same time.)

这允许使用两个辅助功能的模式,

This allows a pattern using two helper functions,

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信号.

so that one calls mask = block_signals() before creating a thread or a subprocess, and restore_signals(mask) afterwards. In the created thread or subprocess, the SIGINT signal is blocked by default.

阻塞的SIGINT信号也可以使用signal.sigwait({signal.SIGINT})(阻塞直到发送一个信号)使用,或者signal.sigtimedwait({signal.SIGINT}, 0)消耗,如果有一个信号处于待处理状态则立即返回该信号,否则使用None.

Blocked SIGINT signal can also be consumed using signal.sigwait({signal.SIGINT}) (which blocks until one is delivered), or signal.sigtimedwait({signal.SIGINT}, 0) which returns immediately with the signal if one is pending, and None otherwise.

当子进程管理自己的信号掩码和信号处理程序时,我们不能使它忽略SIGINT信号.

When a subprocess manages its own signal mask and signal handlers, we cannot make it ignore a SIGINT signal.

在Unix/POSIXy计算机上,我们可以通过将子进程与控制终端分离并在其自己的会话中运行来停止将SIGINT发送给子进程.

On Unix/POSIXy machines, we can stop the SIGINT from being sent to the child process, however, by detaching the child process from the controlling terminal, and running it in its own session.

subprocess.Popen()中需要进行两组更改:

There are two sets of changes needed in subprocess.Popen():

  • setsid下执行命令或二进制文件:[ "setsid", "program", args.. ]"setsid sh -c 'command'",具体取决于您提供的二进制文件是列表还是字符串.

  • Execute the command or binary under setsid: either [ "setsid", "program", args.. ] or "setsid sh -c 'command'", depending on whether you supply the binary to be executed as a list or as a string.

setsid 是一个命令行实用程序在新会话中运行带有指定参数的指定程序.新会话没有控制终端,这意味着如果用户按下 Ctrl + C ,它将不会收到SIGINT.

setsid is a command-line utility that runs the specified program with the specified arguments in a new session. The new session does not have a controlling terminal, which means it will not receive a SIGINT if the user presses Ctrl+C.

如果父级不将管道用于子进程的stdinstdoutstderr,则应将它们显式打开为os.devnull:

If the parent does not use a pipe for the subprocess' stdin, stdout, or stderr, they should be explicitly opened to os.devnull:

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

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

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

This ensures that the subprocess does not fall back under the controlling terminal. (It is the controlling terminal that sends the SIGINT signal to each process when user presses Ctrl+C.)

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

If the parent process wants to, it can send a SIGINT signal to the child process using os.kill(child.pid, signal.SIGINT).

这篇关于Python-防止子线程受到SIGINT信号的影响的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆