Вопрос или проблема
Ограничения:
- Я не могу хранить PID дочерних процессов никаким образом;
- Я не могу наследовать trap;
- Я не могу использовать программы вне coreutils/util-linux (возможно с исключением для pkill).
Постановка задачи:
У меня есть bash-скрипт, который запускает произвольное количество программ следующим образом:
progA &
mkdir -p dirB && progB --arg=dirB &
progC &
...
progF
Я хочу убедиться, что когда основной bash-скрипт завершает работу (например, из-за SIGTERM), все дочерние процессы завершаются.
Что я пробовал:
Пока что я пробовал использовать ловушки такие как trap "trap - TERM ; kill 0" EXIT INT TERM
(плюс что-то вроде kill -- -$$
).
Единственная ловушка, которая работает надежно, это pkill -P $$
за исключением случая, когда программа запускается так mkdir -p dirB && progB &
но когда она запускается в подконсоли как ( mkdir -p dirB && progB ) &
progB завершается надежно.
Почему это так?
Уточнение: когда я говорю ‘надежно’, я имею в виду всегда. Если я запускаю и быстро останавливаю скрипт, то progB не завершается в чем-то вроде 1/10 случаев.
Версия оболочки: GNU bash, версия 5.2.37(1)-release (x86_64-pc-linux-gnu)
Ответ или решение
Теория
В Bash символ &
используется для запуска команды в фоновом режиме, а &&
— для условного выполнения следующей команды только в случае успешного завершения предыдущей. Скобки ( ... )
создают подшелл, позволяя группировать команды для выполнения в отдельной среде. Когда дело доходит до управления процессами и их завершения в Unix-подобных системах, важном инструментом является команда pkill
, особенно с опцией -P
, позволяющей завершать процессы по идентификатору родительского процесса (PID) (в данном случае — PID основного скрипта).
Пример
Команда progA &
Выполнение progA &
запускает progA
в фоновом режиме как отдельный процесс, дочерний к текущему shell-скрипту. Родительским процессом для progA
является сам скрипт. Таким образом, при использовании pkill -P $$
этот процесс будет правильно завершен, поскольку его родитель — основной скрипт.
Команда progB && progA &
Когда выполняется progB && progA &
, progB
запускается в основном shell, и только если он успешно завершится, будет запущен progA
в фоновом режиме. В этой конструкции родительский процесс для progA
также является основным скриптом, но поскольку progA
запущен после успешного выполнения progB
, существует небольшая вероятность, что progA
не успевает стать дочерним процессом основного скрипта из-за временных задержек. Это может объяснить, почему progA
иногда не завершается при быстром завершении основного скрипта.
Команда ( progB && progA ) &
Конструкция (progB && progA) &
создаёт подшелл для выполнения progB
и progA
. Подшелл сам по себе становится независимым фоновым процессом. В этом случае подшелл является дочерним процессом основного скрипта, в то время как progB
и progA
становятся дочерними процессами подшелла. При использовании pkill -P $$
, все дочерние процессы основного скрипта, включая подшелл, будут завершены. Поскольку progA
и progB
выполняются в контексте подшелла, они также будут завершены, так как их родительский процесс (подшелл) завершен.
Применение
В вашей ситуации, критически важно учесть, как создаются и управляются процессы. Использование подшеллов помогает конфигурировать более предсказуемые сценарии завершения процессов. В случае использования основных конструкций Bash можно достигнуть надёжного завершения всех дочерних процессов при завершении основного скрипта. Необходимо просто реализовать каждую требуемую команду в рамках подшелла, чтобы гарантировать их корректное завершение:
(progB && progA) &
Этот подход минимизирует вероятность возникновения состояний гонки и задержек, которые могут приводить к тому, что некоторые процессы не успевают закрепиться как дочерние процессы основного скрипта.
Выводы
Таким образом, ключевое различие между этими командами — это структура родительских и дочерних процессов, создаваемых ими. Основная цель должна заключаться в том, чтобы каждая группа процессов имела общего родителя, которого легко контролировать и завершать. Использование подшеллов предоставляет более надёжный механизм создания и завершения процессов в Unix-подобных системах, таких как Linux, что особенно важно в сценариях, требующих высокой стабильности и предсказуемости в управлении жизненным циклом процессов.