Come elaborare il segnale SIGTERM con garbo?


197

Supponiamo di avere un demone così banale scritto in Python:

def mainloop():
    while True:
        # 1. do
        # 2. some
        # 3. important
        # 4. job
        # 5. sleep

mainloop()

e lo demonizziamo usando start-stop-daemonquale segnale send SIGTERM( TERM) di default--stop .

Supponiamo che sia il passaggio corrente eseguito #2. E proprio in questo momento stiamo inviando TERMsegnale.

Quello che succede è che l'esecuzione termina immediatamente.

Ho scoperto che posso gestire l'evento signal usando signal.signal(signal.SIGTERM, handler)ma il fatto è che interrompe ancora l'esecuzione corrente e passa il controllo a handler.

Quindi, la mia domanda è: è possibile non interrompere l'esecuzione corrente ma gestire il TERMsegnale in un thread separato (?) In modo da poter impostare in shutdown_flag = Truemodo da mainloop()avere la possibilità di fermarmi con grazia?


2
Ho fatto quello che mi stai chiedendo prima usando signalfde mascherando la consegna del SIGTERMprocesso.
Eric Urban,

Risposte:


278

Una soluzione pulita da usare basata su classe:

import signal
import time

class GracefulKiller:
  kill_now = False
  def __init__(self):
    signal.signal(signal.SIGINT, self.exit_gracefully)
    signal.signal(signal.SIGTERM, self.exit_gracefully)

  def exit_gracefully(self,signum, frame):
    self.kill_now = True

if __name__ == '__main__':
  killer = GracefulKiller()
  while not killer.kill_now:
    time.sleep(1)
    print("doing something in a loop ...")

  print("End of the program. I was killed gracefully :)")

1
Grazie per l'idea! Ho usato un approccio modificato in reboot-guard. github.com/ryran/reboot-guard/blob/master/rguard#L284:L304
rsaw

7
Questa è la risposta migliore (non sono necessari thread) e dovrebbe essere l'approccio di primo tentativo preferito.
jose.angel.jimenez,

2
@ Mausy5043 Python ti consente di non avere parentesi per la definizione delle classi. Sebbene vada perfettamente bene per Python 3.x, ma per Python 2.x, la migliore pratica è usare "class XYZ (object):". Il motivo è: docs.python.org/2/reference/datamodel.html#newstyle
Mayank Jaiswal

2
Segui, per mantenerti motivato, grazie. Lo uso sempre.
Chrisfauerbach,

2
Nel caso peggiore, ciò significherebbe semplicemente fare un'altra iterazione prima di chiudere con grazia. Il Falsevalore viene impostato solo una volta, quindi può passare da False a True, quindi l'accesso multiplo non è un problema.
Alceste_25

52

Innanzitutto, non sono sicuro che sia necessario un secondo thread per impostare il file shutdown_flag.
Perché non impostarlo direttamente nel gestore SIGTERM?

Un'alternativa è sollevare un'eccezione dal SIGTERMgestore, che verrà propagato nello stack. Supponendo che tu abbia una corretta gestione delle eccezioni (ad es. Con with/ contextmanagere try: ... finally:blocchi) questo dovrebbe essere un arresto abbastanza grazioso, simile a se fossi al Ctrl+Ctuo programma.

Esempio di programma signals-test.py:

#!/usr/bin/python

from time import sleep
import signal
import sys


def sigterm_handler(_signo, _stack_frame):
    # Raises SystemExit(0):
    sys.exit(0)

if sys.argv[1] == "handle_signal":
    signal.signal(signal.SIGTERM, sigterm_handler)

try:
    print "Hello"
    i = 0
    while True:
        i += 1
        print "Iteration #%i" % i
        sleep(1)
finally:
    print "Goodbye"

Ora vedi il Ctrl+Ccomportamento:

$ ./signals-test.py default
Hello
Iteration #1
Iteration #2
Iteration #3
Iteration #4
^CGoodbye
Traceback (most recent call last):
  File "./signals-test.py", line 21, in <module>
    sleep(1)
KeyboardInterrupt
$ echo $?
1

Questa volta lo invio SIGTERMdopo 4 iterazioni con kill $(ps aux | grep signals-test | awk '/python/ {print $2}'):

$ ./signals-test.py default
Hello
Iteration #1
Iteration #2
Iteration #3
Iteration #4
Terminated
$ echo $?
143

Questa volta abilito il mio SIGTERMgestore personalizzato e lo invio SIGTERM:

$ ./signals-test.py handle_signal
Hello
Iteration #1
Iteration #2
Iteration #3
Iteration #4
Goodbye
$ echo $?
0

3
"Perché non impostarlo direttamente nel gestore SIGTERM" --- perché il thread di lavoro si interromperebbe in un posto casuale. Se inserisci più istruzioni nel tuo ciclo di lavoro, vedrai che la tua soluzione termina un lavoratore in una posizione casuale, che lascia il lavoro in uno stato sconosciuto.
zerkms

Funziona bene per me, anche in un contesto Docker. Grazie!
Marian,

4
Se hai appena impostato un flag e non sollevi l'eccezione, sarà lo stesso del thread. Quindi usare il thread è superfluo qui.
Suor

28

Penso che tu sia vicino a una possibile soluzione.

Eseguire mainloopin un thread separato ed estenderlo con la proprietà shutdown_flag. Il segnale può essere catturato signal.signal(signal.SIGTERM, handler)nel thread principale (non in un thread separato). Il gestore del segnale dovrebbe impostare shutdown_flagsu True e attendere che il thread terminithread.join()


4
Sì, un thread separato è come l'ho finalmente risolto, grazie
zerkms,

7
I thread non sono richiesti qui. In un singolo programma threaded stesso, è possibile innanzitutto registrare un gestore di segnale (la registrazione di un gestore di segnale non è bloccante) e quindi scrivere mainloop. La funzione del gestore del segnale dovrebbe impostare un flag quando e il loop dovrebbe controllare questo flag. Ho incollato una soluzione di classe per lo stesso qui .
Mayank Jaiswal,

2
Non è necessario disporre di un secondo thread. Registra gestore del segnale.
oneloop


26

Ecco un semplice esempio senza thread o classi.

import signal

run = True

def handler_stop_signals(signum, frame):
    global run
    run = False

signal.signal(signal.SIGINT, handler_stop_signals)
signal.signal(signal.SIGTERM, handler_stop_signals)

while run:
    pass # do stuff including other IO stuff

11

Sulla base delle risposte precedenti, ho creato un gestore di contesto che protegge da sigint e sigterm.

import logging
import signal
import sys


class TerminateProtected:
    """ Protect a piece of code from being killed by SIGINT or SIGTERM.
    It can still be killed by a force kill.

    Example:
        with TerminateProtected():
            run_func_1()
            run_func_2()

    Both functions will be executed even if a sigterm or sigkill has been received.
    """
    killed = False

    def _handler(self, signum, frame):
        logging.error("Received SIGINT or SIGTERM! Finishing this block, then exiting.")
        self.killed = True

    def __enter__(self):
        self.old_sigint = signal.signal(signal.SIGINT, self._handler)
        self.old_sigterm = signal.signal(signal.SIGTERM, self._handler)

    def __exit__(self, type, value, traceback):
        if self.killed:
            sys.exit(0)
        signal.signal(signal.SIGINT, self.old_sigint)
        signal.signal(signal.SIGTERM, self.old_sigterm)


if __name__ == '__main__':
    print("Try pressing ctrl+c while the sleep is running!")
    from time import sleep
    with TerminateProtected():
        sleep(10)
        print("Finished anyway!")
    print("This only prints if there was no sigint or sigterm")

4

Ho trovato il modo più semplice per me. Ecco un esempio con fork per chiarezza che in questo modo è utile per il controllo del flusso.

import signal
import time
import sys
import os

def handle_exit(sig, frame):
    raise(SystemExit)

def main():
    time.sleep(120)

signal.signal(signal.SIGTERM, handle_exit)

p = os.fork()
if p == 0:
    main()
    os._exit()

try:
    os.waitpid(p, 0)
except (KeyboardInterrupt, SystemExit):
    print('exit handled')
    os.kill(p, 15)
    os.waitpid(p, 0)

0

La soluzione più semplice che ho trovato, prendendo ispirazione dalle risposte sopra è

class SignalHandler:

    def __init__(self):

        # register signal handlers
        signal.signal(signal.SIGINT, self.exit_gracefully)
        signal.signal(signal.SIGTERM, self.exit_gracefully)

        self.logger = Logger(level=ERROR)

    def exit_gracefully(self, signum, frame):
        self.logger.info('captured signal %d' % signum)
        traceback.print_stack(frame)

        ###### do your resources clean up here! ####

        raise(SystemExit)
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.