Вопрос или проблема
Я запущу несколько процессов и хочу выйти с соответствующим кодом завершения (это значит ошибка в случае неудачи, успех в противном случае), если любой из них завершится с ошибкой или выйдет.
Кроме того, если любой дочерний процесс завершится или потерпит неудачу, все другие дочерние процессы также должны быть завершены.
Мое текущее неработающее решение (yarn — это просто пример; может быть любая другая команда):
#!/bin/bash -e
# Запустить другой процесс перед началом
sh ./bin/optimize.sh
trap 'exit' INT TERM
trap 'kill -INT 0' EXIT
# Запустите расписание
sh ./bin/schedule.sh &
# Запустите долго работающую задачу
yarn task &
wait
./bin/schedule.sh
:
#!/bin/bash -e
while true; do
yarn schedule
sleep 30
done
Если что-то в yarn schedule
не удается, все корректно завершится. Но когда я убиваю процесс с помощью ctrl+c или yarn task
выходит, yarn schedule
продолжает работать.
Как сделать так, чтобы это работало независимо от того, какие дочерние процессы (bash, yarn, php или что-то еще)?
Я не могу использовать GNU parallel.
Это весьма затруднительно в оболочках, потому что встроенная команда wait
не делает «ожидание любого», она делает «ожидание всех». wait
без аргумента ожидает завершения всех дочерних процессов и возвращает 0. wait
с явным списком процессов ожидает завершения всех из них и возвращает статус последнего аргумента. Чтобы дождаться нескольких дочерних процессов и получить их коды завершения, нужен другой подход. wait
может дать вам код завершения только в том случае, если вы знаете, какой дочерний процесс уже завершился.
Один из возможных подходов — использовать выделенный именованный канал для сообщения о статусе каждого дочернего процесса. Следующий фрагмент (не тестировался!) возвращает наибольший из статусов дочерних процессов.
mkfifo status_pipe
children=0
{ child1; echo 1 $? >status_pipe; } & children=$((children+1))
{ child2; echo 2 $? >status_pipe; } & children=$((children+1))
max_status=0
while [ $children -ne 0 ]; do
read -r child status <status_pipe
children=$((children-1))
if [ $status -gt $max_status ]; then
max_status=$status
fi
done
rm status_pipe
Обратите внимание, что это будет блокироваться навсегда, если один из подпроцессов погибнет, не сообщив о своем статусе. Это не произойдет в типичных условиях, но может произойти, если подпроцесс был убит вручную или если у него не хватает памяти.
Если вы хотите сделать что-то, как только один из дочерних процессов завершится с ошибкой, замените if [ $status -gt $max_status ]; then …
на if [ $status -ne 0 ]; then …
.
GNU Parallel имеет --halt
. Это убьет все работающие задания, если одно из заданий завершится или погибнет, и вернет ложь, если задание не удалось:
parallel --halt now,done=1 ::: 'sleep 1;echo a' 'sleep 2;echo b' ||
echo задание, которое завершилось, потерпело неудачу
parallel --halt now,done=1 ::: 'sleep 1;echo a;false' 'sleep 2;echo b' ||
echo задание, которое завершилось, потерпело неудачу
Для систем, на которых не установлен GNU Parallel, вы обычно можете написать ваш скрипт на системе, которая имеет GNU Parallel, и использовать --embed
, чтобы встроить GNU Parallel прямо в скрипт:
parallel --embed > myscript.sh
Вот решение с использованием wait -n
и функции, которая говорит bash, сколько процессов ожидать:
set -o nounset
set -o errexit
set -o pipefail
wait_many () {
for ((i = 0 ; i < $1 ; i++)); do
wait -n
done
}
(sleep 2 && echo "хорошо 1") &
(sleep 3 && echo "хорошо 2") &
(sleep 4 && echo "плохо 3" && echo $unset) &
(sleep 5 && echo "хорошо 4") &
wait_many 4
Преимущества этого подхода:
- он простой, если есть фиксированное количество процессов, которые нужно ожидать
- он может позволить нам выйти из скрипта с ненулевым кодом завершения, если любой из подпроцессов завершится неудачно
Ответ или решение
Вопрос, как корректно запускать несколько процессов и завершаться при выходе любого из них, действительно является важной задачей в программировании и администрировании системы. В данной статье мы рассмотрим несколько эффективных подходов к решению этой проблемы, которые помогут вам контролировать состояние дочерних процессов и корректно завершать выполнение сценария в случае их сбоя.
Введение
Управление жизненным циклом процессов в оболочке Bash требует тщательной проработки, особенно когда необходимо контролировать несколько параллельных задач. При этом важно обеспечить, чтобы при завершении или сбое одного процесса все остальные процессы также были остановлены, а сценарий завершился с адекватным кодом выхода.
Подход с использованием именованного канала
Одним из распространенных методов является использование именованных каналов (named pipes). Этот метод позволяет дочерним процессам сообщать о своем статусе.
#!/bin/bash
set -o nounset
set -o errexit
set -o pipefail
mkfifo status_pipe
# Общее количество дочерних процессов
children=0
# Запуск процессов с записью их статуса
{ child1; echo 1 $? > status_pipe; } &
children=$((children+1))
{ child2; echo 2 $? > status_pipe; } &
children=$((children+1))
max_status=0
while [ $children -ne 0 ]; do
read -r child status < status_pipe
children=$((children-1))
if [ $status -ne 0 ]; then
max_status=$status
# Остановить все остальные процессы
kill 0
fi
done
# Удалить именованный канал
rm status_pipe
exit $max_status
В данной реализации, если любой из дочерних процессов завершится с ненулевым статусом, скрипт остановит другие процессы и завершится с соответствующим кодом выхода. Однако следует учитывать, что этот подход может блокироваться в случае, если один из процессов завершится, не сообщив о своем статусе.
Использование wait -n
С bash 5.0 имеется возможность использовать команду wait -n
, которая позволяет ожидать завершения любого дочернего процесса. Это значительно упрощает задачу.
#!/bin/bash
set -o nounset
set -o errexit
set -o pipefail
# Функция для ожидания множества процессов
wait_many() {
for ((i = 0; i < $1; i++)); do
wait -n
done
}
# Запуск процессов
(sleep 2 && echo "good 1") &
(sleep 3 && echo "good 2") &
(sleep 1 && echo "failed" && false) &
(sleep 5 && echo "good 4") &
# Ожидание завершения всех процессов
wait_many 4
Этот метод предоставляет вам гибкость и простоту при управлении несколькими процессами. Если любой из процессов завершится с ошибкой, вы сможете завершить выполнение скрипта с соответствующим кодом ошибки.
Заключение
Управление несколькими процессами и их завершение при ошибках требует внимательности, но с использованием методов, представленных в этой статье, вы сможете создать надежный скрипт для решения данной задачи. Выбор подхода может зависеть от версии Bash и специфики вашего проекта, однако оба метода демонстрируют, как эффективно контролировать процессы и адекватно реагировать на их завершение.
Следуя описанным подходам, вы сможете значительно упростить процесс управления многими задачами, обеспечивая необходимый контроль и надежность в вашем коде.