Обработка сигналов Unix в Python

Системы UNIX и Linux предлагают специальные механизмы для связи между каждым отдельным процессом. Одним из этих механизмов являются сигналы и относятся к различным методам связи между процессами (Inter Process Communication, сокращенно IPC).

Короче говоря, сигналы – это программные прерывания, которые отправляются программе (или процессу) для уведомления программы о значимых событиях или запросах к программе, чтобы запустить специальную кодовую последовательность. Программа, которая получает сигнал, либо останавливает, либо продолжает выполнение своих инструкций, завершает работу с дампом памяти или без нее, или даже просто игнорирует сигнал.

Хотя это определено в стандарте POSIX, реакция на самом деле зависит от того, как разработчик написал сценарий и реализовал обработку сигналов.

В этой статье мы объясним, что такое сигналы, покажем, как отправить сигнал другому процессу из командной строки, а также как обработать полученный сигнал. Среди других модулей программный код в основном основан на сигнальном модуле.

Введение

В системах на базе UNIX существует три категории сигналов:

  • Системные сигналы (аппаратные и системные ошибки): SIGILL, SIGTRAP, SIGBUS, SIGFPE, SIGKILL, SIGSEGV, SIGXCPU, SIGXFSZ, SIGIO.
  • Сигналы устройства: SIGHUP, SIGINT, SIGPIPE, SIGALRM, SIGCHLD, SIGCONT, SIGSTOP, SIGTTIN, SIGTTOU, SIGURG, SIGWINCH, SIGIO.
  • Определяемые пользователем сигналы: SIGQUIT, SIGABRT, SIGUSR1, SIGUSR2, SIGTERM.

Каждый сигнал представлен целым числом, а список доступных сигналов сравнительно длинный и не согласован между различными вариантами UNIX и Linux. В системе Debian GNU и Linux команда kill -l отображает следующий список сигналов:

$ kill -l
 1) SIGHUP   2) SIGINT   3) SIGQUIT  4) SIGILL   5) SIGTRAP
 6) SIGABRT  7) SIGBUS   8) SIGFPE   9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG  24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF 28) SIGWINCH    29) SIGIO   30) SIGPWR
31) SIGSYS  34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

Сигналы с 1 по 15 примерно стандартизированы и имеют следующее значение в большинстве систем Linux:

  • (SIGHUP): разорвать соединение или перезагрузить конфигурацию для демонов;
  • (SIGINT): прервать сеанс с диалоговой станции;
  • (SIGQUIT): завершить сеанс с диалоговой станции;
  • (SIGILL): была выполнена недопустимая инструкция;
  • (SIGTRAP): выполнить одну инструкцию (trap);
  • (SIGABRT): аварийное завершение;
  • (SIGBUS): ошибка системной шины;
  • (SIGFPE): ошибка с плавающей запятой;
  • (SIGKILL): немедленно завершить процесс;
  • (SIGUSR1): определяемый пользователем сигнал;
  • (SIGSEGV): ошибка сегментации из-за незаконного доступа к сегменту памяти;
  • (SIGUSR2): определяемый пользователем сигнал;
  • (SIGPIPE): запись в канал, и никто из него не читает;
  • (SIGALRM): таймер отключен (тревога);
  • (SIGTERM): мягко завершить процесс.

Чтобы отправить сигнал процессу в терминале Linux, вы вызываете команду kill с номером сигнала (или именем сигнала) из приведенного выше списка и идентификатором процесса (pid). В следующем примере команда отправляет сигнал 15 (SIGTERM) процессу с идентификатором pid 12345:

$ kill -15 12345

Эквивалентный способ – использовать имя сигнала вместо его номера:

$ kill -SIGTERM 12345

Какой способ вы выберете, зависит от того, что вам удобнее. Оба способа имеют одинаковый эффект. В результате процесс получает сигнал SIGTERM и немедленно завершается.

Использование библиотеки

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

import signal

Захват и правильная реакция на полученный сигнал осуществляется функцией обратного вызова – так называемым обработчиком сигнала. Достаточно простой обработчик сигнала с именем receiveSignal() можно записать следующим образом:

def receiveSignal(signalNumber, frame):
    print('Received:', signalNumber)
    return

Этот обработчик сигналов не делает ничего, кроме сообщения номера полученного сигнала. Следующим шагом является регистрация сигналов, которые перехватывает обработчик. Для программ Python все сигналы (кроме 9, SIGKILL) могут быть перехвачены в вашем скрипте:

if __name__ == '__main__':
    # register the signals to be caught
    signal.signal(signal.SIGHUP, receiveSignal)
    signal.signal(signal.SIGINT, receiveSignal)
    signal.signal(signal.SIGQUIT, receiveSignal)
    signal.signal(signal.SIGILL, receiveSignal)
    signal.signal(signal.SIGTRAP, receiveSignal)
    signal.signal(signal.SIGABRT, receiveSignal)
    signal.signal(signal.SIGBUS, receiveSignal)
    signal.signal(signal.SIGFPE, receiveSignal)
    #signal.signal(signal.SIGKILL, receiveSignal)
    signal.signal(signal.SIGUSR1, receiveSignal)
    signal.signal(signal.SIGSEGV, receiveSignal)
    signal.signal(signal.SIGUSR2, receiveSignal)
    signal.signal(signal.SIGPIPE, receiveSignal)
    signal.signal(signal.SIGALRM, receiveSignal)
    signal.signal(signal.SIGTERM, receiveSignal)

Затем мы добавляем информацию о текущем процессе и определяем идентификатор процесса с помощью метода getpid() из модуля os. В бесконечном цикле while ждем входящих сигналов. Мы реализуем это с помощью еще двух модулей в Python – os и time. Мы также импортируем их в начале нашего скрипта:

import os
import time

В цикле while нашей основной программы оператор печати выводит «Ожидание …». Вызов функции time.sleep() заставляет программу ждать три секунды.

    # output current process id
    print('My PID is:', os.getpid())

    # wait in an endless loop for signals 
    while True:
        print('Waiting...')
        time.sleep(3)

Наконец, нам нужно протестировать наш скрипт. Сохранив скрипт как signal-handling.py, мы можем вызвать его в терминале следующим образом:

$ python3 signal-handling.py 
My PID is: 5746
Waiting...
...

Во втором окне терминала мы отправляем сигнал процессу. Мы идентифицируем наш первый процесс – по идентификатору процесса, указанному на экране выше.

$ kill -1 5746

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

...
Received: 1
...

Игнорирование сигналов

Модуль сигналов определяет способы игнорирования полученных сигналов. Для этого сигнал должен быть связан с предопределенной функцией signal.SIG_IGN. Пример ниже демонстрирует это, и в результате программа Python больше не может быть прервана нажатием CTRL + C. Для остановки скрипта реализован альтернативный способ – сигнал SIGUSR1 завершает скрипт. Кроме того, вместо бесконечного цикла мы используем метод signal.pause(). Он просто ждет получения сигнала.

import signal
import os
import time

def receiveSignal(signalNumber, frame):
    print('Received:', signalNumber)
    raise SystemExit('Exiting')
    return

if __name__ == '__main__':
    # register the signal to be caught
    signal.signal(signal.SIGUSR1, receiveSignal)

    # register the signal to be ignored
    signal.signal(signal.SIGINT, signal.SIG_IGN)

    # output current process id
    print('My PID is:', os.getpid())

    signal.pause()

Правильная обработка сигналов

Обработчик сигналов, который мы использовали до сих пор, довольно прост и сообщает о принятом сигнале. Это показывает нам, что интерфейс нашего скрипта Python работает нормально. Давайте улучшим это.

Улавливание сигнала уже является хорошей основой, но требует некоторого улучшения для соответствия правилам стандарта POSIX. Для большей точности каждый сигнал требует соответствующей реакции. Это означает, что обработчик сигнала в нашем скрипте Python должен быть расширен определенной процедурой для каждого сигнала. Это работает лучше всего, если мы понимаем, что делает сигнал и какова обычная реакция. Процесс, который получает сигнал 1, 2, 9 или 15, завершается. В любом другом случае ожидается также запись дампа ядра.

До сих пор мы реализовали единую процедуру, которая охватывает все сигналы и обрабатывает их одинаково. Следующим шагом является реализация индивидуальной процедуры для каждого сигнала. Следующий пример кода демонстрирует это для сигналов 1 (SIGHUP) и 15 (SIGTERM).

def readConfiguration(signalNumber, frame):
    print ('(SIGHUP) reading configuration')
    return

def terminateProcess(signalNumber, frame):
    print ('(SIGTERM) terminating the process')
    sys.exit()

Две указанные выше функции связаны с сигналами следующим образом:

    signal.signal(signal.SIGHUP, readConfiguration)
    signal.signal(signal.SIGTERM, terminateProcess)

Запуск сценария Python и отправка сигнала 1 (SIGHUP), за которым следует сигнал 15 (SIGTERM) командами UNIX kill -1 16640 и kill -15 16640, приводит к следующему результату:

$ python3 daemon.py
My PID is: 16640
Waiting...
Waiting...
(SIGHUP) reading configuration
Waiting...
Waiting...
(SIGTERM) terminating the process

Скрипт получает сигналы и правильно их обрабатывает. Для наглядности это весь скрипт:

import signal
import os
import time
import sys

def readConfiguration(signalNumber, frame):
    print ('(SIGHUP) reading configuration')
    return

def terminateProcess(signalNumber, frame):
    print ('(SIGTERM) terminating the process')
    sys.exit()

def receiveSignal(signalNumber, frame):
    print('Received:', signalNumber)
    return

if __name__ == '__main__':
    # register the signals to be caught
    signal.signal(signal.SIGHUP, readConfiguration)
    signal.signal(signal.SIGINT, receiveSignal)
    signal.signal(signal.SIGQUIT, receiveSignal)
    signal.signal(signal.SIGILL, receiveSignal)
    signal.signal(signal.SIGTRAP, receiveSignal)
    signal.signal(signal.SIGABRT, receiveSignal)
    signal.signal(signal.SIGBUS, receiveSignal)
    signal.signal(signal.SIGFPE, receiveSignal)
    #signal.signal(signal.SIGKILL, receiveSignal)
    signal.signal(signal.SIGUSR1, receiveSignal)
    signal.signal(signal.SIGSEGV, receiveSignal)
    signal.signal(signal.SIGUSR2, receiveSignal)
    signal.signal(signal.SIGPIPE, receiveSignal)
    signal.signal(signal.SIGALRM, receiveSignal)
    signal.signal(signal.SIGTERM, terminateProcess)

    # output current process id
    print('My PID is:', os.getpid())

    # wait in an endless loop for signals 
    while True:
        print('Waiting...')
        time.sleep(3)

Заключение

Используя модуль сигналов и соответствующий обработчик событий, относительно легко улавливать сигналы. Следующим шагом является знание значения различных сигналов и правильная реакция, как определено в стандарте POSIX. Это требует, чтобы обработчик событий различал разные сигналы и имел отдельную процедуру для всех из них.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *