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

Системы UNIX / Linux предлагают специальные механизмы для связи между каждым отдельным процессом. Одним из этих механизмов являются сигналы [https://en.wikipedia.org/wiki/Signal_(IPC)], которые относятся к различным методам связи между процессами (Inter Process Communication, сокращенно IPC). Короче говоря, сигналы - это программные прерывания, которые отправляются программе (или процессу) для уведомления программы о значимых событиях или запросах к программе, чтобы запустить специальную кодовую последовательность. А

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

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

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

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

Знакомство с сигналами

В системах на базе 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:

  • 1 (SIGHUP): разорвать соединение или перезагрузить конфигурацию для демонов
  • 2 (SIGINT): прервать сеанс с диалоговой станции
  • 3 (SIGQUIT): завершить сеанс с диалоговой станции
  • 4 (SIGILL): была выполнена недопустимая инструкция
  • 5 (SIGTRAP): выполнить одну инструкцию (trap)
  • 6 (SIGABRT): аварийное завершение
  • 7 (SIGBUS): ошибка системной шины
  • 8 (SIGFPE): ошибка с плавающей запятой
  • 9 (SIGKILL): немедленно завершить процесс
  • 10 (SIGUSR1): определяемый пользователем сигнал
  • 11 (SIGSEGV): ошибка сегментации из-за незаконного доступа к сегменту памяти
  • 12 (SIGUSR2): определяемый пользователем сигнал
  • 13 (SIGPIPE): запись в канал, и никто из него не читает
  • 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 

Захват и правильная реакция на полученный сигнал осуществляется функцией обратного вызова - так называемым обработчиком сигнала. Достаточно простой обработчик сигнала с именем 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 . Мы также импортируем их в начале нашего скрипта Python:

 import os 
 import time 

В while петли нашей основной программы оператор печати выдает «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 

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

comments powered by Disqus