Wie verarbeite ich das SIGTERM-Signal ordnungsgemäß?

197

Nehmen wir an, wir haben einen so trivialen Daemon in Python geschrieben:

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

mainloop()

und wir dämonisieren es, indem wir start-stop-daemonstandardmäßig das Signal SIGTERM( TERM) senden --stop.

Nehmen wir an, der aktuell ausgeführte Schritt ist #2. Und in diesem Moment senden wir ein TERMSignal.

Was passiert ist, dass die Ausführung sofort beendet wird.

Ich habe festgestellt, dass ich das Signalereignis mit verarbeiten kann, signal.signal(signal.SIGTERM, handler)aber die Sache ist, dass es immer noch die aktuelle Ausführung unterbricht und die Steuerung an übergibt handler.

Meine Frage ist also: Ist es möglich, die aktuelle Ausführung nicht zu unterbrechen, sondern das TERMSignal in einem getrennten Thread (?) Zu verarbeiten, damit ich es shutdown_flag = Trueso einstellen konnte, dass ich die Möglichkeit mainloop()hatte, ordnungsgemäß anzuhalten?

zerkms
quelle
2
Ich habe getan, wonach Sie gefragt haben, indem signalfdich die Lieferung SIGTERMan den Prozess verwendet und ausgeblendet habe.
Eric Urban

Antworten:

275

Eine klassenbasierte Clean-to-Use-Lösung:

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 :)")
Mayank Jaiswal
quelle
1
Danke für die Idee! Ich habe einen modifizierten Ansatz in Reboot-Guard verwendet. github.com/ryran/reboot-guard/blob/master/rguard#L284:L304
rsaw
7
Dies ist die beste Antwort (keine Threads erforderlich) und sollte der bevorzugte Ansatz beim ersten Versuch sein.
Jose.angel.jimenez
2
@ Mausy5043 Mit Python können Sie keine Klammern zum Definieren von Klassen haben. Obwohl es für Python 3.x vollkommen in Ordnung ist, aber für Python 2.x, empfiehlt es sich, "Klasse XYZ (Objekt):" zu verwenden. Grund dafür ist: docs.python.org/2/reference/datamodel.html#newstyle
Mayank Jaiswal
2
Follow-up, um Sie motiviert zu halten, danke. Ich benutze das die ganze Zeit.
Chrisfauerbach
2
Im schlimmsten Fall würde dies einfach bedeuten, eine weitere Iteration durchzuführen, bevor das System ordnungsgemäß heruntergefahren wird. Der FalseWert wird nur einmal festgelegt und kann dann nur von False auf True geändert werden, sodass Mehrfachzugriff kein Problem darstellt.
Alceste_
52

Erstens bin ich mir nicht sicher, ob Sie einen zweiten Thread benötigen, um das festzulegen shutdown_flag.
Warum nicht direkt im SIGTERM-Handler einstellen?

Eine Alternative besteht darin, eine Ausnahme vom SIGTERMHandler auszulösen, die über den Stapel weitergegeben wird. Vorausgesetzt, Sie haben eine ordnungsgemäße Ausnahmebehandlung (z. B. mit with/ contextmanagerund try: ... finally:Blöcken), sollte dies ein recht elegantes Herunterfahren sein, ähnlich wie bei Ctrl+CIhrem Programm.

Beispielprogramm 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"

Nun sehen Sie das Ctrl+CVerhalten:

$ ./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

Diesmal sende ich es SIGTERMnach 4 Iterationen mit 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

Dieses Mal aktiviere ich meinen benutzerdefinierten SIGTERMHandler und sende ihn SIGTERM:

$ ./signals-test.py handle_signal
Hello
Iteration #1
Iteration #2
Iteration #3
Iteration #4
Goodbye
$ echo $?
0
Will Manley
quelle
3
"Warum nicht direkt im SIGTERM-Handler einstellen" --- weil der Worker-Thread an einer zufälligen Stelle unterbrechen würde. Wenn Sie mehrere Anweisungen in Ihre Worker-Schleife einfügen, sehen Sie, dass Ihre Lösung einen Worker an einer zufälligen Position beendet, wodurch der Job in einem unbekannten Zustand verbleibt.
Zerkms
Funktioniert gut für mich, auch im Docker-Kontext. Vielen Dank!
Marian
4
Wenn Sie nur ein Flag setzen und keine Ausnahme auslösen, ist dies dasselbe wie beim Thread. Die Verwendung von Thread ist hier also überflüssig.
Suor
28

Ich denke, Sie sind einer möglichen Lösung nahe.

Ausführen mainloopin einem separaten Thread und erweitern sie mit der Eigenschaft shutdown_flag. Das Signal kann signal.signal(signal.SIGTERM, handler)im Haupt-Thread abgefangen werden (nicht in einem separaten Thread). Der Signalhandler sollte shutdown_flagauf True gesetzt sein und warten, bis der Thread beendet istthread.join()

Moliware
quelle
4
Ja, ein getrennter Thread ist, wie ich es endlich gelöst habe, danke
zerkms
7
Threads sind hier nicht erforderlich. In einem Programm mit einem Thread selbst können Sie zuerst einen Signalhandler registrieren (das Registrieren eines Signalhandlers blockiert nicht) und dann den Mainloop schreiben. Die Signalhandlerfunktion sollte ein Flag setzen, wenn und die Schleife nach diesem Flag suchen soll. Ich habe eine Klasse - basierte Lösung für das gleiche klebt hier .
Mayank Jaiswal
2
Auf keinen Fall ist ein zweiter Thread erforderlich. Signalhandler registrieren.
Oneloop
hilfreiche Seite: g-loaded.eu/2016/11/24/…
Kamil Sindi
26

Hier ist ein einfaches Beispiel ohne Threads oder Klassen.

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
Gedankenarray
quelle
11

Basierend auf den vorherigen Antworten habe ich einen Kontextmanager erstellt, der vor Sigint und Sigterm schützt.

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")
Okke
quelle
4

Am einfachsten für mich gefunden. Hier ein Beispiel mit Gabel zur Verdeutlichung, dass dieser Weg für die Flusskontrolle nützlich ist.

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)
Kron
quelle
0

Die einfachste Lösung, die ich gefunden habe und die sich von den obigen Antworten inspirieren lässt, ist

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)
loretoparisi
quelle