Как в Bash-скрипте пересылать SIGTERM дочернему процессу

Supervisord требует, чтобы программы, на исполнение которых он сконфигурирован, не становились демонами самостоятельно. Вместо этого они должны выполняться на переднем плане и отвечать на сигнал остановки (по умолчанию TERM) для корректного завершения работы. У Docker есть аналогичное требование для команд, указанных в инструкциях CMD и ENTRYPOINT в файлах Dockerfile. В обоих случаях, только процессы созданные непосредственно Supervisord и Docker получают сигнал TERM и именно они отвечают за корректную остановку дочерних процессов.

Это становится проблемой, если процесс на сервере порожден shell-скриптом, что является частым случаем для сервисов Java:

#!/bin/bash

# Подготовка командной строки JVM
...

$JAVA_EXECUTABLE $JAVA_ARGS

# Уборка
...

В этом случае сигнал TERM будет получен shell-процессом, но Bash не пересылает этот сигнал дочерним процессам. Это означает, что shell-процесс не остановит выполнение, а JVM продолжит работать.

Если команда, создающая дочерний процесс, является последней в скрипте, то проблему можно легко решить с помощью exec:

#!/bin/bash
...
exec $JAVA_EXECUTABLE $JAVA_ARGS

Вместо создания нового процесса exec заменит shell-процесс на JVM. В этом случае сигнал TERM будет получен JVM напрямую и проблема решена.

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

Когда Bash получает сигнал, для которого была установлена ловушка, во время ожидания завершения выполнения команды, ловушка не будет вызвана пока команда не закончит свою работу.

Такое поведение вынуждает запускать JVM в фоновом процессе (используя &) и ждать завершения ее выполнения так, чтобы shell-процесс мог вызвать ловушку в момент, когда дочерний процесс все еще работает. Часто в сети предлагают писать скрипт следующим образом, применяя встроенную команду wait для ожидания завершения выполнения дочернего процесса:

#!/bin/bash
...
trap 'kill -TERM $PID' TERM
$JAVA_EXECUTABLE $JAVA_ARGS &
PID=$!
wait $PID
...

Однако, это неправильно. Чтобы понять почему, давай посмотрим как trap и wait взаимодействуют друг с другом:

Когда Bash ожидает асинхронную команду через встроенную команду wait, получение сигнала, на который установлена ловука, приведет к тому, что wait моментально вернет код завершения больше 128, и сразу же следом будет вызвана ловушка.

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

#!/bin/bash
...
trap 'kill -TERM $PID; wait $PID' TERM
$JAVA_EXECUTABLE $JAVA_ARGS &
PID=$!
wait $PID
...

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

wait [jobspec or pid ...]
Ожидать, пока дочерний процесс с pid ID или спецификация задачи jobspec завершатся и вернут код выхода последней команды. Если использутеся спецификация задачи, то ожидается завершение всех процессов задачи. Если аргументы отсутствуют, то ожидается завершение всех активных на текущий момент дочерних процессов, а затем возвращается код выхода ноль.

Так оно работает до тех пор, пока не будет прервано сигналом, в случае которого немедленно завершается с кодом выхода больше 128 (в действительности, 128 плюс цифровое значение сигнала, равное 15 для SIGTERM). Такая информация может быть использована для разделения одного случая от другого. Однако, такой подход не сработает, поскольку код выхода дочернего процесса может быть сам по себе больше 128 (в частности, по умолчанию обработчик сигнала SIGTERM вынуждает процесс завершиться с кодом выхода 143). Одним из решением проблемы может стать двойной вызов wait:

#!/bin/bash
...
trap 'kill -TERM $PID; wait $PID' TERM
$JAVA_EXECUTABLE $JAVA_ARGS &
PID=$!
wait $PID
wait $PID
EXIT_STATUS=$?
...

Это сработает, так как wait делает возврат моментально (вместе с кодовм выхода процесса), если процесс уже завершен. Ожидание в ловушке более не требуется и скрипт может быть упрощен до такого:

#!/bin/bash
...
trap 'kill -TERM $PID' TERM
$JAVA_EXECUTABLE $JAVA_ARGS &
PID=$!
wait $PID
wait $PID
EXIT_STATUS=$?
...

Однако, такое решение все еще не идеально, так как оно не может корректно обработать SIGINT. При нажатии CTRL+C терминал посылает SIGINT группе процессов терминала, выполняющихся на переднем плане (в рассматриваемом здесь случае это shell-процесс и его дочерние процессы). Проблема в том, что Bash конфигурирует запускаемые в фоновом режиме процессы на игнорирование SIGINT. Это означает, что CTRL+C останавливает только скрипт, а не JVM. Для решения этой проблемы и имитации поведения исходного скрипта (который просто запускает $JAVA_EXECUTABLE $JAVA_ARGS в фоновом режиме) достаточно настроить ту же самую ловушку на срабатывание на сигнал INT:

#!/bin/bash
...
trap 'kill -TERM $PID' TERM INT
$JAVA_EXECUTABLE $JAVA_ARGS &
PID=$!
wait $PID
wait $PID
EXIT_STATUS=$?
...

Напоследок, хорошей идеей может стать удаление ловушки после получения первого сигнала или после остановки JVM по любой другой причине:

#!/bin/bash
...
trap 'kill -TERM $PID' TERM INT
$JAVA_EXECUTABLE $JAVA_ARGS &
PID=$!
wait $PID
trap - TERM INT
wait $PID
EXIT_STATUS=$?
...

Источник: http://veithen.github.io/2014/11/16/sigterm-propagation.html

Статьи и заметки
ЛИЧНЫЙ КАБИНЕТ
На вашу почту отправлено сообщение с кодом подтверждения. Введите его для завершения регистрации.
ВЫПОЛНИТЬ