Короткий сценарий живёт секунды, и вопрос его остановки решается сам. Долгоиграющий сценарий это другой жанр: минуты и часы работы, накопленные ресурсы, фоновые помощники и половина дела на момент, когда кто-то решает его остановить. Поведение сценария в этот момент отделяет профессиональную автоматизацию от самодеятельности: остановка либо проходит корректно, с чисткой и осмысленным кодом, либо оставляет после себя сломанные полуфабрикаты, которые придётся разыскивать по системе. Статья разбирает сигналы завершения, устройство ловушек, мягкую остановку циклов, честное завершение дочерних процессов и шаблоны, которые ставят это в систему.

Сигналы завершения и что именно получает сценарий

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

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

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

Отдельная тонкость о сессиях: сценарий, запущенный в фоне из интерактивной сессии, имеет собственные отношения с сигналами, и сочетание клавиш не останавливает фоновые задания. Знание этого факта экономит удивление в момент, когда комбинация прерывания останавливает передний план, а фоновая работа продолжает копить задачу.

Ловушка сигналов и её размещение в каркасе сценария

Ловушка назначается командой из трёх слов: сигнал, реакция, список сигналов. Реакция это команда или функция, которая выполняется в момент прихода сигнала, прерывая обычный поток сценария в произвольной точке:

cleanup() {
  rm -f /run/worker.lock
}
trap cleanup EXIT
trap 'echo "остановка по запросу" >&2; exit 130' INT TERM

Назначение ловушек живёт в начале сценария, до первой работы. Причина не в эстетике: интервал между запуском и назначением ловушки это окно, в котором сигнал проходит мимо реакции и завершает сценарий без уборки. Чем короче окно, тем предсказуемее поведение, и каркас из строгого режима, ловушек и лока ставится одной шапкой до всего содержательного.

Ловушка на выходе отличается от ловушек на сигналы принципиально: она срабатывает при любом завершении сценария, включая штатный конец и отказ строгого режима. Ловушки на сигналах срабатывают только по своим поводам. Практическая связка из двух уровней: сигнал отвечает за осмысленный код завершения и протокол, выход отвечает за уборку ресурсов.

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

Мягкая остановка циклов через флаг вместо аварийного выхода

Долгий сценарий почти всегда это цикл: шаги обработки, опросы, паузы. Аварийный выход из середины шага портит полуфабрикат: полусозданный файл, недоезженная партия, держатель в состоянии между. Мягкая остановка решает это флагом: обработчик сигнала не завершает сценарий сам, а лишь поднимает флаг, цикл проверяет флаг между шагами и выходит ровно на границе шагов:

stop=0
request_stop() { stop=1; }
trap request_stop INT TERM

while (( stop == 0 )); do
  process_batch
  sleep 60
done
cleanup

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

Вариант с немедленной остановкой тоже законен, для сценариев с шагами, которые безопасно прерывать: обработчик сам вызывает завершение с уборкой. Выбор между мягкой и немедленной схемой это выбор по вреду от прерванного шага: если полуфабрикат дороже секунды ожидания, остановка мягкая, если шаги дешёвые, реакция немедленная.

Пауза между шагами живёт с сигналами в особом мире: команда сна прерывается сигналом, и цикл просыпается досрочно. Каркас с флагом переживает это естественно, сон вернётся на следующем витке, а сценарий, строивший остановку на удачном моменте сна, узнаёт о реальности в первый же сигнал.

Дочерние процессы и честное завершение всего семейства

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

Сигналы осложняют жизнь асимметрией. Прерывание с клавиатуры уходит всей группе процессов переднего плана, и дети узнают о нём одновременно с родителем. Запрос на завершение, отправленный конкретному процессу, детям не доставляется вовсе, и вежливый родитель обязан переслать его своим помощникам сам. Сценарий, который этого не делает, выглядит остановленным, а его копирование продолжает жить в списке процессов ещё часами:

start_helper() {
  command &
  echo "$!" >> /run/worker.children
}
relay_stop() {
  while read -r pid; do
    kill "$pid" 2>/dev/null || true
  done < /run/worker.children
}

Список идентификаторов в файле переживает и перезапуски сценария: файл очищается при старте, наполняется при запуске помощников и читается при остановке. Пересылка завершения с игнорированием уже завершённых собирает семейство аккуратно и без ложных ошибок. Ждать детей после пересылки необязательно, но вежливо: короткое ожидание с проверкой ухода освобождает систему от лишнего шума в списке процессов.

Отдельная строка о группах: сценарий может объявить себя лидером группы процессов, и тогда один сигнал группе достаётся всем участникам сразу. Приём зрелый и требует понимания тонкостей, а начинается практика с простого списка помощников, который уже закрывает девять сценариев из десяти.

Взаимодействие с systemd при остановке службы

Системный менеджер это главный заказчик вежливых остановок в современной среде. Штатная остановка службы означает запрос на завершение основному процессу, ожидание лимита времени и безусловный сигнал, если процесс не ушёл. Три параметра менеджера решают судьбу сценария: лимит остановки, режим отслеживания процессов и тип запуска, и настройка их под длительный сценарий это не экзотика, а часть сдачи в эксплуатацию.

Лимит остановки согласуется с самым длинным шагом сценария. Мягкая остановка на границе шагов занимает до одного шага, и лимит обязан покрывать это с запасом. Лимит по умолчанию подходит коротким службам и вечно мал долгим, и первая остановка новой службы всегда проверяется руками, с журналом перед глазами.

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

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

Идемпотентная очистка и повторные сигналы

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

Идемпотентность сборщика достигается привычными приёмами: проверки существования перед действиями, подавление ожидаемых ошибок удаления, флаг выполненной уборки для повторных входов. Сборщик пишется с расчётом на наихудший вход: сигнал пришёл в момент уборки, уборка началась при уже удалённых ресурсах, фоновый помощник завершился между проверкой и пересылкой.

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

Журнал в уборке делает поведение наблюдаемым: строка о входе, строка о каждом ресурсе, строка о выходе с кодом. При разборе ночной остановки этот журнал отвечает на вопрос, что именно сценарий успел убрать до безусловного сигнала, и ответ этот превращает спор о догадках в чтение фактов.

Рабочие шаблоны обработки сигналов в длительных сценариях

Свод шаблонов, из которых собирается честная остановка, выглядит так:

  1. Перехват обоих вежливых сигналов одним обработчиком с единой реакцией;
  2. Уборка на ловушке выхода, короткая и идемпотентная;
  3. Мягкая остановка циклов флагом на границе шагов;
  4. Пересылка завершения запомненным дочерним процессам;
  5. Лимит остановки службы, согласованный с длиной шага;
  6. Журнал остановки с входом, ресурсами и кодом завершения.

За шаблонами стоит единая мысль: остановка это часть работы сценария, а не её противоположность. Сценарий, который проектируется вместе со своей остановкой, переживает перезапуски, отмены и обновления без мусора и недоумения, а сценарий, который о своей остановке не думал, оставляет о себе воспоминания во временном хранилище и в памяти дежурных.

Итоговое суждение спокойное. Перехват сигналов это десяток строк каркаса, ловушка на выходе, обработчик с журналом, список помощников и флаг для цикла. Эти строки не добавляют функциональности, они добавляют достоинство: сценарий уходит так же аккуратно, как работал. Для инструментов, которые живут в планировщиках и менеджерах годами, достоинство в часах остановки значит не меньше, чем скорость в часах работы, и заслуживает такого же внимания при проектировании.
Мелкая привычка с большим эффектом: учебная остановка нового сценария в первый день его жизни. Один запуск с прерыванием руками на середине, одна остановка через менеджер, один взгляд в журнал и в список процессов. Пять минут проверки закрывают вопросы, которые иначе приходят в самую неудачную ночь.