Как в 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
Как сделать паузу в shell-скрипте на одну секунду перед продолжением?
Как работает "cat << EOF" в bash?
Node / Express: EADDRINUSE, адрес уже занят - как остановить процесс, использующий порт?
Как получить пароль из оболочки без вывода в терминал?