Как ждать в 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 430

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 параллельно, что полезно для тестирования:

https://github.com/ORESoftware/generic-subshell

ловушка - это ваш друг. Вы можете поймать на 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