Как остановить python от распространения сигналов на подпроцессы?

Я использую python для управления некоторыми симуляциями. Я создаю параметры и запускаю программу, используя:

pipe = open('/dev/null', 'w')
pid = subprocess.Popen(shlex.split(command), stdout=pipe, stderr=pipe)

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

Я хочу отправить сигнал (SIGINT, Ctrl+C) моему скрипту Python, который спросит пользователя, какой сигнал он хочет отправить программе.

Единственное, что мешает коду работать, это то, что кажется, что что бы я ни делал, Ctrl+C будет "перенаправлено" в подпроцесс: код поймает его и выйдет:

try:
  <wait for available slots>
except KeyboardInterrupt:
  print "KeyboardInterrupt catched! All simulations are paused. Please choose the signal to send:"
  print "  0: SIGCONT (Continue simulation)"
  print "  1: SIGINT  (Exit and save)"
  [...]
  answer = raw_input()
  pid.send_signal(signal.SIGCONT)
  if   (answer == "0"):
    print "    --> Continuing simulation..."
  elif (answer == "1"):
    print "    --> Exit and save."
    pid.send_signal(signal.SIGINT)
    [...]

Итак, что бы я ни делал, программа получает SIGINT, который я хочу видеть только в моем скрипте Python. Как мне это сделать???

Я также пробовал:

signal.signal(signal.SIGINT, signal.SIG_IGN)
pid = subprocess.Popen(shlex.split(command), stdout=pipe, stderr=pipe)
signal.signal(signal.SIGINT, signal.SIG_DFL)

запустить программу, но это дает тот же результат: программа ловит SIGINT.

Спасибо!


person big_gie    schedule 24.09.2010    source источник
comment
Здесь много хороших ответов: stackoverflow.com/questions/13593223/   -  person totaam    schedule 15.02.2020


Ответы (5)


Объединение некоторых других ответов, которые помогут - никакой сигнал, отправленный в основное приложение, не будет перенаправлен в подпроцесс.

import os
from subprocess import Popen

def preexec(): # Don't forward signals.
    os.setpgrp()

Popen('whatever', preexec_fn = preexec)
person Marek Sapota    schedule 27.03.2011
comment
Рассмотрите возможность использования [subprocess32][1] вместо subprocess, если вы используете Python 2.x и больше не используете preexec_fn. Это небезопасно. Вместо этого используйте новый параметр Popen start_new_session=True. [1]: code.google.com/p/python-subprocess32. - person gps; 18.09.2012
comment
Это именно то, что я искал. Спасибо. - person Giampaolo Rodolà; 12.02.2014
comment
Помимо аргумента безопасности, приведенный выше код работает, потому что обычно, когда вы нажимаете Ctrl-C, SIGINT отправляется в группу процессов. По умолчанию все подпроцессы находятся в той же группе процессов, что и родительский процесс. Вызывая setpgrp(), вы помещаете свой дочерний процесс в новую группу процессов, чтобы он не получал сигналы от родителя. - person Cenk Alti; 15.11.2014
comment
С точки зрения стиля, немного более похоже на Pythonic: Popen('whatever', preexec_fn=os.setpgrp). Нет необходимости определять preexec(). - person Scott; 30.05.2015
comment
Почему бы просто не использовать signal.signal(signal.SIGINT, signal.SIG_IGN) в preexec? - person omikron; 29.02.2016

Это действительно можно сделать с помощью ctypes. Я бы не рекомендовал это решение, но мне было достаточно интересно, чтобы что-то приготовить, поэтому я решил поделиться им.

parent.py

#!/usr/bin/python

from ctypes import *
import signal
import subprocess
import sys
import time

# Get the size of the array used to
# represent the signal mask
SIGSET_NWORDS = 1024 / (8 * sizeof(c_ulong))

# Define the sigset_t structure
class SIGSET(Structure):
    _fields_ = [
        ('val', c_ulong * SIGSET_NWORDS)
    ]

# Create a new sigset_t to mask out SIGINT
sigs = (c_ulong * SIGSET_NWORDS)()
sigs[0] = 2 ** (signal.SIGINT - 1)
mask = SIGSET(sigs)

libc = CDLL('libc.so.6')

def handle(sig, _):
    if sig == signal.SIGINT:
        print("SIGINT from parent!")

def disable_sig():
    '''Mask the SIGINT in the child process'''
    SIG_BLOCK = 0
    libc.sigprocmask(SIG_BLOCK, pointer(mask), 0)

# Set up the parent's signal handler
signal.signal(signal.SIGINT, handle)

# Call the child process
pid = subprocess.Popen("./child.py", stdout=sys.stdout, stderr=sys.stdin, preexec_fn=disable_sig)

while (1):
    time.sleep(1)

child.py

#!/usr/bin/python
import time
import signal

def handle(sig, _):
    if sig == signal.SIGINT:
        print("SIGINT from child!")

signal.signal(signal.SIGINT, handle)
while (1):
    time.sleep(1)

Обратите внимание, что это делает кучу предположений о различных структурах libc и поэтому, вероятно, довольно хрупко. При запуске вы не увидите сообщение "SIGINT from child!" напечатано. Однако, если вы закомментируете вызов sigprocmask, вы это сделаете. Вроде справляется со своей задачей :)

person Michael Mior    schedule 25.09.2010
comment
Спасибо за ваше предложение. Я думаю, что это слишком сложно для моей цели. По сути, я просто хочу приостановить родительский скрипт, что-то спросить у пользователя и отправить сигнал всем дочерним процессам. Может быть, другой ввод с клавиатуры может приостановить родительский скрипт, например, ctrl+x? - person big_gie; 26.09.2010
comment
Да, как я уже сказал, я бы не рекомендовал решение. Вы можете использовать любую комбинацию клавиш, которую хотите приостановить, если вы слушаете события клавиатуры в отдельном потоке. - person Michael Mior; 27.09.2010

POSIX говорит, что программа запускается с execvp (это то, что subprocess.Popen использует) должен наследовать маску сигнала вызывающего процесса.

Я могу ошибаться, но я не думаю, что вызов signal изменяет маска. Вам нужен sigprocmask, который python не предоставляет напрямую.

Это будет хак, но вы можете попробовать установить его через прямой вызов libc через ctypes. Мне определенно было бы интересно увидеть лучший ответ по этой стратегии.

Альтернативной стратегией был бы опрос стандартного ввода для пользовательского ввода как часть вашего основного цикла. («Нажмите Q, чтобы выйти/приостановить» — что-то в этом роде.) Это позволяет обойти проблему обработки сигналов.

person bstpierre    schedule 25.09.2010

Я решил эту проблему, создав вспомогательное приложение, которое я вызываю вместо непосредственного создания дочернего элемента. Этот помощник изменяет свою родительскую группу, а затем порождает настоящий дочерний процесс.

import os
import sys

from time import sleep
from subprocess import Popen

POLL_INTERVAL=2

# dettach from parent group (no more inherited signals!)
os.setpgrp()

app = Popen(sys.argv[1:])
while app.poll() is None:
    sleep(POLL_INTERVAL)

exit(app.returncode)

Я вызываю этот хелпер у родителя, передавая реальный дочерний элемент и его параметры в качестве аргументов:

Popen(["helper", "child", "arg1", ...])

Я должен сделать это, потому что мое дочернее приложение не находится под моим контролем, если бы я мог добавить туда setpgrp и вообще обойти помощника.

person Marcelo Santos    schedule 02.02.2011

Функция:

os.setpgrp()

Работает хорошо, только если сразу после этого вызывается Popen. Если вы пытаетесь предотвратить распространение сигналов на подпроцессы произвольного пакета, пакет может переопределить это перед созданием подпроцессов, вызывающих распространение сигналов в любом случае. Это тот случай, когда, например, пытаются предотвратить распространение сигнала в процессы веб-браузера, порожденные пакетом Selenium.

Эта функция также лишает возможности легко обмениваться данными между отдельными процессами без чего-то вроде сокетов.

Для моих целей это казалось излишеством. Вместо того, чтобы беспокоиться о распространении сигналов, я использовал собственный сигнал SIGUSR1. Многие пакеты Python игнорируют SIGUSR1, поэтому, даже если он отправляется всем подпроцессам, он обычно игнорируется.

Его можно отправить в процесс в bash на Ubuntu, используя

kill -10 <pid>

Его можно распознать в вашем коде через

signal.signal(signal.SIGUSR1, callback_function)

Доступные номера сигналов в Ubuntu можно найти по адресу /usr/include/asm/signal.h.

person jbass    schedule 04.05.2015