Обработка сигналов в сценариях оболочки






Сигналы — это числовые сообщения, отправляемые запущенным приложениям операционной системой, другими приложениями или пользователем. Сигналы, как правило, ожидают от приложения какой-то определённой реакции, например «корректно заверши работу», «приостановись, чтобы я мог тебя перевести в фоновый режим» или «умри!».


Обычно для отправки сигналов приложениям используется программа kill, но некоторые сигналы можно отправлять и при помощи клавиатурных комбинаций, например таких, как знакомые многим  Ctrl+C или Ctrl+Z.

Сигналы обрабатываются «каскадно». То есть, сигнал отправляется приложению и, если приложение не обработало поступивший сигнал, то он возвращается обратно оболочке или операционной системе. Некоторые типы сигналов в принципе не могут обрабатываться приложениями. Например, сигнал SIGKILL вообще не доставляется приложению, а перехватывается операционной системой, которая немедленно завершает работу приложения, которому сигнал был адресован.

Давайте начнём изучение сигналов с того, что узнаем, какие они бывают. Для этого можно воспользоваться командой kill -l:

$ kill -l
1)  SIGHUP    2) SIGINT      3) SIGQUIT   4) SIGILL
5)  SIGTRAP   6) SIGABRT     7) SIGEMT    8) SIGFPE
9)  SIGKILL  10) SIGBUS     11) SIGSEGV  12) SIGSYS
13) SIGPIPE  14) SIGALRM    15) SIGTERM  16) SIGURG
17) SIGSTOP  18) SIGTSTP    19) SIGCONT  20) SIGCHLD
21) SIGTTIN  22) SIGTTOU    23) SIGIO    24) SIGXCPU
25) SIGXFSZ  26) SIGVTALRM  27) SIGPROF  28) SIGWINCH
29) SIGINFO  30) SIGUSR1    31) SIGUSR2

Большинство перечисленных сигналов нам неинтересны и используются редко. Среди часто используемых я бы отметил следующие. SIGHUP — обычно отправляется в момент выхода пользователя из системы; SIGINT (именно он посылается приложению при нажатии Ctrl+C) - запрос прервать работу; SIGKILL — немедленное завершение работы процесса операционной системой; SIGTSTP — отправляется комбинацией Ctrl+Z; SIGCONT — это тот сигнал, который отправляется приложению из оболочки командами fg и bg после приостановки его работы сигналом SIGTSTP; SIGWINCH используется в оконных системах для отправки приложению уведомления об изменении размеров окна; сигналы SIGUSR1 и SIGUSR2 используются для организации межпроцессного взаимодействия.

Давайте, наконец, попробуем написать немного кода. В сценариях оболочки сигналы перехватываются при помощи встроенной команды trap. Ниже на примере показано, как обычно это делается:

trap 'echo "Ctrl-C Ignored" ' INT

Как это использовать в сценарии? Очень просто!

#!/bin/bash

trap 'echo " - Ctrl-C ignored" ' INT
while /usr/bin/true ; do
  sleep 30
done
exit 0

Видите здесь бесконечный цикл? Он практически не использует ресурсов, поскольку практически всё время «спит» и будет выполняться вечно или до тех пор, пока вы не остановите его.

Давайте рассмотрим более гибкий способ работы с сигналами в сценариях оболочки при помощи функций:

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 signals
echo "test script started. My PID is $$"
while /usr/bin/true ; do
  sleep 30
done

Запустите приведённый выше код и из другого терминала попробуйте отправить ему несколько разных сигналов:

$ kill -HUP  25309
$ kill -QUIT 25309
$ kill -INT  25309

Ниже показано, как скрипт будет реагировать на отправленные ему сигналы:

$ ./test.sh
test script started. My PID is 25309
signal QUIT received
signal INT received, script ending

Вооружившись этим примером, давайте посмотрим, как обрабатывать более сложные сигналы, например отправляемые по нажатию Ctrl+Z.

Создавая сложные скрипты, вы обнаружите, что попадается масса моментов, когда необходимо игнорировать поступивший сигнал TSTP (он же SIGTSTP, Ctrl+Z или сигнал номер 18), а иногда — перезапустить работу сценария.

Начиная решать подобную задачу, сперва создадим функцию, которая не только обрабатывает определённый сигнал, но и затем «отключает» сама себя и не будет срабатывать при последующих вызовах:

sigtstp()
{
  echo "SIGTSTP received"
  trap - TSTP
  echo "SIGTSTP standard handling restored"
}

Вызовом 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 signal

echo "Daemon started. Assigned PID is $$"

. $config               # read it first time

while /usr/bin/true; do
  echo "Target number = $number"
  sleep 5
done

trap - USR1             # reset to be elegant
exit 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 и я надеюсь, что читатели подскажут мне, как это сделать.

По мотивам LinuxJournal.Com