Автор оригинала: 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-это следующий шаг. Это требует, чтобы обработчик событий различал различные сигналы и имел отдельную процедуру для всех из них.