Python - предотвращает влияние дочерних потоков на сигнал SIGINT - PullRequest
0 голосов
/ 11 октября 2018

У меня есть программа, состоящая из бегуна (который является основным потоком), который создает 1 или более дочерних потоков, которые в основном используют подпроцессы для запуска сторонних приложений.

Я хотел иметь возможность изящного завершениявсе потоки, как только SIGINT получен, поэтому я определил обработчик в моем основном потоке следующим образом:

signal.signal(signal.SIGINT, handler)

Я изначально думал, что, как только SIGINT получен, это повлияет только на мой основной поток, а затем я 'Я смогу управлять дочерними потоками для завершения,.

Однако на самом деле я наблюдаю, что нажатие control + c также влияет на мои дочерние потоки (я вижу, что подпроцесс в дочернем потокевызывая исключение с RC 512, как только я нажимаю control + c).

Может кто-нибудь посоветовать, если возможно, что только основной поток обнаружит этот сигнал, не затрагивая дочерние потоки?

1 Ответ

0 голосов
/ 29 октября 2018

Если вы используете 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

# 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, вызывая 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).


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 по умолчанию блокируется.

Заблокированный сигнал 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 - утилита командной строки, которая запускает указанную программу с указанными аргументами в новом сеансе.Новый сеанс не имеет управляющего терминала, что означает, что он не получит SIGINT, если пользователь нажмет Ctrl + C .

  • Если родительский объект не использует канал для подпроцесса 'stdin, stdout или stderr, они должны быть явно открыты в os.devnull:

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

    Это гарантирует, что подпроцесс не попадет под управляющий терминал.(Именно управляющий терминал посылает сигнал SIGINT каждому процессу, когда пользователь нажимает Ctrl + C .)

Если родительский процессхочет, он может отправить сигнал SIGINT дочернему процессу, используя os.kill(child.pid, signal.SIGINT).

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...