Bagaimana cara memproses sinyal SIGTERM dengan anggun?

197

Mari kita asumsikan kita memiliki daemon sepele yang ditulis dengan python:

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

mainloop()

dan kami mengubahnya menggunakan start-stop-daemonyang secara default mengirimkan sinyal SIGTERM( TERM) aktif --stop.

Misalkan langkah saat ini dilakukan adalah #2. Dan saat ini kami sedang mengirim TERMsinyal.

Apa yang terjadi adalah bahwa eksekusi segera berakhir.

Saya telah menemukan bahwa saya dapat menangani acara sinyal menggunakan signal.signal(signal.SIGTERM, handler)tetapi masalahnya adalah masih mengganggu eksekusi saat ini dan melewati kontrol handler.

Jadi, pertanyaan saya adalah - apakah mungkin untuk tidak menghentikan eksekusi saat ini tetapi menangani TERMsinyal di utas yang terpisah (?) Sehingga saya dapat mengatur shutdown_flag = Truesehingga mainloop()memiliki kesempatan untuk berhenti dengan anggun?

zerkms
sumber
2
Saya melakukan apa yang Anda minta sebelumnya dengan menggunakan signalfddan menutupi pengiriman SIGTERMke proses.
Eric Urban

Jawaban:

277

Solusi bersih untuk menggunakan berbasis kelas:

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
sumber
1
Terima kasih atas idenya! Saya menggunakan pendekatan yang dimodifikasi di reboot-guard. github.com/ryran/reboot-guard/blob/master/rguard#L284:L304
rsaw
7
Ini adalah jawaban terbaik (tidak perlu utas), dan harus menjadi pendekatan percobaan pertama yang disukai.
jose.angel.jimenez
2
@ Mausy5043 Python memungkinkan Anda untuk tidak memiliki tanda kurung untuk mendefinisikan kelas. Meskipun sangat baik untuk python 3.x, tetapi untuk python 2.x, praktik terbaik adalah dengan menggunakan "kelas XYZ (objek):". Alasannya adalah: docs.python.org/2/reference/datamodel.html#newstyle
Mayank Jaiswal
2
Tindak lanjut, untuk membuat Anda tetap termotivasi, terima kasih. Saya menggunakan ini sepanjang waktu.
chrisfauerbach
2
Dalam kasus yang lebih buruk, itu hanya berarti melakukan iterasi lain sebelum ditutup dengan anggun. The Falsenilai ditetapkan hanya sekali, dan kemudian hanya bisa pergi dari False ke True sehingga beberapa akses tidak masalah.
Alceste_
52

Pertama, saya tidak yakin Anda perlu utas kedua untuk mengatur shutdown_flag.
Mengapa tidak mengaturnya langsung di handler SIGTERM?

Alternatifnya adalah dengan menaikkan pengecualian dari SIGTERMhandler, yang akan diperbanyak ke atas tumpukan. Dengan asumsi Anda memiliki penanganan pengecualian yang tepat (misalnya dengan with/ contextmanagerdan try: ... finally:blok) ini harus menjadi shutdown yang cukup anggun, mirip dengan jika Anda ke Ctrl+Cprogram Anda.

Contoh program 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"

Sekarang lihat Ctrl+Cperilaku:

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

Kali ini saya kirim SIGTERMsetelah 4 iterasi dengan 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

Kali ini saya mengaktifkan SIGTERMhandler kustom saya dan mengirimkannya SIGTERM:

$ ./signals-test.py handle_signal
Hello
Iteration #1
Iteration #2
Iteration #3
Iteration #4
Goodbye
$ echo $?
0
Will Manley
sumber
3
"Mengapa tidak mengaturnya langsung di pengendali SIGTERM" --- karena utas pekerja akan menyela secara acak. Jika Anda memasukkan banyak pernyataan ke lingkaran pekerja Anda, Anda akan melihat bahwa solusi Anda mengakhiri pekerja pada posisi acak, yang membuat pekerjaan dalam keadaan yang tidak diketahui.
zerkms
Bekerja dengan baik untuk saya, juga dalam konteks Docker. Terima kasih!
Marian
4
Jika Anda hanya mengatur bendera dan tidak menaikkan pengecualian maka itu akan sama dengan utas. Jadi menggunakan utas berlebihan di sini.
Suor
28

Saya pikir Anda sudah dekat dengan solusi yang mungkin.

Jalankan mainloopdi utas terpisah dan rentangkan dengan properti shutdown_flag. Sinyal dapat ditangkap dengan signal.signal(signal.SIGTERM, handler)di utas utama (bukan di utas terpisah). Penangan sinyal harus diatur shutdown_flagke True dan menunggu utas berakhirthread.join()

moliware
sumber
4
Yap, utas terpisah adalah bagaimana saya akhirnya bisa menyelesaikannya, terima kasih
zerkms
7
Utas tidak diperlukan di sini. Dalam program berulir tunggal itu sendiri, Anda pertama-tama dapat mendaftarkan penangan sinyal (mendaftarkan penangan sinyal tidak menghalangi) dan kemudian menulis mainloop. Fungsi pengendali sinyal harus menetapkan flag kapan dan loop harus memeriksa flag ini. Saya telah menempelkan solusi berbasis kelas untuk hal yang sama di sini .
Mayank Jaiswal
2
Tidak perlu memiliki utas kedua. Daftarkan pengendali sinyal.
oneloop
halaman membantu: g-loaded.eu/2016/11/24/…
Kamil Sindi
26

Berikut adalah contoh sederhana tanpa utas atau kelas.

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
thoughtarray
sumber
11

Berdasarkan jawaban sebelumnya, saya telah membuat manajer konteks yang melindungi dari sigint dan 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")
Okke
sumber
4

Menemukan cara termudah untukku. Berikut ini contoh dengan garpu untuk kejelasan bahwa cara ini berguna untuk kontrol aliran.

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
sumber
0

Solusi paling sederhana yang saya temukan, mengambil inspirasi dari tanggapan di atas adalah

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
sumber