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