Как подождать в bash script для нескольких подпроцессов, порожденных из этого script, чтобы закончить и вернуть код выхода!= 0, когда какой-либо из подпроцессов заканчивается кодом!= 0?
Простой script:
#!/bin/bash
for i in `seq 0 9`; do
doCalculations $i &
done
wait
Вышеупомянутый script будет ждать все 10 порожденных подпроцессов, но всегда будет давать статус выхода 0 (см. help wait
). Как я могу изменить этот script, чтобы он обнаружил статусы выхода порожденных подпроцессов и возвращал код выхода 1, когда какой-либо из подпроцессов заканчивается кодом!= 0?
Есть ли лучшее решение для этого, чем сбор PID подпроцессов, ожидание их по порядку и суммирование статусов выхода?
wait
также (необязательно) принимает PID процесса для ожидания и с $! вы получаете PID последней команды, запущенной в фоновом режиме.
Измените цикл, чтобы сохранить PID каждого порожденного подпроцесса в массив, а затем снова зациклиться на каждом PID.
http://jeremy.zawodny.com/blog/archives/010717.html:
#!/bin/bash
FAIL=0
echo "starting"
./sleeper 2 0 &
./sleeper 2 1 &
./sleeper 3 0 &
./sleeper 2 0 &
for job in `jobs -p`
do
echo $job
wait $job || let "FAIL+=1"
done
echo $FAIL
if [ "$FAIL" == "0" ];
then
echo "YAY!"
else
echo "FAIL! ($FAIL)"
fi
jobs -p
задает PID подпроцессов, которые находятся в состоянии выполнения. Процесс будет пропущен, если процесс завершится до jobs -p
. Поэтому, если какой-либо из подпроцессов завершится до jobs -p
, состояние выхода этого процесса будет потеряно.
Если у вас установлен GNU Parallel, вы можете сделать:
seq 0 9 | parallel doCalculations {}
GNU Parallel даст вам код выхода:
0 - Все задания выполнялись без ошибок.
1-253 - Не удалось выполнить некоторые из заданий. Статус выхода дает количество неудачных заданий
254 - Сработало более 253 заданий.
255 - Другая ошибка.
Смотрите видеоролики, чтобы узнать больше: http://pi.dk/1
Установка за 10 секунд:
wget -O - pi.dk/3 | sh
doCalculations
- это функция, определенная в этом же скрипте (хотя ОП не doCalculations
этого требования). Когда я пытаюсь, parallel
говорит /bin/bash: doCalculations: command not found
(она говорит это 10 раз для примера seq 0 9
выше). Смотрите здесь для обхода.
Вот что я придумал до сих пор. Я хотел бы видеть, как прерывать команду sleep, если ребенок завершается, так что не нужно было бы настраивать WAITALL_DELAY
на одно использование.
waitall() { # PID...
## Wait for children to exit and indicate whether all exited with 0 status.
local errors=0
while :; do
debug "Processes remaining: $*"
for pid in "$@"; do
shift
if kill -0 "$pid" 2>/dev/null; then
debug "$pid is still alive."
set -- "$@" "$pid"
elif wait "$pid"; then
debug "$pid exited with zero exit status."
else
debug "$pid exited with non-zero exit status."
((++errors))
fi
done
(("$#" > 0)) || break
# TODO: how to interrupt this sleep when a child terminates?
sleep ${WAITALL_DELAY:-1}
done
((errors == 0))
}
debug() { echo "DEBUG: $*" >&2; }
pids=""
for t in 3 5 4; do
sleep "$t" &
pids="$pids $!"
done
waitall $pids
Как насчет просто:
#!/bin/bash
pids=""
for i in `seq 0 9`; do
doCalculations $i &
pids="$pids $!"
done
wait $pids
...code continued here ...
Update:
Как указано несколькими комментаторами, вышеупомянутое ожидает завершения всех процессов до продолжения, но не выходит и не сработает, если один из них терпит неудачу, его можно сделать со следующей модификацией, предложенной @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
...code continued here ...
for pid in $pids; do wait $pid; done
Вот простой пример, используя wait
.
Запустите некоторые процессы:
$ sleep 10 &
$ sleep 10 &
$ sleep 20 &
$ sleep 20 &
Затем дождитесь их командой wait
:
$ wait < <(jobs -p)
Или просто wait
(без аргументов) для всех.
Это будет ждать завершения всех заданий в фоновом режиме.
Если включена опция -n
, она ждет завершения следующего задания и возвращает его статус выхода.
Смотрите: help wait
и help jobs
для синтаксиса.
Однако недостатком является то, что он вернется только к статусу последнего идентификатора, поэтому вам нужно проверить статус для каждого подпроцесса и сохранить его в переменной.
Или сделайте свою вычислительную функцию для создания некоторого файла при сбое (пустой или с журналом сбоев), затем проверьте его, если существует, например
$ sleep 20 && true || tee fail &
$ sleep 20 && false || tee fail &
$ wait < <(jobs -p)
$ test -f fail && echo Calculation failed.
sleep 20 && true
и sleep 20 && false
- то есть: замените их вашими функциями. Чтобы понять &&
и ||
, запустите man bash
и введите «/» (поиск), затем «^ * Lists» (регулярное выражение), затем введите: man прокрутит вниз до описания &&
и ||
Чтобы распараллелить это...
for i in $(whatever_list) ; do
do_something $i
done
Перевести его на это...
for i in $(whatever_list) ; do echo $i ; done | ## execute in parallel...
(
export -f do_something ## export functions (if needed)
export PATH ## export any variables that are required
xargs -I{} --max-procs 0 bash -c ' ## process in batches...
{
echo "processing {}" ## optional
do_something {}
}'
)
--max-procs
в зависимости от того, сколько parallelism вы хотите (0
означает "все одновременно" ).xargs
- но он не всегда устанавливается по умолчанию.for
не является строго необходимым, так как echo $i
в основном просто регенерирует вывод $(whatever_list
). Я просто думаю, что использование ключевого слова for
позволяет немного легче понять, что происходит.Здесь приведен упрощенный рабочий пример...
for i in {0..5} ; do echo $i ; done |xargs -I{} --max-procs 2 bash -c '
{
echo sleep {}
sleep 2s
}'
--max-procs
: Как получить количество процессоров / ядер в Linux из командной строки?
Я вижу много хороших примеров, приведенных здесь, хотел также выбросить мой.
#! /bin/bash
items="1 2 3 4 5 6"
pids=""
for item in $items; do
sleep $item &
pids+="$! "
done
for pid in $pids; do
wait $pid
if [ $? -eq 0 ]; then
echo "SUCCESS - Job $pid exited with a status of $?"
else
echo "FAILED - Job $pid exited with a status of $?"
fi
done
Я использую что-то очень похожее на запуск/остановку серверов/служб параллельно и проверку каждого статуса выхода. Отлично работает для меня. Надеюсь, это поможет кому-то!
Я не думаю, что это возможно с Bash встроенной функциональностью.
Вы можете получать уведомление, когда ребенок выходит:
#!/bin/sh
set -o monitor # enable script job control
trap 'echo "child died"' CHLD
Однако нет очевидного способа получить статус выхода ребенка в обработчике сигнала.
Получение этого дочернего статуса обычно является заданием семейства функций wait
в API-интерфейсах POSIX более низкого уровня. К сожалению, поддержка Bash для этого ограничена - вы можете подождать одного конкретного дочернего процесса (и получить его статус выхода), или вы можете дождаться их всех и всегда получать результат 0.
То, что кажется невозможным, - это эквивалент waitpid(-1)
, который блокируется до тех пор, пока не вернется любой дочерний процесс.
Следующий код будет ждать завершения всех вычислений и возврата статуса выхода 1, если какой-либо из doCalculations не удался.
#!/bin/bash
for i in $(seq 0 9); do
(doCalculations $i >&2 & wait %1; echo $?) &
done | grep -qv 0 && exit 1
Здесь моя версия, которая работает для нескольких pids, записывает предупреждения, если выполнение занимает слишком много времени, и останавливает подпроцессы, если выполнение занимает больше времени, чем заданное значение.
function WaitForTaskCompletion {
local pids="${1}" # pids to wait for, separated by semi-colon
local soft_max_time="${2}" # If execution takes longer than $soft_max_time seconds, will log a warning, unless $soft_max_time equals 0.
local hard_max_time="${3}" # If execution takes longer than $hard_max_time seconds, will stop execution, unless $hard_max_time equals 0.
local caller_name="${4}" # Who called this function
local exit_on_error="${5:-false}" # Should the function exit program on subprocess errors
Logger "${FUNCNAME[0]} called by [$caller_name]."
local soft_alert=0 # Does a soft alert need to be triggered, if yes, send an alert once
local log_ttime=0 # local time instance for comparaison
local seconds_begin=$SECONDS # Seconds since the beginning of the script
local exec_time=0 # Seconds since the beginning of this function
local retval=0 # return value of monitored pid process
local errorcount=0 # Number of pids that finished with errors
local pidCount # number of given pids
IFS=';' read -a pidsArray <<< "$pids"
pidCount=${#pidsArray[@]}
while [ ${#pidsArray[@]} -gt 0 ]; do
newPidsArray=()
for pid in "${pidsArray[@]}"; do
if kill -0 $pid > /dev/null 2>&1; then
newPidsArray+=($pid)
else
wait $pid
result=$?
if [ $result -ne 0 ]; then
errorcount=$((errorcount+1))
Logger "${FUNCNAME[0]} called by [$caller_name] finished monitoring [$pid] with exitcode [$result]."
fi
fi
done
## Log a standby message every hour
exec_time=$(($SECONDS - $seconds_begin))
if [ $((($exec_time + 1) % 3600)) -eq 0 ]; then
if [ $log_ttime -ne $exec_time ]; then
log_ttime=$exec_time
Logger "Current tasks still running with pids [${pidsArray[@]}]."
fi
fi
if [ $exec_time -gt $soft_max_time ]; then
if [ $soft_alert -eq 0 ] && [ $soft_max_time -ne 0 ]; then
Logger "Max soft execution time exceeded for task [$caller_name] with pids [${pidsArray[@]}]."
soft_alert=1
SendAlert
fi
if [ $exec_time -gt $hard_max_time ] && [ $hard_max_time -ne 0 ]; then
Logger "Max hard execution time exceeded for task [$caller_name] with pids [${pidsArray[@]}]. Stopping task execution."
kill -SIGTERM $pid
if [ $? == 0 ]; then
Logger "Task stopped successfully"
else
errrorcount=$((errorcount+1))
fi
fi
fi
pidsArray=("${newPidsArray[@]}")
sleep 1
done
Logger "${FUNCNAME[0]} ended for [$caller_name] using [$pidCount] subprocesses with [$errorcount] errors."
if [ $exit_on_error == true ] && [ $errorcount -gt 0 ]; then
Logger "Stopping execution."
exit 1337
else
return $errorcount
fi
}
# Just a plain stupid logging function to replace with yours
function Logger {
local value="${1}"
echo $value
}
Пример: подождите, пока все три процесса закончатся, запишите предупреждение, если выполнение занимает регистратор менее 5 секунд, остановите все процессы, если выполнение занимает более 120 секунд. Не выходите из программы при сбоях.
function something {
sleep 10 &
pids="$!"
sleep 12 &
pids="$pids;$!"
sleep 9 &
pids="$pids;$!"
WaitForTaskCompletion $pids 5 120 ${FUNCNAME[0]} false
}
# Launch the function
someting
Если у вас есть bash 4.2 или более поздняя версия, вам может быть полезно следующее. Он использует ассоциативные массивы для хранения имен задач и их "кода", а также имен задач и их значений. Я также разработал простой метод ограничения скорости, который может пригодиться, если ваши задачи потребляют много времени процессора или ввода-вывода, и вы хотите ограничить количество одновременных задач.
script запускает все задачи в первом цикле и потребляет результаты во втором.
Это немного избыточно для простых случаев, но это позволяет довольно аккуратный материал. Например, можно хранить сообщения об ошибках для каждой задачи в другом ассоциативном массиве и печатать их после того, как все опустилось.
#! /bin/bash
main () {
local -A pids=()
local -A tasks=([task1]="echo 1"
[task2]="echo 2"
[task3]="echo 3"
[task4]="false"
[task5]="echo 5"
[task6]="false")
local max_concurrent_tasks=2
for key in "${!tasks[@]}"; do
while [ $(jobs 2>&1 | grep -c Running) -ge "$max_concurrent_tasks" ]; do
sleep 1 # gnu sleep allows floating point here...
done
${tasks[$key]} &
pids+=(["$key"]="$!")
done
errors=0
for key in "${!tasks[@]}"; do
pid=${pids[$key]}
local cur_ret=0
if [ -z "$pid" ]; then
echo "No Job ID known for the $key process" # should never happen
cur_ret=1
else
wait $pid
cur_ret=$?
fi
if [ "$cur_ret" -ne 0 ]; then
errors=$(($errors + 1))
echo "$key (${tasks[$key]}) failed."
fi
done
return $errors
}
main
Я только что модифицировал script для фона и распараллеливал процесс.
Я немного экспериментировал (в Solaris с bash и ksh) и обнаружил, что "wait" выводит статус выхода, если он не равен нулю, или список заданий, возвращающих ненулевой выход, когда аргумент PID не предоставляется, Например.
Bash:
$ sleep 20 && exit 1 &
$ sleep 10 && exit 2 &
$ wait
[1]- Exit 2 sleep 20 && exit 2
[2]+ Exit 1 sleep 10 && exit 1
КШ:
$ sleep 20 && exit 1 &
$ sleep 10 && exit 2 &
$ wait
[1]+ Done(2) sleep 20 && exit 2
[2]+ Done(1) sleep 10 && exit 1
Этот вывод записывается в stderr, поэтому простым решением для примера OP может быть:
#!/bin/bash
trap "rm -f /tmp/x.$$" EXIT
for i in `seq 0 9`; do
doCalculations $i &
done
wait 2> /tmp/x.$$
if [ `wc -l /tmp/x.$$` -gt 0 ] ; then
exit 1
fi
Пока это:
wait 2> >(wc -l)
также вернет счет, но без файла tmp. Это также можно использовать таким образом, например:
wait 2> >(if [ `wc -l` -gt 0 ] ; then echo "ERROR"; fi)
Но это не намного полезнее, чем файл IMO tmp. Я не мог найти полезный способ избежать tmp файла, а также избегать запуска "wait" в подоболочке, которая вообще не работает.
У меня все получилось, и здесь собраны все лучшие части из других примеров. Этот script будет выполнять функцию checkpids
, когда любой фоновый процесс завершается, и выдает статус выхода, не прибегая к опросу.
#!/bin/bash
set -o monitor
sleep 2 &
sleep 4 && exit 1 &
sleep 6 &
pids=`jobs -p`
checkpids() {
for pid in $pids; do
if kill -0 $pid 2>/dev/null; then
echo $pid is still alive.
elif wait $pid; then
echo $pid exited with zero exit status.
else
echo $pid exited with non-zero exit status.
fi
done
echo
}
trap checkpids CHLD
wait
#!/bin/bash
set -m
for i in `seq 0 9`; do
doCalculations $i &
done
while fg; do true; done
set -m
позволяет использовать fg и bg в scriptfg
, в дополнение к последнему процессу на переднем плане, имеет тот же статус выхода, что и процесс, который он переднего планаwhile fg
прекратит цикл, когда любой fg
завершает работу с ненулевым статусом выходак сожалению, это не будет обрабатывать случай, когда процесс в фоновом режиме выходит с ненулевым статусом выхода. (цикл не будет немедленно завершен, он будет ждать завершения предыдущих процессов.)
Просто сохраните результаты из оболочки, например. в файле.
#!/bin/bash
tmp=/tmp/results
: > $tmp #clean the file
for i in `seq 0 9`; do
(doCalculations $i; echo $i:$?>>$tmp)&
done #iterate
wait #wait until all ready
sort $tmp | grep -v ':0' #... handle as required
Это работает, должно быть так же хорошо, если не лучше, чем @HoverHell ответ!
#!/usr/bin/env bash
set -m # allow for job control
EXIT_CODE=0; # exit code of overall script
function foo() {
echo "CHLD exit code is $1"
echo "CHLD pid is $2"
echo $(jobs -l)
for job in `jobs -p`; do
echo "PID => ${job}"
wait ${job} || echo "At least one test failed with exit code => $?" ; EXIT_CODE=1
done
}
trap 'foo $? $$' CHLD
DIRN=$(dirname "$0");
commands=(
"{ echo "foo" && exit 4; }"
"{ echo "bar" && exit 3; }"
"{ echo "baz" && exit 5; }"
)
clen=`expr "${#commands[@]}" - 1` # get length of commands - 1
for i in `seq 0 "$clen"`; do
(echo "${commands[$i]}" | bash) & # run the command via bash in subshell
echo "$i ith command has been issued as a background job"
done
# wait for all to finish
wait;
echo "EXIT_CODE => $EXIT_CODE"
exit "$EXIT_CODE"
# end
trap $? $$
кажется, устанавливает код выхода равным 0 и PID для текущей запущенной оболочки bash, каждый раз для меня
Здесь уже много ответов, но я удивлен, что никто, кажется, не предложил использовать массивы... Итак, вот что я сделал - это может быть полезно для некоторых в будущем.
n=10 # run 10 jobs
c=0
PIDS=()
while true
my_function_or_command &
PID=$!
echo "Launched job as PID=$PID"
PIDS+=($PID)
(( c+=1 ))
# required to prevent any exit due to error
# caused by additional commands run which you
# may add when modifying this example
true
do
if (( c < n ))
then
continue
else
break
fi
done
# collect launched jobs
for pid in "${PIDS[@]}"
do
wait $pid || echo "failed job PID=$pid"
done
- ваш друг. Вы можете использовать ERR во многих системах. Вы можете заблокировать EXIT или DEBUG, чтобы выполнить кусок кода после каждой команды.
Это в дополнение ко всем стандартным сигналам.
set -e
fail () {
touch .failure
}
expect () {
wait
if [ -f .failure ]; then
rm -f .failure
exit 1
fi
}
sleep 2 || fail &
sleep 2 && false || fail &
sleep 2 || fail
expect
Верхняя часть set -e
заставляет ваш script останавливаться при сбое.
expect
вернет 1
, если какой-либо подзаголовок не удался.
Треппинг сигнала CHLD может не работать, потому что вы можете потерять некоторые сигналы, если они пришли одновременно.
#!/bin/bash
trap 'rm -f $tmpfile' EXIT
tmpfile=$(mktemp)
doCalculations() {
echo start job $i...
sleep $((RANDOM % 5))
echo ...end job $i
exit $((RANDOM % 10))
}
number_of_jobs=10
for i in $( seq 1 $number_of_jobs )
do
( trap "echo job$i : exit value : \$? >> $tmpfile" EXIT; doCalculations ) &
done
wait
i=0
while read res; do
echo "$res"
let i++
done < "$tmpfile"
echo $i jobs done !!!
Мне это нужно, но целевой процесс не был дочерним элементом текущей оболочки, и в этом случае wait $PID
не работает. Вместо этого я нашел следующую альтернативу:
while [ -e /proc/$PID ]; do sleep 0.1 ; done
Это зависит от наличия procfs, который может быть недоступен (например, Mac не предоставляет его). Поэтому для переносимости вы можете использовать это вместо:
while ps -p $PID >/dev/null ; do sleep 0.1 ; done
Я использовал это недавно (спасибо Алнитаку):
#!/bin/bash
# activate child monitoring
set -o monitor
# locking subprocess
(while true; do sleep 0.001; done) &
pid=$!
# count, and kill when all done
c=0
function kill_on_count() {
# you could kill on whatever criterion you wish for
# I just counted to simulate bash wait with no args
[ $c -eq 9 ] && kill $pid
c=$((c+1))
echo -n '.' # async feedback (but you don't know which one)
}
trap "kill_on_count" CHLD
function save_status() {
local i=$1;
local rc=$2;
# do whatever, and here you know which one stopped
# but remember, you're called from a subshell
# so vars have their values at fork time
}
# care must be taken not to spawn more than one child per loop
# e.g don't use `seq 0 9` here!
for i in {0..9}; do
(doCalculations $i; save_status $i $?) &
done
# wait for locking subprocess to be killed
wait $pid
echo
Оттуда можно легко экстраполировать и иметь триггер (прикоснуться к файлу, отправить сигнал) и изменить критерии подсчета (прикоснитесь к файлам или что-то еще), чтобы ответить на этот триггер. Или, если вы просто хотите "any" non zero rc, просто убейте блокировку с save_status.
Я думаю, что, возможно, запустите doCalculations; эхо "$?" → /tmp/acc в подоболочке, которая отправляется на задний план, а затем ждать, тогда /tmp/acc будет содержать статусы выхода, по одному на строку. Однако я не знаю о каких-либо последствиях нескольких процессов, добавляемых к файлу аккумулятора.
Вот пример этого предложения:
Файл: doCalcualtions
#!/bin/sh random -e 20 sleep $? random -e 10
Файл: try
#!/bin/sh rm /tmp/acc for i in $( seq 0 20 ) do ( ./doCalculations "$i"; echo "$?" >>/tmp/acc ) & done wait cat /tmp/acc | fmt rm /tmp/acc
Результат работы. /try
5 1 9 6 8 1 2 0 9 6 5 9 6 0 0 4 9 5 5 9 8
wait -n
, доступный в современном bash и возвращающий только после завершения первой / следующей команды.