Как ждать в bash для нескольких подпроцессов, чтобы закончить и вернуть код выхода!=0, когда любой подпроцесс заканчивается кодом!=0?
Как ждать в скрипте bash для нескольких подпроцессов, порожденных из этого скрипта, чтобы закончить и вернуть код выхода !=0, когда любой из подпроцессов заканчивается кодом !=0 ?
простой скрипт:
#!/bin/bash
for i in `seq 0 9`; do
doCalculations $i &
done
wait
приведенный выше скрипт будет ждать все 10 порожденных подпроцессов, но он всегда будет давать статус выхода 0 (см. help wait
). Как я могу изменить этот скрипт, чтобы он обнаруживал статусы выхода порожденных подпроцессов и возвращал код выхода 1, когда любой из подпроцессов заканчивается кодом !=0?
есть ли лучшее решение для этого, чем собирать PIDs подпроцессов, ждать их по порядку и суммировать статусы выхода?
27 ответов:
wait
также (необязательно) принимает PID процесса, чтобы ждать, и с $! вы получаете PID последней команды, запущенной в фоновом режиме. Измените цикл, чтобы сохранить PID каждого порожденного подпроцесса в массив, а затем цикл снова ждет каждого PID.# run processes and store pids in array for i in $n_procs; do ./procs[${i}] & pids[${i}]=$! done # wait for all pids for pid in ${pids[*]}; do wait $pid done
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
Если у вас установлен GNU Parallel, вы можете сделать:
# If doCalculations is a function export -f doCalculations seq 0 9 | parallel doCalculations {}
GNU Parallel даст вам код выхода:
0 - Все задания выполняются без ошибок.
1-253 - некоторые задания не удалось. Статус выхода дает количество неудачных заданий
254-не удалось выполнить более 253 заданий.
255-другая ошибка.
посмотреть интро видео, чтобы узнать больше: http://pi.dk/1
вот что я придумал до сих пор. Я хотел бы посмотреть, как прервать команду сна, если ребенок завершается, так что не придется настраивать
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 ...
обновление:
как указано несколькими комментаторами, вышеизложенное ожидает завершения всех процессов перед продолжением, но не выходит и не завершается, если один из них не удается, это можно сделать со следующей модификацией, предложенной @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 ...
вот простой пример, используя
wait
.запустить какие-то процессы:
$ sleep 10 & $ sleep 10 & $ sleep 20 & $ sleep 20 &
тогда ждите их с :
$ 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.
чтобы распараллелить это...
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
на основе того, сколько параллелизма вы хотите (0
означает "все сразу").- GNU Parallel предлагает некоторые дополнительные функции при использовании вместо
xargs
-- но он не всегда устанавливается по умолчанию.- The
for
цикл не является строго необходимым в этом примере, так какecho $i
в основном просто регенерирует выход$(whatever_list
). Я просто думаю, что использованиеfor
ключевое слово делает его немного легче увидеть, что происходит.- обработка строк Bash может привести к путанице -- I обнаружили, что использование одинарных кавычек лучше всего подходит для обертывания нетривиальных скриптов.
- вы можете легко прервать всю операцию (используя ^C или аналогичный),в отличие от более прямого подхода к параллелизму Bash.
вот упрощенный пример...
for i in {0..5} ; do echo $i ; done |xargs -I{} --max-procs 2 bash -c ' { echo sleep {} sleep 2s }'
Я вижу много хороших примеров, перечисленных здесь,хотел бы бросить и мой.
#! /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
просто сохраните результаты из оболочки, например, в файле.
#!/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
вот моя версия, которая работает для нескольких PID, регистрирует предупреждения, если выполнение занимает слишком много времени, и останавливает подпроцессы, если выполнение занимает больше времени, чем заданное значение.
function WaitForTaskCompletion { local pids="" # pids to wait for, separated by semi-colon local soft_max_time="" # If execution takes longer than $soft_max_time seconds, will log a warning, unless $soft_max_time equals 0. local hard_max_time="" # If execution takes longer than $hard_max_time seconds, will stop execution, unless $hard_max_time equals 0. local caller_name="" # 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="" 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 или более поздняя версия, вам может быть полезно следующее. Он использует ассоциативные массивы для хранения имен задач и их" кода", а также имен задач и их идентификаторов. Я также построил простой метод ограничения скорости, который может пригодиться, если ваши задачи потребляют много времени процессора или ввода-вывода, и вы хотите ограничить количество параллельных задач.
скрипт запускает все задачи в первом цикле и потребляет результаты во втором.
Это немного перебор для простых случаев, но это позволяет довольно аккуратные вещи. Например, можно хранить сообщения об ошибках для каждой задачи в другом ассоциативном массиве и печатать их после того, как все успокоится.
#! /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
Я только что изменил сценарий для фона и распараллеливания процесса.
я провел некоторые эксперименты (на Solaris с bash и ksh) и обнаружил , что "wait" выводит статус выхода, если он не равен нулю, или список заданий, которые возвращают ненулевой выход, когда аргумент PID не предоставляется. Е. Г.
Баш:
$ sleep 20 && exit 1 & $ sleep 10 && exit 2 & $ wait [1]- Exit 2 sleep 20 && exit 2 [2]+ Exit 1 sleep 10 && exit 1
Ksh:
$ sleep 20 && exit 1 & $ sleep 10 && exit 2 & $ wait [1]+ Done(2) sleep 20 && exit 2 [2]+ Done(1) sleep 10 && exit 1
этот вывод записывается в stderr, поэтому простое решение для примера OPs может быть:
#!/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)
но это не очень полезно, чем файл tmp IMO. Я не мог найти полезный способ избежать файла tmp, а также избежать запуска "ожидания" в подрешетке, которая вообще не будет работать.
#!/bin/bash set -m for i in `seq 0 9`; do doCalculations $i & done while fg; do true; done
set -m
позволяет использовать fg & bg в скриптеfg
, в дополнение к размещению последнего процесса на переднем плане, имеет тот же статус выхода, что и процесс, который он выдвигает на передний планwhile fg
прекратит цикл, когда любойfg
завершает работу с ненулевым статусом выходак сожалению, это не будет обрабатывать случай, когда процесс в фоновом режиме выходит с ненулевым статусом выхода. (цикл не завершится немедленно. оно будет дождитесь завершения предыдущих процессов.)
это работает, должно быть так же хорошо, если не лучше, чем ответ @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 " echo "CHLD pid is " 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 ""); 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
и, конечно же, я увековечил этот скрипт в проекте NPM, который позволяет запускать команды bash параллельно, что полезно для тестирования:
ловушка - это ваш друг. Вы можете поймать на ERR во многих системах. Вы можете остановить выход или выполнить отладку для выполнения фрагмента кода после каждой команды.
Это в дополнение ко всем стандартным сигналов.
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
The
set -e
в верхней части делает ваш скрипт остановить при сбое.
expect
вернутся1
Если какое-либо подзадание не удалось.
здесь уже много ответов, но я удивлен, что никто, похоже, не предложил использовать массивы... Так вот что я сделал - это может быть полезно в будущем.
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
я использовал это недавно (благодаря Alnitak):
#!/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's 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=; local rc=; # 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
оттуда можно легко экстраполировать и иметь триггер (коснитесь файла, отправьте сигнал) и изменить критерии подсчета (количество файлов, затронутых или что-то еще), чтобы ответить на этот триггер. Или если вы просто хотите "любой" ненулевой rc, просто убейте блокировку из save_status.
мне это было нужно, но целевой процесс не был дочерним для текущей оболочки, и в этом случае
wait $PID
не работает. Вместо этого я нашел следующую альтернативу:while [ -e /proc/$PID ]; do sleep 0.1 ; done
Это зависит от наличия procfs, который может быть недоступен (Mac не предоставляет его, например). Поэтому для переносимости вы можете использовать это вместо:
while ps -p $PID >/dev/null ; do sleep 0.1 ; done
захват сигнала 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-n'
#!/bin/bash wait_for_pids() { for (( i = 1; i <= $#; i++ )) do wait -n $@ status=$? echo "received status: "$status if [ $status -ne 0 ] && [ $status -ne 127 ]; then exit 1 fi done } sleep_for_10() { sleep 10 exit 10 } sleep_for_20() { sleep 20 } sleep_for_10 & pid1=$! sleep_for_20 & pid2=$! wait_for_pids $pid2 $pid1
код состояния ' 127 ' предназначен для несуществующего процесса, что означает, что ребенок мог выйти.
может быть случай, когда процесс завершен до ожидания процесса. Если мы запускаем ожидание процесса, который уже завершен, он вызовет ошибку, такую как pid не является дочерним элементом этой оболочки. Чтобы избежать таких случаев, можно использовать следующую функцию для определения того, завершен ли процесс или нет:
isProcessComplete(){ PID= while [ -e /proc/$PID ] do echo "Process: $PID is still running" sleep 5 done echo "Process $PID has finished" }
Я думаю, что самый прямой способ запустить работу параллельно и проверить статус-это использовать временные файлы. Уже есть несколько подобных ответов (например, Ницше-Джоу и mug896).
#!/bin/bash rm -f fail for i in `seq 0 9`; do doCalculations $i || touch fail & done wait ! [ -f fail ]
приведенный выше код не является потокобезопасным. Если вы обеспокоены тем, что приведенный выше код будет работать одновременно с самим собой, лучше использовать более уникальное имя файла, например fail.$$. Последняя строка выполнить требование: "вернуть код выхода 1, когда любой из подпроцессы заканчиваются кодом !=0?"Я бросил туда дополнительное требование, чтобы очистить. Возможно, было бы яснее написать это так:
#!/bin/bash trap 'rm -f fail.$$' EXIT for i in `seq 0 9`; do doCalculations $i || touch fail.$$ & done wait ! [ -f fail.$$ ]
вот аналогичный фрагмент для сбора результатов из нескольких заданий: я создаю временный каталог, рассказываю результаты всех подзадач в отдельном файле, а затем сбрасываю их для просмотра. Это действительно не соответствует вопросу - я бросаю его в качестве бонуса:
#!/bin/bash trap 'rm -fr $WORK' EXIT WORK=/tmp/$$.work mkdir -p $WORK cd $WORK for i in `seq 0 9`; do doCalculations $i >$i.result & done wait grep $ * # display the results with filenames and contents
Я думаю, может быть, запустить doCalculations; echo"$?">>/tmp/acc в A subshell то, что отправляется в фоновом режиме, то ждать, то / tmp / acc будет содержать статусы выхода, по одному на строку. Однако я не знаю о каких-либо последствиях нескольких процессов, добавляющихся к файлу аккумулятора.
вот испытание этого предложения:
File: doCalcualtions
#!/bin/sh random -e 20 sleep $? random -e 10: попробуй
#!/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выход работает ./ попробуй
5 1 9 6 8 1 2 0 9 6 5 9 6 0 0 4 9 5 5 9 8