trap ДЕЙСТВИЕ СИГНАЛ...
Trap работает очень просто: при возникновении сигнала будет выполнено указанное действие. Если действие простое (последовательность команд, умещающаяся на одной строке), его можно указать прямо в аргументе trap
. Если не очень простое, то надо объявить функцию и поместить вызов этой функции в trap
.
Можно обрабатывать стандартные сигналы (их полный список выводится по trap -l
). Также доступны специфические для Bash: DEBUG
, RETURN
, ERR
, EXIT
.
На практике trap оказывается не такой уж простой штукой. Дьявол как всегда кроется в деталях. Сейчас покажу.
- Изучаем trap на простых примерах
- Переменные внутри trap
- Trap и функции
- Практическое применение trap
- Ограничения
Изучаем trap на простых примерах
Ко мне понимание пришло одновременно с этим демонстрационным скриптом:
Скрипт выводит “start”, спит одну минуту, затем выводит “end”. Если во время сна поступает один из обрабатываемых сигналов, то просто выводится соответствующее сообщение.
Тестируем:
$ ./trap_signal_demo.sh
start
end
trap EXIT
$ ./trap_signal_demo.sh &
start
$ kill -SIGINT %1
trap SIGINT
trap ERR
end
trap EXIT
$ ./trap_signal_demo.sh &
start
$ kill -SIGHUP %1
Обрыв терминальной линии
trap SIGHUP
trap ERR
end
trap EXIT
$ ./trap_signal_demo.sh &
start
$ kill -SIGQUIT %1
Выход (core dumped)
trap SIGQUIT
trap ERR
end
trap EXIT
$ ./trap_signal_demo.sh &
start
$ kill -SIGTERM %1
Завершено
trap SIGTERM
trap ERR
end
trap EXIT
Вывод реального терминала выглядит несколько иначе, я скрыл несущественные детали. Поясню что здесь происходило. Я запускал скрипт в фоне (&
после команды), затем командой kill
посылал сигнал только что запущенному процессу. Чтобы послать SIGINT
, не обязательно связываться с kill
, можно во время работы скрипта нажать Ctrl+C.
Если убрать из скрипта обработку прерывающих сигналов, то будет уже не так. SIGINT
, SIGHUP
, SIGTERM
не создают сигнал ERR
, а сразу ведут на выход:
$ ./trap_signal_demo.sh &
start
$ kill -SIGINT %1
trap EXIT
$ ./trap_signal_demo.sh &
start
$ kill -SIGHUP %1
trap EXIT
$ ./trap_signal_demo.sh &
start
$ kill -SIGQUIT %1
Выход (core dumped)
trap ERR
end
trap EXIT
$ ./trap_signal_demo.sh &
start
$ kill -SIGTERM %1
trap EXIT
SIGQUIT
создаёт ERR
, но на выход не ведёт. Никакой закономерности тут нет, просто так работают дефолтные обработчики сигналов. У каждого сигнала своя специфика.
Для игнорирования сигналов используется пустой trap
. Этот демонстрационный скрипт можно прервать только смертоносным сигналом SIGKILL
.
#!/bin/bash
trap '' SIGINT SIGTERM SIGHUP SIGQUIT
sleep 1m
Вернуть дефолтный обработчик сигнала тоже можно, пусть и не совсем очевидным способом:
trap - СИГНАЛ...
Вызов trap
без аргументов покажет все установленные обработчики сигналов. Это полезно при отладке.
Переменные внутри trap
Можно по-разному запихивать переменные внутрь trap
. Здесь я использую ls
, чтобы продемонстрировать обработку пробелов и false
для имитации возникновения ошибки. Обратите внимание на кавычки.
$ touch 'one two' 'three four'
$ ./variables_in_trap_demo1.sh
ls: невозможно получить доступ к three: Нет такого файла или каталога
ls: невозможно получить доступ к four: Нет такого файла или каталога
$ ./variables_in_trap_demo2.sh
ls: невозможно получить доступ к one: Нет такого файла или каталога
ls: невозможно получить доступ к two: Нет такого файла или каталога
$ ./variables_in_trap_demo3.sh
one two
$ ./variables_in_trap_demo4.sh
three four
$ ./variables_in_trap_demo5.sh
three four
Если вам непонятно почему так происходит, попробуйте запустить эти примеры с включенной опцией xtrace. Для этого добавьте в начале скрипта set -x
или set -o xtrace
. Или укажите в sha-bang’е bash -x
. Или запускайте скрипты командой bash -x СКРИПТ
.
Trap и функции
Наследует ли функция обработчики сигналов? Если да, то в какой момент: при вызове функции или при её объявлении?
$ ./trap_and_functions_demo1.sh
global trap:
trap -- 'echo trap EXIT' EXIT
trap -- 'echo trap SIGHUP' SIGHUP
trap -- 'echo trap SIGINT' SIGINT
trap -- 'echo trap SIGQUIT' SIGQUIT
trap -- 'echo trap SIGTERM' SIGTERM
trap -- 'echo trap ERR' ERR
trap -- 'echo trap RETURN' RETURN
call f1
f1 start
trap inside f1:
trap -- 'echo trap EXIT' EXIT
trap -- 'echo trap SIGHUP' SIGHUP
trap -- 'echo trap SIGINT' SIGINT
trap -- 'echo trap SIGQUIT' SIGQUIT
trap -- 'echo trap SIGTERM' SIGTERM
f1 exit
call f2
f2 start
trap inside f2:
trap -- 'echo trap EXIT' EXIT
trap -- 'echo trap SIGHUP' SIGHUP
trap -- 'echo trap SIGINT' SIGINT
trap -- 'echo trap SIGQUIT' SIGQUIT
trap -- 'echo trap SIGTERM' SIGTERM
f2 exit
end of script
trap EXIT
Из выхлопа видно, что функция наследует обработчики сигналов в момент вызова. В противном случае trap
первой функции был бы пустой.
Внимательные читатели заметили странное поведение обработчиков ERR и RETURN: они не наследуются! Чтобы получить обработку этих сигналов внутри функции, надо включить bash-опцию errtrace или объявить их явно в теле функции. Попробуйте сами.
Всё становится ещё запутаннее, если объявить функцию, которая выполняется в подоболочке (subshell). Этот пример отличается от предыдущего заменой фигурных скобочек на круглые:
$ ./trap_and_functions_demo2.sh
global trap:
trap -- 'echo trap EXIT' EXIT
trap -- 'echo trap SIGHUP' SIGHUP
trap -- 'echo trap SIGINT' SIGINT
trap -- 'echo trap SIGQUIT' SIGQUIT
trap -- 'echo trap SIGTERM' SIGTERM
trap -- 'echo trap ERR' ERR
trap -- 'echo trap RETURN' RETURN
call f
f start
trap inside f:
trap -- 'echo trap EXIT' EXIT
trap -- 'echo trap SIGHUP' SIGHUP
trap -- 'echo trap SIGINT' SIGINT
trap -- 'echo trap SIGQUIT' SIGQUIT
trap -- 'echo trap SIGTERM' SIGTERM
f exit
end of script
trap EXIT
Видим такое же поведение: наследуются все обработчики кроме ERR и RETURN. Однако если объявить какой-либо trap внутри функции, то наследование пропадает полностью!
$ ./trap_and_functions_demo3.sh
global trap:
trap -- 'echo trap EXIT' EXIT
trap -- 'echo trap SIGHUP' SIGHUP
trap -- 'echo trap SIGINT' SIGINT
trap -- 'echo trap SIGQUIT' SIGQUIT
trap -- 'echo trap SIGTERM' SIGTERM
trap -- 'echo trap ERR' ERR
trap -- 'echo trap RETURN' RETURN
call f
f start
trap inside f:
trap -- 'echo f trap ERR' ERR
f exit
end of script
trap EXIT
А как bash ведёт себя в обратной ситуации? Попадают ли обработчики сигналов из функций наружу? Да, если функция была объявлена без подоболочки. Пруф:
$ ./trap_and_functions_demo4.sh
global trap:
call f1
f1 start
trap inside f1:
trap -- 'echo f1 trap EXIT' EXIT
trap -- 'echo f1 trap SIGHUP' SIGHUP
trap -- 'echo f1 trap SIGINT' SIGINT
trap -- 'echo f1 trap SIGQUIT' SIGQUIT
trap -- 'echo f1 trap SIGTERM' SIGTERM
trap -- 'echo f1 trap ERR' ERR
trap -- 'echo f1 trap RETURN' RETURN
f1 exit
f1 trap RETURN
global trap:
trap -- 'echo f1 trap EXIT' EXIT
trap -- 'echo f1 trap SIGHUP' SIGHUP
trap -- 'echo f1 trap SIGINT' SIGINT
trap -- 'echo f1 trap SIGQUIT' SIGQUIT
trap -- 'echo f1 trap SIGTERM' SIGTERM
trap -- 'echo f1 trap ERR' ERR
trap -- 'echo f1 trap RETURN' RETURN
call f2
f2 start
trap inside f2:
trap -- 'echo f2 trap EXIT' EXIT
trap -- 'echo f2 trap SIGHUP' SIGHUP
trap -- 'echo f2 trap SIGINT' SIGINT
trap -- 'echo f2 trap SIGQUIT' SIGQUIT
trap -- 'echo f2 trap SIGTERM' SIGTERM
trap -- 'echo f2 trap ERR' ERR
trap -- 'echo f2 trap RETURN' RETURN
f2 exit
f2 trap EXIT
global trap:
trap -- 'echo f1 trap EXIT' EXIT
trap -- 'echo f1 trap SIGHUP' SIGHUP
trap -- 'echo f1 trap SIGINT' SIGINT
trap -- 'echo f1 trap SIGQUIT' SIGQUIT
trap -- 'echo f1 trap SIGTERM' SIGTERM
trap -- 'echo f1 trap ERR' ERR
trap -- 'echo f1 trap RETURN' RETURN
end of script
f1 trap EXIT
Надеюсь эти примеры внесли ясность, а не запутали вас ещё больше.
Практическое применение trap
В реальной жизни вам вряд ли придётся писать такие запутанные обработчики. Обычно всё сводится к двум сценариям.
Блокировка скрипта lock-файлом:
Удаление временных файлов и подчистка за собой.
Простой однострочный вариант:
С использованием функции:
К сожалению эта система не даёт 100% надёжности. Trap сработает при завершении скрипта любым из стандартных способов:
- при нормальном завершении,
- при возникновении ошибки при включённой опцией errexit,
- при получении прерывающего сигнала, который может быть обработан.
но не сработает, если:
- скрипт был убит сигналом SIGKILL,
- пришёл OOM-killer и убил ваш процесс,
- у компьютера внезапно отобрали питание.
Ограничения
Bash умеет в рекурсию, но не вызывает обработчик сигнала, уже находясь в нём. Похоже, что в такой ситуации он вызывает дефолтный обработчик. Посмотрим:
$ ./trap_inside_trap.sh
trapper begin
trap -- 'trapper' ERR
trapper end
end
А если с подоболочкой? То же самое. Скорее всего так было сделано чтобы избежать зацикливания.