Làm thế nào để xử lý tín hiệu SIGTERM một cách duyên dáng?


197

Giả sử chúng ta có một daemon tầm thường như vậy được viết bằng python:

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

mainloop()

và chúng tôi daemonize nó bằng cách sử dụng start-stop-daemonmà theo mặc định gửi tín hiệu SIGTERM( TERM) --stop.

Hãy giả sử bước hiện tại được thực hiện là #2. Và tại thời điểm này, chúng tôi đang gửi TERMtín hiệu.

Điều gì xảy ra là việc thực thi chấm dứt ngay lập tức.

Tôi thấy rằng tôi có thể xử lý sự kiện tín hiệu bằng cách sử dụng signal.signal(signal.SIGTERM, handler)nhưng điều này là nó vẫn làm gián đoạn quá trình thực thi hiện tại và chuyển điều khiển sang handler.

Vì vậy, câu hỏi của tôi là - có thể không làm gián đoạn việc thực hiện hiện tại mà xử lý TERMtín hiệu trong một luồng riêng biệt (?) Để tôi có thể đặt shutdown_flag = Truesao cho mainloop()có cơ hội dừng lại một cách duyên dáng?


2
Tôi đã làm những gì bạn đang yêu cầu cho trước bằng cách sử dụng signalfdvà mặt nạ ra giao SIGTERMcho quá trình này.
Eric Urban

Câu trả lời:


276

Một lớp dựa trên sạch để sử dụng giải pháp:

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
Cảm ơn ý kiến! Tôi đã sử dụng một cách tiếp cận sửa đổi trong bảo vệ khởi động lại. github.com/ryran/reboot-guard/blob/master/rguard#L284:L304
rsaw

7
Đây là câu trả lời tốt nhất (không yêu cầu chủ đề) và nên là phương pháp thử đầu tiên được ưa thích.
jose.angel.jimenez

2
@ Mausy5043 Python cho phép bạn không có dấu ngoặc đơn để xác định các lớp. Mặc dù nó hoàn toàn tốt cho python 3.x, nhưng đối với python 2.x, cách tốt nhất là sử dụng "class XYZ (object):". Lý do là: docs.python.org/2/reference/datamodel.html#newstyle
Mayank Jaiswal

2
Theo dõi, để giữ cho bạn có động lực, cảm ơn bạn. Tôi sử dụng tất cả các thời gian.
chrisfauerbach

2
Trong trường hợp xấu hơn, điều đó chỉ đơn giản có nghĩa là thực hiện một lần lặp khác trước khi tắt một cách duyên dáng. Các Falsegiá trị được thiết lập một lần duy nhất, và sau đó nó chỉ có thể đi từ False True rất đa truy nhập không phải là một vấn đề.
Alceste_

52

Đầu tiên, tôi không chắc chắn rằng bạn cần một luồng thứ hai để thiết lập shutdown_flag.
Tại sao không đặt nó trực tiếp trong trình xử lý SIGTERM?

Một cách khác là đưa ra một ngoại lệ từ SIGTERMtrình xử lý, sẽ được truyền lên ngăn xếp. Giả sử bạn đã xử lý ngoại lệ phù hợp (ví dụ: với with/ contextmanagertry: ... finally:các khối), đây sẽ là một tắt máy khá duyên dáng, tương tự như khi bạn tham gia Ctrl+Cchương trình của mình.

Chương trình ví dụ 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"

Bây giờ hãy xem Ctrl+Chành vi:

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

Lần này tôi gửi nó SIGTERMsau 4 lần lặp với 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

Lần này tôi kích hoạt SIGTERMtrình xử lý tùy chỉnh của mình và gửi nó SIGTERM:

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

3
"Tại sao không đặt nó trực tiếp trong trình xử lý SIGTERM" --- bởi vì luồng công nhân sẽ ngắt trên một vị trí ngẫu nhiên. Nếu bạn đặt nhiều câu lệnh vào vòng lặp worker của bạn, bạn sẽ thấy rằng giải pháp của bạn chấm dứt một worker ở một vị trí ngẫu nhiên, khiến công việc ở trạng thái không xác định.
zerkms

Hoạt động tốt cho tôi, cũng trong bối cảnh Docker. Cảm ơn!
Mary

4
Nếu bạn chỉ đặt cờ và không tăng ngoại lệ thì nó sẽ giống như với luồng. Vì vậy, sử dụng chủ đề là không cần thiết ở đây.
Suor

28

Tôi nghĩ rằng bạn đang ở gần một giải pháp có thể.

Thực hiện mainlooptrong một chủ đề riêng biệt và mở rộng nó với tài sản shutdown_flag. Tín hiệu có thể được bắt với signal.signal(signal.SIGTERM, handler)trong luồng chính (không phải trong một luồng riêng). Trình xử lý tín hiệu phải được đặt shutdown_flagthành True và đợi luồng kết thúc bằngthread.join()


4
Đúng, một chủ đề riêng biệt là cách cuối cùng tôi đã giải quyết nó, cảm ơn
zerkms

7
Chủ đề không được yêu cầu ở đây. Trong một chương trình luồng đơn, trước tiên bạn có thể đăng ký trình xử lý tín hiệu (đăng ký trình xử lý tín hiệu là không chặn) và sau đó viết mainloop. Chức năng xử lý tín hiệu nên đặt cờ khi và vòng lặp sẽ kiểm tra cờ này. Tôi đã dán một giải pháp dựa trên lớp cho cùng ở đây .
Mayank Jaiswal 16/07/2015

2
Không có cách nào có một chủ đề thứ hai là cần thiết. Đăng ký xử lý tín hiệu.
oneloop


26

Đây là một ví dụ đơn giản không có chủ đề hoặc các lớp.

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

Dựa trên các câu trả lời trước đó, tôi đã tạo ra một trình quản lý bối cảnh bảo vệ khỏi sigintm và 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

Tìm cách dễ nhất cho tôi. Dưới đây là một ví dụ với ngã ba cho rõ ràng rằng cách này hữu ích cho việc kiểm soát dòng chảy.

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

Giải pháp đơn giản nhất mà tôi đã tìm thấy, lấy cảm hứng từ các câu trả lời ở trên là

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)
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.