Как в bash дождаться завершения нескольких подпроцессов и вернуть код завершения !=0, если любой подпроцесс завершился с кодом !=0?
Проблема: Как в bash-скрипте дождаться завершения нескольких подпроцессов и вернуть код выхода !=0, если любой из подпроцессов завершился с кодом !=0?
Я написал простой скрипт, который создает 10 подпроцессов, выполняя функцию calculations с параметрами от 0 до 9 в фоновом режиме:
#!/bin/bash
for i in `seq 0 9`; do
calculations $i &
done
wait
Этот скрипт ожидает завершения всех 10 созданных подпроцессов, но всегда возвращает код выхода 0, даже если один или несколько из подпроцессов завершились с ненулевым кодом (см. help wait).
Как я могу модифицировать этот скрипт, чтобы он мог отслеживать коды возврата подпроцессов и возвращал код выхода 1, если любой из отпроцессов завершился с кодом !=0?
Есть ли более эффективное решение для этой задачи, чем собирать PID подпроцессов, ждать их завершения по порядку и суммировать коды выхода?
5 ответ(ов)
Этот скрипт написан на Bash и выполняет несколько фоновых процессов с использованием команды ./sleeper. Давайте разберем его по шагам.
Инициализация переменной:
FAIL=0Здесь мы инициализируем счетчик
FAIL, который будет использоваться для отслеживания, сколько процессов завершились с ошибкой.Запуск фоновых процессов:
echo "starting" ./sleeper 2 0 & ./sleeper 2 1 & ./sleeper 3 0 & ./sleeper 2 0 &echo "starting"выводит сообщение о начале работы скрипта. Команды./sleeperзапускают четыре процесса в фоновом режиме (из-за&в конце каждой строки), передавая разные параметры для каждого.Ожидание завершения процессов:
for job in `jobs -p` do echo $job wait $job || let "FAIL+=1" doneЗдесь мы используем цикл для обработки каждого запущенного фонового процесса. Команда
jobs -pвыводит идентификаторы процессов, запущенных в фоне. Внутри цикла снова выводим идентификатор процесса, ждем его завершения с помощьюwait, и если процесс завершился с ошибкой (код возврата не 0), увеличиваем счетчикFAIL.Вывод результата:
echo $FAIL if [ "$FAIL" == "0" ]; then echo "YAY!" else echo "FAIL! ($FAIL)" fiПосле завершения всех процессов мы выводим общее количество неудачных завершений. Если счетчик
FAILравен 0, выводим "YAY!", в противном случае — "FAIL!" с указанием числа неудачных завершений.
Таким образом, скрипт запускает несколько процессов, ждет их завершения и проверяет, все ли выполненные процессы завершились успешно, а затем отображает итоговый результат.
Вот простой пример использования команды wait.
Запустим несколько процессов:
$ sleep 10 &
$ sleep 10 &
$ sleep 20 &
$ sleep 20 &
Затем дождемся их завершения с помощью команды wait:
$ wait < <(jobs -p)
^ Или просто используйте wait (без аргументов) для ожидания всех фоновых процессов.
Это позволит дождаться завершения всех запущенных в фоне задач.
^ Если указать опцию -n, команда будет ждать завершения следующей задачи и вернет ее код завершения.
Смотрите: help wait и help jobs для получения информации о синтаксисе.
Однако недостатком этого метода является то, что команда вернет только статус последнего ID, поэтому вам нужно проверить статус для каждого подсного процесса и сохранить его в переменную.
Или вы можете создать функцию для расчета, которая создает файл в случае ошибки (пустой файл или с логом об ошибке), а затем проверять наличие этого файла, например:
$ sleep 20 && true || tee fail &
$ sleep 20 && false || tee fail &
$ wait < <(jobs -p)
$ test -f fail && echo Расчет завершился с ошибкой.
Вот перевод вашего текста в стиле ответа на StackOverflow:
Как насчет простого варианта:
#!/bin/bash
pids=""
for i in `seq 0 9`; do
doCalculations $i &
pids="$pids $!"
done
wait $pids
...код продолжается здесь...
Обновление:
Как указали несколько комментаторов, приведенный выше код ожидает завершения всех процессов перед тем, как продолжить выполнение, но не завершит скрипт с ошибкой, если один из процессов завершится неудачно, что может привести к проблемам, особенно если это не последняя запись в pids. Это можно исправить с помощью следующей модификации, предложенной @Bryan, @SamBrightman и другими:
#!/bin/bash
pids=""
RESULT=0
for i in `seq 0 9`; do
doCalculations $i &
pids="$pids $!"
done
for pid in $pids; do
wait $pid || let "RESULT=1"
done
if [ "$RESULT" == "1" ]; then
exit 1
fi
...код продолжается здесь...
Надеюсь, это поможет!
Если у вас установлен GNU Parallel, вы можете использовать следующий код:
# Если doCalculations - это функция
export -f doCalculations
seq 0 9 | parallel doCalculations {}
GNU Parallel вернёт вам код выхода:
- 0 - Все задачи выполнены без ошибок.
- 1-253 - Некоторые задачи завершились с ошибками. Код выхода указывает количество неудавшихся задач.
- 254 - Ошибки произошли в более чем 253 задачах.
- 255 - Другая ошибка.
Посмотрите вводные видео, чтобы узнать больше: http://pi.dk/1
Чтобы прервать выполнение команды sleep в функции waitall, когда один из дочерних процессов завершается, можно использовать сигнал, например, SIGUSR1. Ниже приведен пример того, как можно изменить вашу функцию, чтобы она могла прерывать sleep, если обнаружится, что один из процессов завершился.
waitall() { # PID...
## Ждем завершения дочерних процессов и указываем, завершились ли все с кодом 0.
local errors=0
local pids=("$@") # Сохраняем PID в массив
local interrupted=0
trap 'interrupted=1' SIGUSR1 # Устанавливаем обработчик сигнала
while :; do
debug "Remaining processes: ${pids[*]}"
for pid in "${pids[@]}"; do
if kill -0 "$pid" 2>/dev/null; then
debug "$pid is still alive."
elif wait "$pid"; then
debug "$pid exited with zero exit status."
pids=("${pids[@]/$pid}") # Убираем завершенный PID из массива
else
debug "$pid exited with non-zero exit status."
((++errors))
pids=("${pids[@]/$pid}") # Убираем завершенный PID из массива
fi
done
# Если все дочерние процессы завершены, выходим из цикла
[[ "${#pids[@]}" -eq 0 ]] && break
if [[ $interrupted -eq 1 ]]; then
debug "Sleep interrupted by child termination."
interrupted=0 # Сбрасываем флаг
else
# Пауза, если не было прерываний
sleep ${WAITALL_DELAY:-1}
fi
done
((errors == 0))
}
debug() { echo "DEBUG: $*" >&2; }
pids=""
for t in 3 5 4; do
sleep "$t" &
pids="$pids $!"
done
waitall $pids
Объяснение:
- Я добавил конструкцию
trap, которая перехватывает сигналSIGUSR1и устанавливает флагinterrupted, когда сигнал получен. - Если один из процессов завершится, мы убираем его PID из массива
pids. - Если сигнал
SIGUSR1был получен, мы выводим отладочное сообщение и можем прерватьsleep, не дожидаясь его окончания.
Такой подход позволяет избежать необходимости настраивать WAITALL_DELAY и проведения дополнительных настроек при использовании функции waitall.
Как изменить цвет вывода echo в Linux
Как работает "cat << EOF" в bash?
Расширение переменных внутри одинарных кавычек в команде Bash
Как разделить большой текстовый файл на меньшие файлы с равным количеством строк?
Автоматический вход в Docker через Bash-скрипт