Как остановить python от распространения сигналов на подпроцессы?
Я использую python для управления некоторыми симуляторами. Я создаю параметры и запускаю программу, используя:
pipe = open('/dev/null', 'w')
pid = subprocess.Popen(shlex.split(command), stdout=pipe, stderr=pipe)
Мой код обрабатывает разные сигналы. Ctrl + C остановит симуляцию, спросит, хочу ли я сохранить и выйдет изящно. У меня есть другие обработчики сигналов (например, для принудительного вывода данных).
Я хочу отправить сигнал (SIGINT, Ctrl + C) на мой python script, который спросит пользователя, какой сигнал он хочет отправить в программу.
Единственное, что предотвращает работу кода, это то, что кажется, что что бы я ни делал, 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 script. Как я могу это сделать?
Я также пробовал:
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.
Thanx!
Ответы
Ответ 1
Сочетание некоторых других ответов, которые будут делать трюк - ни один сигнал, отправленный в основное приложение, не будет перенаправлен на подпроцесс.
import os
from subprocess import Popen
def preexec(): # Don't forward signals.
os.setpgrp()
Popen('whatever', preexec_fn = preexec)
Ответ 2
Это действительно можно сделать, используя 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 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 от ребенка!". распечатаны. Однако, если вы закомментируете вызов sigprocmask
, тогда вы это сделаете. Кажется, чтобы выполнить эту работу:)
Ответ 3
POSIX говорит, что программа, выполняемая с execvp (это то, что использует подпроцесс .Popen), должно наследовать маску сигнала вызывающего процесса.
Я мог ошибаться, но я не думаю, что вызов signal
изменяет маску. Вы хотите sigprocmask
, который python напрямую не раскрывает.
Это будет взлом, но вы можете попробовать установить его через прямой вызов libc через ctypes. Я определенно был бы заинтересован в том, чтобы лучше ответить на эту стратегию.
Альтернативной стратегией будет опрос stdin для ввода пользователем как часть вашего основного цикла. ( "Нажмите Q, чтобы выйти/приостановить" - что-то вроде этого.) Это оборачивается проблемой обработки сигналов.
Ответ 4
Я решил эту проблему, создав вспомогательное приложение, которое я вызываю, вместо того, чтобы напрямую создавать дочерний элемент. Этот помощник меняет родительскую группу, а затем порождает реальный дочерний процесс.
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 туда и вообще обходить помощника.
Ответ 5
Функция:
os.setpgrp()
Хорошо работает, только если Popen вызывается сразу после. Если вы пытаетесь предотвратить распространение сигналов на подпроцессы произвольного пакета, пакет может переопределить это, прежде чем создавать подпроцессы, вызывающие распространение сигналов в любом случае. Это тот случай, когда, например, пытается предотвратить распространение сигнала в процессах веб-браузера, порожденных из пакета Selenium.
Эта функция также удаляет возможность легко общаться между отдельными процессами без каких-либо сокетов.
Для моих целей это казалось излишним. Вместо того, чтобы беспокоиться о распространении сигналов, я использовал собственный сигнал SIGUSR1. Многие пакеты Python игнорируют SIGUSR1, поэтому, даже если они отправляются во все подпроцессы, их обычно игнорируют.
Он может быть отправлен процессу в bash на Ubuntu с помощью
kill -10 <pid>
Он может быть распознан в вашем коде через
signal.signal(signal.SIGUSR1, callback_function)
Доступные номера сигналов на Ubuntu можно найти в /usr/include/asm/signal.h.