Рубрики
Без рубрики

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

Автор оригинала: Frank Hofmann.

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

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

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

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

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

Введение в сигналы

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

  • Системные сигналы (аппаратные и системные ошибки): SIGILL, SIGTRAP, SIGBUS, SIGFPE, SIGILL, SIGSEGV, SIGXCPU, SIGXFSZ, SIGIO

  • Сигналы устройства: SIGHUP, SIGINT, SIGPIPE, SIGALRM, SIGCHLD, SIGCONT, SIGTSTP, SIGTTIN, SIGTTOU, SIGURG, SIGWINCH, SIGIO

  • Пользовательские сигналы: SIGQUIT, SIGABRT, SIGUSR1, SIGUSR2, SIGTERM

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

$ 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:

  • 1 (SIGHUP): завершите соединение или перезагрузите конфигурацию для демонов.
  • 2 (SIGINT): прервать сеанс с диалоговой станции
  • 3 (SIGQUIT): завершение сеанса с диалоговой станции
  • 4 (СИГИЛЛ): незаконная инструкция была выполнена
  • 5 (SIGTRAP): выполните одну инструкцию (trap)
  • 6 (SIGABRT): ненормальное завершение
  • 7 (SIGBUS): ошибка на системной шине
  • 8 (SIGFPE): ошибка с плавающей запятой
  • 9 (SIGKILL): немедленно завершите процесс
  • 10 (SIGUSR1): пользовательский сигнал
  • 11 (SIGSEGV): ошибка сегментации из-за незаконного доступа к сегменту памяти
  • 12 (SIGUSR2): пользовательский сигнал
  • 13 (СИГПАЙП): пишу в трубку, и никто не читает из нее
  • 14 (SIGALRM): таймер завершен (сигнал тревоги)
  • 15 (SIGTERM): завершите процесс мягким способом

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

$ kill -15 12345

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

$ kill -SIGTERM 12345

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

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

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

import signal

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

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)

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

import os
import time

В цикле while нашей основной программы оператор print выводит “Waiting…”. Вызов функции 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...
...

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

$ kill -1 5746

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

...
Received: 1
...

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

Сигнальный модуль определяет способы игнорирования принятых сигналов. Для этого сигнал должен быть связан с предопределенной функцией signal.SIG_IGN . Приведенный ниже пример демонстрирует это, и в результате программа Python больше не может быть прервана CTRL+C . Чтобы остановить скрипт Python, в примере скрипта был реализован альтернативный способ – сигнал SIGUSR1 завершает скрипт Python. Кроме того, вместо бесконечного цикла мы используем метод 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)

Дальнейшее чтение

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