Ловля сигналов в Bash и корректное завершение скриптов

Быстрые ссылки
- Сигналы и процессы
- Список сигналов
- Сигналы в командной строке
- Ловля сигналов в скриптах
- Обработка сигналов в скриптах
- Тестирование и отладка
Сигналы и процессы
Определение: сигнал — это краткое однонаправленное сообщение от ядра или процесса к другому процессу, уведомляющее о событии, требующем реакции.
Сигналы используются для информирования процесса о том, что произошло: пользователь нажал Ctrl+C, закрылась сессия SSH, приложение попыталось обратиться к запрещённой памяти и т.д. Если автор процесса ожидал определённый сигнал, он может написать обработчик — функцию, которая будет вызвана при получении этого сигнала.
Ключевая идея для сценариев: ловите сигналы, которые требуют аккуратного завершения (очистка временных файлов, закрытие сетевых соединений, завершение фоновых задач). Без обработчика процесс может мгновенно завершиться и оставить систему в неоднозначном состоянии.
Важно: не все сигналы можно перехватить. Например, SIGKILL и SIGSTOP не поддаются перехвату или игнорированию.
Список сигналов
Команда trap используется не только для установки обработчиков — с опцией -l она показывает все сигналы, используемые в системе:
trap -l
Замечания по реализации:
- Нумерация сигналов на Linux может достигать 64, но не все номера последовательны: 32 и 33 обычно не реализованы в Linux и зарезервированы для внутреннего использования/реалтайм-потоков. Реалтайм‑сигналы обычно идут от SIGRTMIN до SIGRTMAX.
- На других Unix‑платформах (например, OpenIndiana) набор и номера сигналов могут отличаться.

Обращение к сигналам может быть по имени (SIGTERM), по сокращённому имени без префикса SIG (TERM) или по номеру (15).
Категории воздействия сигналов:
- Terminate — сигнал завершения процесса (может быть перехвачен, если не SIGKILL).
- Ignore — информационный сигнал, который можно игнорировать.
- Core — приводит к дампу памяти (core dump).
- Stop — приостанавливает процесс (пауза), не завершает.
- Continue — возобновляет выполнение остановленного процесса.
Часто используемые сигналы:
- SIGHUP (1): сессия/подключение оборвано; часто используется демонами для перечитывания конфигурации.
- SIGINT (2): пользователь нажал Ctrl+C; прерывание.
- SIGQUIT (3): Ctrl+\ или Ctrl+D в некоторых терминалах — завершение с дампом.
- SIGFPE (8): ошибка арифметики, например деление на ноль.
- SIGKILL (9): немедленное убийство процесса — нельзя перехватить.
- SIGTERM (15): корректное завершение, можно перехватить и выполнить очистку.
Сигналы на командной строке
Вы можете установить ловушку прямо в оболочке. Пример: ловим SIGINT и выводим сообщение при получении Ctrl+C.
trap 'echo -e "\nCtrl+C обнаружен."' SIGINTПосле этого при нажатии Ctrl+C в текущей оболочке будет печататься сообщение вместо немедленного завершения (если команда внутри оболочки поддерживает перехват).
Проверка установленного trap:
trap -p SIGINTСброс обработчика к состоянию по умолчанию:
trap - SIGINTЕсли trap -p не выводит ничего, обработчик для сигнала не установлен.
Важно: применение trap в интерактивной оболочке отличается от использования в скриптах — будьте внимательны при тестировании.
Ловля сигналов в скриптах
Ниже — практические примеры. Код максимально сохранён от оригинала и корректно работает в bash.
Пример 1. Простой цикл, ловящий SIGINT, SIGQUIT и SIGTERM:
#!/bin/bash
trap "echo I was SIGINT terminated; exit" SIGINT
trap "echo I was SIGQUIT terminated; exit" SIGQUIT
trap "echo I was SIGTERM terminated; exit" SIGTERM
echo $$
counter=0
while true
do
echo "Loop number:" $((++counter))
sleep 1
doneСохраните как simple-loop.sh и сделайте исполняемым:
chmod +x simple-loop.shЗапустите и проверьте реакцию на Ctrl+C или отправку сигнала из другого терминала:
./simple-loop.sh
kill -SIGQUIT
kill -SIGTERM Пример 2. Функция для аккуратной остановки и удаления временного файла:
#!/bin/bash
trap graceful_shutdown SIGINT SIGQUIT SIGTERM
graceful_shutdown()
{
echo -e "\nRemoving temporary file:" $temp_file
rm -rf "$temp_file"
exit
}
temp_file=$(mktemp -p /tmp tmp.XXXXXXXXXX)
echo "Created temp file:" $temp_file
counter=0
while true
do
echo "Loop number:" $((++counter))
sleep 1
doneСохраните как grace.sh и сделайте исполняемым:
chmod +x grace.shЭтот шаблон даёт стандартную стратегию: собрать ресурсы в переменные, поставить trap на начало, затем в обработчике — выполнить чистку и exit.
Пример 3. Разные обработчики и EXIT‑handler:
#!/bin/bash
trap sigint_handler SIGINT
trap sigusr1_handler SIGUSR1
trap exit_handler EXIT
function sigint_handler() {
((++sigint_count))
echo -e "\nSIGINT received $sigint_count time(s)."
if [[ "$sigint_count" -eq 3 ]]; then
echo "Starting close-down."
loop_flag=1
fi
}
function sigusr1_handler() {
echo "SIGUSR1 sent and received $((++sigusr1_count)) time(s)."
}
function exit_handler() {
echo "Exit handler: Script is closing down..."
}
echo $$
sigusr1_count=0
sigint_count=0
loop_flag=0
while [[ $loop_flag -eq 0 ]]; do
kill -SIGUSR1 $$
sleep 1
doneЭтот пример демонстрирует:
- использование SIGUSR1/2 для пользовательских сигналов;
- обработку EXIT, который вызывается при нормальном завершении (кроме ситуаций с SIGKILL);
- накопительную логику (закрытие после 3 нажатий Ctrl+C).
Обработка сигналов — лучшие практики
- Минимизируйте действия в критической секции обработчика. Выполняйте только ту очистку, которая необходима немедленно; сложные долгие операции лучше сигнализировать через флаг и выполнить в основном цикле.
- Ставьте trap в начале скрипта, до создания ресурсов, чтобы не пропустить сигнал на старте.
- Используйте один общий exit‑handler для завершающей очистки и логирования.
- Не полагайтесь на перехват SIGKILL и SIGSTOP — их нельзя обработать.
- Делайте обработчики идемпотентными — повторный вызов не должен ломать логику.
- Если скрипт запускает фоновые процессы, перехватывайте сигналы и корректно завершайте дочерние процессы (kill – -$PID для группы процессов).
Важно: rm -rf в обработчике должен использовать заранее проверенный путь (не удаляйте непроверенные переменные).
Проверка и отладка
Полезные шаги для тестирования:
- Вывод PID в начале скрипта: echo $$ — чтобы отправлять сигналы извне.
- trap -p — проверка установленных ловушек.
- kill -l — показать список сигналов и их номера.
- Используйте set -x для трассировки исполнения скрипта.
- Тестируйте сценарии: нормальное завершение, повторный сигнал во время работы handler, получение сигнала при создании временных ресурсов.
Пример сценариев тестирования (тест‑кейсы):
- Скрипт создаёт временный файл и при SIGTERM удаляет его.
- Скрипт стартует фоновые процессы; при SIGINT они корректно завершаются.
- Обработчик вызывает exit только после успешной очистки (проверить состояние файлов/портов).
Критерии приёмки
- Скрипт не оставляет временных файлов/блокировок после полученного SIGTERM или SIGINT.
- Скрипт корректно завершает или пересоздаёт сетевые порты/слоты.
- Обработчики устойчивы к многократным вызовам.
Шаблонная методология для добавления обработки сигналов
- Идентифицируйте ресурсы, требующие очистки: временные файлы, сокеты, PID‑файлы, портфорвардинг.
- Добавьте переменные для всех ресурсов (вверху скрипта).
- Создайте функцию cleanup() с проверками и безопасными удалениями.
- Установите trap “cleanup; exit” для SIGINT SIGTERM SIGHUP и trap cleanup EXIT.
- Тестируйте сценарии с инструментами (kill, pkill, systemd‑stop, терминалы).
- Логируйте события очистки для аудита.
Мини‑шаблон cleanup:
cleanup() {
# проверяем, задана ли переменная и существует ли файл
if [[ -n "$temp_file" && -e "$temp_file" ]]; then
rm -f "$temp_file" || echo "Не удалось удалить $temp_file"
fi
# остановить фоновые процессы по группе
if [[ -n "$child_pgid" ]]; then
kill -- -$child_pgid
fi
}
trap 'cleanup; exit' SIGINT SIGTERM SIGHUP
trap cleanup EXITРиски и смягчения
Риск: handler выполняет долгую операцию и получает повторный сигнал.
- Смягчение: в обработчике ставьте блокировку (файл‑блок, флаг) или переключайтесь в краткую ветку, которая только помечает факт и возвращает контроль основному циклу.
Риск: некорректное использование rm в обработчике.
- Смягчение: всегда проверяйте путь и переменные, избегайте удаления корня или пустых переменных.
Риск: несовместимость между системами.
- Смягчение: документируйте минимальную целевую платформу (например, bash >= 4), используйте portable‑конструкции.
Советы по безопасности и соответствию
- Не храните конфиденциальные данные в временных файлах без прав доступа: используйте umask и mktemp для создания безопасных временных файлов.
- Если ваш скрипт запускается от имени сервиса (systemd), обрабатывайте сигналы корректно и используйте systemd‑юниты с KillMode=control‑group для завершения всех дочерних процессов.
- Для обработки секретов предпочтительны in‑memory решения и закрытие дескрипторов в cleanup.
Совместимость и миграция
- Linux (bash) — trap и SIG* работают ожидаемо.
- BSD/FreeBSD — набор сигналов совпадает, но номера могут отличаться; всегда полагайтесь на имена.
- Solaris/OpenIndiana — могут быть дополнительные сигналы; тестируйте в целевой среде.
Совет: при переносе скриптов между системами добавьте тест совместимости: trap -l и анализ вывода.
Ролевые чек‑листы
Для разработчика:
- Идентифицированы все ресурсы, требующие очистки.
- Добавлена функция cleanup и trap для SIGINT/SIGTERM/SIGHUP.
- Код обработчиков краткий и идемпотентный.
- Добавлены юнит‑ и интеграционные тесты на поведение при сигналах.
Для системного администратора:
- Настроено логирование завершений/сбоев для анализа.
- Тестирование поведения при обновлениях и рестартах сервиса.
- Обеспечена совместимость с systemd/upstart/inetd как требуется.
Playbook внедрения в проект
- Проанализируйте текущие скрипты и выделите критические.
- Для каждого скрипта добавьте секцию переменных ресурсов вверху.
- Создайте cleanup() и поставьте trap на SIGINT SIGTERM SIGHUP и EXIT.
- Выполните ручное тестирование и сценарии отказа.
- Добавьте автоматические тесты, покрывающие поведение при сигналах.
- Обновите документацию и SOP для оперативного персонала.
Примеры расширённых сценариев
- Скрипт‑демон, создающий PID‑файл и запускающий дочерние процессы:
- Создайте PID‑файл atomically (запись в tmp + mv) и укажите его в cleanup.
- Смело используйте группы процессов: при старте инициируйте set -m и сохраняйте PGID, чтобы при завершении использовать kill – -$PGID.
- Скрипт, управляющий iptables/netfilter:
- В cleanup снимайте правила, добавленные скриптом, по идентификатору или метке; не удаляйте все правила целиком.
- Скрипт, использующий TCP‑порт:
- Закрывайте слушающий сокет в cleanup; учитывайте TIME_WAIT и повторные старты.
Решение задач: decision tree
flowchart TD
A[Получен сигнал] --> B{Это SIGKILL или SIGSTOP?}
B -- Да --> Z[Немедленное действие ядра, нет обработчика]
B -- Нет --> C{Есть trap для сигнала?}
C -- Да --> D[Вызвать обработчик]
C -- Нет --> E{Это SIGTERM или SIGINT?}
E -- Да --> F[Применяется поведение по умолчанию: завершение]
E -- Нет --> G[Поведение зависит от сигнала 'stop/continue/core' ]
D --> H{Обработчик завершает мгновенно?}
H -- Да --> I[Выполнить exit -> EXIT handler -> выход]
H -- Нет --> J[Установить флаг, вернуть управление основному циклу]
J --> K[Основной цикл завершает неотложные задачи -> exit]Глоссарий в одну строку
- trap — встроенная bash‑команда для установки обработчиков сигналов.
- SIGTERM — вежливый сигнал завершения, можно перехватить.
- SIGKILL — мгновенное убийство процесса, нельзя перехватить.
- EXIT — псевдо‑сигнал оболочки, вызывается при завершении скрипта.
Примеры тест‑кейсов и критерии приёмки
Тест‑кейсы:
- При отправке SIGTERM скрипт удаляет все временные файлы, записанные им самими.
- При трёх подряд SIGINT скрипт переходит в режим завершения и запускает exit‑handler.
- При немедленном SIGKILL скрипт не успевает удалить временные файлы (ожидаемое поведение).
Критерии приёмки:
- 100% чистка ресурсов при SIGTERM/SIGINT в тестовой среде.
- Логи фиксируют запуск cleanup с временной меткой и перечислением очищенных ресурсов.
- Отсутствие «зависших» портов или процессов после корректного завершения.
Типичные ошибки и как их избежать
Ошибка: выполнять длительные операции в обработчике.
- Решение: только «малые» операции в обработчике, основная работа — в основном потоке по флагу.
Ошибка: удаление по пустой переменной: rm -rf “$temp_dir” (если переменная пуста, опасно).
- Решение: проверять переменную: [[ -n “$temp_dir” ]] && rm -rf “$temp_dir”.
Ошибка: ожидание пользовательского ввода в обработчике.
- Решение: никакого ввода — обработчик должен быть автономным.
Заключение
Ловля и обработка сигналов — простая, но критически важная часть написания надёжных Bash‑скриптов. Небольшие усилия — централизованный cleanup, аккуратные обработчики и тесты — значительно снижают риск накопления временных файлов, «подвисших» портов и других побочных эффектов. Начните с шаблона cleanup/trap, протестируйте его и интегрируйте в CI, чтобы гарантировать повторяемое и безопасное поведение.
Важно: всегда тестируйте на целевой платформе и документируйте поведение при сигналах в README скрипта.
Сводка
- Добавляйте trap в начало скрипта.
- Делайте обработчики короткими и идемпотентными.
- Используйте EXIT для окончательной очистки.
- Тестируйте все варианты (SIGTERM, SIGINT, SIGKILL — ожидание поведения).
Похожие материалы
Советы по уборке и организации дома — 5 ресурсов
Rufus — AI-помощник покупок на Amazon
Как скрыть или показать пост в Facebook для конкретных людей