Сигналы — это числовые
сообщения, отправляемые запущенным приложениям операционной системой, другими
приложениями или пользователем. Сигналы, как правило, ожидают от приложения
какой-то определённой реакции, например «корректно заверши работу»,
«приостановись, чтобы я мог тебя перевести в фоновый режим» или «умри!».
Обычно для отправки сигналов приложениям
используется программа kill, но некоторые сигналы можно
отправлять и при помощи клавиатурных комбинаций, например таких, как знакомые
многим Ctrl+C или Ctrl+Z.
Сигналы обрабатываются «каскадно». То есть,
сигнал отправляется приложению и, если приложение не обработало поступивший
сигнал, то он возвращается обратно оболочке или операционной системе. Некоторые
типы сигналов в принципе не могут обрабатываться приложениями. Например, сигнал
SIGKILL вообще не доставляется приложению, а перехватывается
операционной системой, которая немедленно завершает работу приложения, которому
сигнал был адресован.
Давайте начнём изучение сигналов с того, что
узнаем, какие они бывают. Для этого можно воспользоваться командой kill
-l:
Большинство перечисленных сигналов нам
неинтересны и используются редко. Среди часто используемых я бы отметил
следующие. SIGHUP — обычно отправляется в момент выхода
пользователя из системы; SIGINT (именно он посылается
приложению при нажатии Ctrl+C) - запрос прервать работу;
SIGKILL — немедленное завершение работы процесса операционной
системой; SIGTSTP — отправляется комбинацией
Ctrl+Z; SIGCONT — это тот сигнал, который
отправляется приложению из оболочки командами fg и bg
после приостановки его работы сигналом SIGTSTP;
SIGWINCH используется в оконных системах для отправки
приложению уведомления об изменении размеров окна; сигналы SIGUSR1
и SIGUSR2 используются для организации межпроцессного
взаимодействия.
Давайте, наконец, попробуем написать немного
кода. В сценариях оболочки сигналы перехватываются при помощи встроенной команды
trap. Ниже на примере показано, как обычно это делается:
Видите здесь бесконечный цикл? Он практически не
использует ресурсов, поскольку практически всё время «спит» и будет выполняться
вечно или до тех пор, пока вы не остановите его.
Давайте рассмотрим более гибкий способ работы с
сигналами в сценариях оболочки при помощи функций:
sigquit(){echo "signal QUIT received"
}
sigint(){echo "signal INT received, script ending"
exit 0
}trap 'sigquit' QUIT
trap 'sigint' INT
trap ':' HUP # ignore the specified signalsecho "test script started. My PID is $$"
while/usr/bin/true ; dosleep 30
done
Запустите приведённый выше код и из другого
терминала попробуйте отправить ему несколько разных сигналов:
Ниже показано, как скрипт будет реагировать на
отправленные ему сигналы:
$ ./test.sh
test script started. My PID is 25309
signal QUIT received
signal INT received, script ending
Вооружившись этим примером, давайте посмотрим,
как обрабатывать более сложные сигналы, например отправляемые по нажатию
Ctrl+Z.
Создавая сложные скрипты, вы обнаружите, что
попадается масса моментов, когда необходимо игнорировать поступивший сигнал
TSTP (он же SIGTSTP, Ctrl+Z
или сигнал номер 18), а иногда — перезапустить работу
сценария.
Начиная решать подобную задачу, сперва создадим
функцию, которая не только обрабатывает определённый сигнал, но и затем
«отключает» сама себя и не будет срабатывать при последующих вызовах:
Вызовом trap — TSTP в любом
месте скрипта вы отключаете назначенный ранее обработчик указанного сигнала.
Теперь, если у нас в коде есть строка
trap 'sigtstp' TSTP
в том месте, где мы хотим включить игнорирование
нажатия Ctrl+Z, то она заставит скрипт не реагировать на
Ctrl+Z, но только один раз. В следующий раз, когда вы нажмёте
Ctrl+Z, сигнал будет обработан так же, как обычно.
Игнорирование Ctrl+Z часто
используется тогда, когда ваш скрипт ещё не готов «правильно» на него
реагировать. Просто вставьте этот код, там где требуется такое поведение
сценария:
trap : TSTP # ignore Ctrl-Z requests
Затем, когда ваш скрипт будет готов к обработке
Ctrl+Z, используйте следующую конструкцию, которая вернёт
обычную реакцию сценария на сигнал останова:
trap - TSTP # allow Ctrl-Z requests
Эксперименты показывают, что имеет место быть
какая-то странноватая буферизация вывода терминалом, связанная с сигналом
SIGTSTP, так что не удивляйтесь, если, перехватывая этот
сигнал, вы вдруг не получите вывод от вашего скрипта вплоть до момента его
завершения.
Давайте рассмотрим более практичный пример.
Представим, что у вас есть некий административный скрипт, который работает в
режиме демона. Иногда у вас возникает необходимость внести какие-либо изменения
в его конфигурационный файл, но при этом нужно сделать так, чтобы файл
конфигурации перечитывался скриптом в процессе его работы, а не только при
запуске. Такая возможность позволяет значительно сократить время
неработоспособности демона, поскольку его не нужно останавливать, а затем заново
запускать.
При решении этой задачи мы будем использовать
сигнал SIGUSR1, поскольку он обычно для этого и используется.
Считывание файла конфигурации может быть
реализовано достаточно просто:
. $config
Вспомним, что использование команды «точка»
приведёт к тому, что переменные, определённые во внешнем файле, станут
доступными в текущей оболочке. Вместо точки можно использовать команду
source.
Вот так выглядит наш экспериментальный
скрипт:
#!/bin/bash
config="our.config.file"
sigusr1(){echo "(SIGUSR1: re-reading config file)"
. $config
}trap sigusr1 USR1 # catch -USR1 signalecho "Daemon started. Assigned PID is $$"
. $config # read it first timewhile/usr/bin/true; doecho "Target number = $number"
sleep 5
donetrap - USR1 # reset to be elegantexit 0
Начнём с того, что определим в нашем
конфигурационном файле переменную number со значением
5. Затем, через 10-15 секунд изменим значение переменной на
1. До тех пор, пока мы не пошлём скрипту сигнал USR1, он будет
выводить начальное значение переменной:
$ ./test2.sh
Daemon started. Assigned PID is 25843
Target number = 5
Target number = 5
Target number = 5
Тем временем, когда значение переменной в
конфигурационном файле уже изменено, из другого терминала пошлём нашему скрипту
сигнал:
$ kill -USR1 25843
Теперь посмотрим, что происходит в терминале, где
выполняется наш скрипт:
(SIGUSR1: re-reading config file)
Target number = 1
Target number = 1
Классно, не правда ли?
Надеюсь, что сегодняшнее наше исследование работы
с сигналами в сценариях оболочки пригодится вам в работе. Лично я многому
научился, пока разбирался со всем, о чём здесь написано. Однако, для меня всё
ещё остаётся загадкой, каким образом возобновить вывод скрипта после того, как
он перехватывает сигнал SIGTSTP и я надеюсь, что читатели подскажут мне, как это
сделать.