Python multiprocessing-захват сигналов для перезапуска дочерних процессов или завершения родительского процесса

Я использую многопроцессорную библиотеку для создания двух дочерних процессов. Я хотел бы убедиться, что пока родительский процесс жив, если дочерние процессы умирают (получают SIGKILL или SIGTERM), они перезапускаются автоматически. С другой стороны, если родительский процесс получает SIGTERM/SIGINT, я хочу, чтобы он прекратил все дочерние процессы, а затем вышел.

вот как я подошел к проблеме:

import sys
import time
from signal import signal, SIGINT, SIGTERM, SIGQUIT, SIGCHLD, SIG_IGN
from functools import partial
import multiprocessing
import setproctitle

class HelloWorld(multiprocessing.Process):
    def __init__(self):
        super(HelloWorld, self).__init__()

        # ignore, let parent handle it
        signal(SIGTERM, SIG_IGN)

    def run(self):

        setproctitle.setproctitle("helloProcess")

        while True:
            print "Hello World"
            time.sleep(1)

class Counter(multiprocessing.Process):
    def __init__(self):
        super(Counter, self).__init__()

        self.counter = 1

        # ignore, let parent handle it
        signal(SIGTERM, SIG_IGN)

    def run(self):

        setproctitle.setproctitle("counterProcess")

        while True:
            print self.counter
            time.sleep(1)
            self.counter += 1


def signal_handler(helloProcess, counterProcess, signum, frame):

    print multiprocessing.active_children()
    print "helloProcess: ", helloProcess
    print "counterProcess: ", counterProcess

    if signum == 17:

        print "helloProcess: ", helloProcess.is_alive()

        if not helloProcess.is_alive():
            print "Restarting helloProcess"

            helloProcess = HelloWorld()
            helloProcess.start()

        print "counterProcess: ", counterProcess.is_alive()

        if not counterProcess.is_alive():
            print "Restarting counterProcess"

            counterProcess = Counter()
            counterProcess.start()

    else:

        if helloProcess.is_alive():
            print "Stopping helloProcess"
            helloProcess.terminate()

        if counterProcess.is_alive():
            print "Stopping counterProcess"
            counterProcess.terminate()

        sys.exit(0)



if __name__ == '__main__':

    helloProcess = HelloWorld()
    helloProcess.start()

    counterProcess = Counter()
    counterProcess.start()

    for signame in [SIGINT, SIGTERM, SIGQUIT, SIGCHLD]:
        signal(signame, partial(signal_handler, helloProcess, counterProcess))

    multiprocessing.active_children()

Если я отправить сигнал SIGKILL к counterProcess, это перезагрузится правильно. Однако отправка SIGKILL в helloProcess также перезапускает контрпроцесс вместо helloProcess?

Если я отправлю SIGTERM родительскому процессу, родитель выйдет, но дочерние процессы станут сиротами и продолжатся. Как исправить это поведение?

2 ответов


воссоздать мертвых детей из signal.SIGCHLD обработчик, мать должна вызвать одного из os.wait функции, потому что Process.is_alive здесь не работает.
Хотя это возможно, это сложно, потому что signal.SIGCHLD доставляется матери, когда один из его детей меняет статус f.e. signal.SIGSTOP, signal.SIGCONT или любые другие завершающие сигналы принимаются ребенком.
Так что signal.SIGCHLD обработчик должен различать эти состояния ребенка. Просто воссоздаю детей, когда signal.SIGCHLD доставлен может создать больше детей, чем необходимо.

следующий код использует os.waitpid С os.WNOHANG чтобы сделать его неблокирующим и os.WUNTRACED и os.WCONTINUED для обучения, если signal.SIGCHLD С signal.SIGSTOP или signal.SIGCONT.
os.waitpid не работает, т. е. возвращает (0, 0) если Process экземпляр printЭд, я.е str(Process()) прежде чем позвонить os.waitpid.

import sys
import time
from signal import signal, pause, SIGINT, SIGTERM, SIGQUIT, SIGCHLD, SIG_DFL
import multiprocessing
import os

class HelloWorld(multiprocessing.Process):
    def run(self):
        # reset SIGTERM to default for Process.terminate to work
        signal(SIGTERM, SIG_DFL)
        while True:
            print "Hello World"
            time.sleep(1)

class Counter(multiprocessing.Process):
    def __init__(self):
        super(Counter, self).__init__()
        self.counter = 1

    def run(self):
        # reset SIGTERM to default for Process.terminate to work
        signal(SIGTERM, SIG_DFL)
        while True:
            print self.counter
            time.sleep(1)
            self.counter += 1


def signal_handler(signum, _):
    global helloProcess, counterProcess

    if signum == SIGCHLD:
        pid, status = os.waitpid(-1, os.WNOHANG|os.WUNTRACED|os.WCONTINUED)
        if os.WIFCONTINUED(status) or os.WIFSTOPPED(status):
            return
        if os.WIFSIGNALED(status) or os.WIFEXITED(status):
            if helloProcess.pid == pid:
                print("Restarting helloProcess")
                helloProcess = HelloWorld()
                helloProcess.start()

            elif counterProcess.pid == pid:
                print("Restarting counterProcess")
                counterProcess = Counter()
                counterProcess.start()

    else:
        # mother shouldn't be notified when it terminates children
        signal(SIGCHLD, SIG_DFL)
        if helloProcess.is_alive():
            print("Stopping helloProcess")
            helloProcess.terminate()

        if counterProcess.is_alive():
            print("Stopping counterProcess")
            counterProcess.terminate()

        sys.exit(0)

if __name__ == '__main__':

    helloProcess = HelloWorld()
    helloProcess.start()

    counterProcess = Counter()
    counterProcess.start()

    for signame in [SIGINT, SIGTERM, SIGQUIT, SIGCHLD]:
        signal(signame, signal_handler)

    while True:
        pause()

следующий код воссоздает мертвых детей без использования signal.SIGCHLD. Так это проще чем первый.
Создав двух детей, материнский процесс устанавливает обработчик сигнала с именем term_child для SIGINT, SIGTERM, SIGQUIT. term_child завершает работу и присоединяется к каждому ребенку при вызове.

материнский процесс продолжает проверять, живы ли дети, и воссоздает их при необходимости в while петли.

потому что каждый ребенок наследует обработчики сигналов от матери,SIGINT обработчик должен быть сброшен в значение по умолчанию для Process.terminate для работы

import sys
import time
from signal import signal, SIGINT, SIGTERM, SIGQUIT
import multiprocessing

class HelloWorld(multiprocessing.Process):    
    def run(self):
        signal(SIGTERM, SIG_DFL)
        while True:
            print "Hello World"
            time.sleep(1)

class Counter(multiprocessing.Process):
    def __init__(self):
        super(Counter, self).__init__()
        self.counter = 1

    def run(self):
        signal(SIGTERM, SIG_DFL)
        while True:
            print self.counter
            time.sleep(1)
            self.counter += 1

def term_child(_, __):
    for child in children:
        child.terminate()
        child.join()
    sys.exit(0)

if __name__ == '__main__':

    children = [HelloWorld(), Counter()]
    for child in children:
        child.start()

    for signame in (SIGINT, SIGTERM, SIGQUIT):
        signal(signame, term_child)

    while True:
        for i, child in enumerate(children):
            if not child.is_alive():
                children[i] = type(child)()
                children[i].start()
        time.sleep(1)

есть несколько проблем с кодом, так что я пойду за ним в sequentailly.

если я отправить сигнал SIGKILL к counterProcess, это будет правильно перезагрузить. Однако отправка SIGKILL в helloProcess также перезапускает контрпроцесс вместо helloProcess?

это своеобразное поведение, скорее всего, связано с отсутствием блокировки вызова в вашем основном процессе, так как multiprocessing.active_children() на самом деле не действует как один. Я не могу объяснить точная причина, почему программа ведет себя так, как она делает, но добавление блокировки вызова в , например.

while True:
    time.sleep(1)

решает проблему.

еще одна довольно серьезная проблема заключается в том, как вы передаете объекты в обработчик:

helloProcess = HelloWorld()
...
partial(signal_handler, helloProcess, counterProcess)

что obsolate, учитывая, что вы создаете новые объекты внутри:

if not helloProcess.is_alive():
    print "Restarting helloProcess"

    helloProcess = HelloWorld()
    helloProcess.start()

обратите внимание, что оба объекта используют различные псевдонимы для HelloWorld() объекты. Частичный объект привязан к алиасу в , а объект в callback привязан к его псевдониму локальной области. Поэтому, назначая новый объект псевдониму локальной области, вы на самом деле не влияете на объект, к которому привязан обратный вызов (он по-прежнему привязан к объекту, созданному в __main__ scope).

вы можете исправить это, восстановив обратный вызов сигнала с новыми объектами таким же образом в области обратного вызова:

def signal_handler(...):
    ...
    for signame in [SIGINT, SIGTERM, SIGQUIT, SIGCHLD]:
        signal(signame, partial(signal_handler, helloProcess, counterProcess))
    ...

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

for signame in [SIGINT, SIGTERM, SIGQUIT, SIGCHLD]:
    signal(signame, SIG_DFL)

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

signal(SIGCHLD, SIG_IGN)

обратите внимание, что вы хотите изменить архитектуру своего приложения и использовать некоторые из функций multiprocessing обеспечивает.

финал код:

import sys
import time
from signal import signal, SIGINT, SIGTERM, SIGQUIT, SIGCHLD, SIG_IGN, SIG_DFL
from functools import partial
import multiprocessing
#import setproctitle

class HelloWorld(multiprocessing.Process):
    def __init__(self):
        super(HelloWorld, self).__init__()

        # ignore, let parent handle it
        #signal(SIGTERM, SIG_IGN)

    def run(self):

        #setproctitle.setproctitle("helloProcess")

        while True:
            print "Hello World"
            time.sleep(1)

class Counter(multiprocessing.Process):
    def __init__(self):
        super(Counter, self).__init__()

        self.counter = 1

        # ignore, let parent handle it
        #signal(SIGTERM, SIG_IGN)

    def run(self):

        #setproctitle.setproctitle("counterProcess")

        while True:
            print self.counter
            time.sleep(1)
            self.counter += 1


def signal_handler(helloProcess, counterProcess, signum, frame):

    print multiprocessing.active_children()
    print "helloProcess: ", helloProcess
    print "counterProcess: ", counterProcess

    print "current_process: ", multiprocessing.current_process()

    if signum == 17:

        # Since each new child inherits current signal handler,
        # temporarily set it to default before spawning new child.
        for signame in [SIGINT, SIGTERM, SIGQUIT, SIGCHLD]:
            signal(signame, SIG_DFL)

        print "helloProcess: ", helloProcess.is_alive()

        if not helloProcess.is_alive():
            print "Restarting helloProcess"

            helloProcess = HelloWorld()
            helloProcess.start()

        print "counterProcess: ", counterProcess.is_alive()

        if not counterProcess.is_alive():
            print "Restarting counterProcess"

            counterProcess = Counter()
            counterProcess.start()

        # After new children are spawned, revert to old signal handling policy.
        for signame in [SIGINT, SIGTERM, SIGQUIT, SIGCHLD]:
            signal(signame, partial(signal_handler, helloProcess, counterProcess))


    else:

        # Ignore any signal that child communicates before quit   
        signal(SIGCHLD, SIG_IGN) 

        if helloProcess.is_alive():
            print "Stopping helloProcess"
            helloProcess.terminate()

        if counterProcess.is_alive():
            print "Stopping counterProcess"
            counterProcess.terminate()

        sys.exit(0)



if __name__ == '__main__':

    helloProcess = HelloWorld()
    helloProcess.start()

    counterProcess = Counter()
    counterProcess.start()

    for signame in [SIGINT, SIGTERM, SIGQUIT, SIGCHLD]:
        signal(signame, partial(signal_handler, helloProcess, counterProcess))

    while True:
        print multiprocessing.active_children()
        time.sleep(1)