Wie kann man in Bash warten, bis mehrere Unterprozesse abgeschlossen sind, und den Exit-Code zurückgeben! = 0, wenn ein Unterprozess mit Code endet! = 0?


562

Wie kann man in einem Bash-Skript auf mehrere Unterprozesse warten, die aus diesem Skript hervorgegangen sind, um den Exit-Code zu beenden und zurückzugeben! = 0, wenn einer der Unterprozesse mit Code endet! = 0?

Einfaches Skript:

#!/bin/bash
for i in `seq 0 9`; do
  doCalculations $i &
done
wait

Das obige Skript wartet auf alle 10 erzeugten Unterprozesse, gibt jedoch immer den Exit-Status 0 (siehe help wait). Wie kann ich dieses Skript so ändern, dass es den Beendigungsstatus von erzeugten Unterprozessen erkennt und den Beendigungscode 1 zurückgibt, wenn einer der Unterprozesse mit Code endet! = 0?

Gibt es dafür eine bessere Lösung, als PIDs der Unterprozesse zu sammeln, in der richtigen Reihenfolge auf sie zu warten und den Exit-Status zu summieren?


1
Dies könnte erheblich verbessert werden, um es zu berühren. Es wait -nist in der modernen Bash verfügbar und kann nur zurückgegeben werden, wenn der erste / nächste Befehl abgeschlossen ist.
Charles Duffy

Wenn Sie mit Bash testen möchten, versuchen Sie dies: github.com/sstephenson/bats
Alexander Mills

2
Die aktive Entwicklung von BATS wurde auf github.com/bats-core/bats-core
Potherca am

3
@CharlesDuffy wait -nhat ein kleines Problem: Wenn keine untergeordneten Jobs mehr vorhanden sind (auch als Race-Bedingung bezeichnet), wird ein Exit-Status ungleich Null (fehlgeschlagen) zurückgegeben, der von einem fehlgeschlagenen untergeordneten Prozess nicht zu unterscheiden ist.
Drevicko

5
@CharlesDuffy - Sie haben wundervolle Einblicke und leisten SO einen großen Dienst, indem Sie sie teilen. Es scheint, dass ungefähr 80% der SO-Beiträge, die ich lese, Sie dazu bringen, wunderbare kleine Diamanten des Wissens in den Kommentaren zu teilen, die aus einem riesigen Ozean von Erfahrungen stammen müssen. Danke vielmals!
Brett Holman

Antworten:


520

waitAußerdem (optional) wird die PID des Prozesses zum Warten verwendet, und zwar mit $! Sie erhalten die PID des zuletzt im Hintergrund gestarteten Befehls. Ändern Sie die Schleife, um die PID jedes erzeugten Unterprozesses in einem Array zu speichern, und warten Sie dann erneut auf jede 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

9
Weel, da Sie auf alle Prozesse warten, spielt es keine Rolle, ob Sie z. B. auf den ersten warten, während der zweite bereits abgeschlossen ist (der zweite wird ohnehin bei der nächsten Iteration ausgewählt). Es ist der gleiche Ansatz, den Sie in C mit wait (2) verwenden würden.
Luca Tettamanti

7
Ah, ich verstehe - andere Interpretation :) Ich habe die Frage so gelesen, dass sie "Exit-Code 1 sofort zurückgibt, wenn einer der Unterprozesse beendet wird".
Alnitak

56
Die PID kann zwar wiederverwendet werden, Sie können jedoch nicht auf einen Prozess warten, der dem aktuellen Prozess nicht untergeordnet ist (in diesem Fall schlägt das Warten fehl).
Tkokoszka

12
Sie können auch% n verwenden, um auf den n-ten Job im Hintergrund zu verweisen, und %%, um auf den neuesten Job zu verweisen.
Conny

30
@Nils_M: Du hast recht, es tut mir leid. Es wäre also so etwas wie: for i in $n_procs; do ./procs[${i}] & ; pids[${i}]=$!; done; for pid in ${pids[*]}; do wait $pid; done;richtig?
Synack

284

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

103
jobs -pgibt PIDs von Unterprozessen an, die sich im Ausführungsstatus befinden. Ein Prozess wird übersprungen, wenn der zuvor abgeschlossene Prozess jobs -paufgerufen wird. Wenn also einer der Unterprozesse zuvor endet jobs -p, geht der Exit-Status dieses Prozesses verloren.
Tkokoszka

15
Wow, diese Antwort ist viel besser als die am besten bewertete. : /
e40

4
@ e40 und die Antwort unten ist wahrscheinlich noch besser. Und noch besser wäre es wahrscheinlich, jeden Befehl mit '(cmd; echo "$?" >> "$ tmpfile") auszuführen, diese Wartezeit zu verwenden und dann die Datei für die Fehler zu lesen. Auch Annotate-Ausgabe. … Oder verwenden Sie einfach dieses Skript, wenn Sie sich nicht so sehr darum kümmern.
HoverHell

Ich möchte hinzufügen, dass diese Antwort besser ist als akzeptiert
Shurikk

2
@tkokoszka um genau zu sein, jobs -pgibt keine PIDs von Unterprozessen an, sondern GPIDs . Die Wartelogik scheint sowieso zu funktionieren, sie wartet immer auf die Gruppe, wenn eine solche Gruppe existiert, und pid, wenn nicht, aber es ist gut, sich dessen bewusst zu sein. Insbesondere, wenn man darauf aufbaut und so etwas wie das Senden von Nachrichten an den Unterprozess einbezieht, in dem Fall die Syntax ist unterschiedlich, je nachdem, ob Sie PIDs oder GPIDs haben .. dh kill -- -$GPIDvskill $PID
Timo

58

Hier ist ein einfaches Beispiel mit wait.

Führen Sie einige Prozesse aus:

$ sleep 10 &
$ sleep 10 &
$ sleep 20 &
$ sleep 20 &

Dann warten Sie mit waitBefehl auf sie:

$ wait < <(jobs -p)

Oder einfach wait(ohne Argumente) für alle.

Dies wartet, bis alle Jobs im Hintergrund abgeschlossen sind.

Wenn die -nOption angegeben ist, wartet sie auf das Beenden des nächsten Jobs und gibt seinen Beendigungsstatus zurück.

Siehe: help waitund help jobsfür die Syntax.

Der Nachteil ist jedoch, dass nur der Status der letzten ID zurückgegeben wird. Sie müssen daher den Status für jeden Unterprozess überprüfen und in der Variablen speichern.

Oder lassen Sie Ihre Berechnungsfunktion eine Datei bei einem Fehler erstellen (leer oder mit Fehlerprotokoll) und überprüfen Sie diese Datei, falls vorhanden, z

$ sleep 20 && true || tee fail &
$ sleep 20 && false || tee fail &
$ wait < <(jobs -p)
$ test -f fail && echo Calculation failed.

1
Für diejenigen, die neu in Bash sind, sind die beiden Berechnungen im Beispiel hier sleep 20 && trueund sleep 20 && false- dh: Ersetzen Sie diese durch Ihre Funktion (en). Um zu verstehen &&und ||, führen Sie man bash'/' (Suche) aus und geben Sie '^ * Lists' (ein &&||
regulärer Ausdruck) ein. Geben Sie dann Folgendes

1
Sie sollten wahrscheinlich überprüfen, ob die Datei 'fail' beim Start nicht vorhanden ist (oder sie löschen). Abhängig von der Anwendung kann es auch eine gute Idee sein, '2> & 1' hinzuzufügen, bevor ||STDERR ebenfalls fehlschlägt.
Drevicko

Ich mag dieses, irgendwelche Nachteile? Eigentlich nur, wenn ich alle Unterprozesse auflisten und einige Aktionen ausführen möchte, z. Sende ein Signal, dass ich versuchen werde, Pids zu buchen oder Jobs zu iterieren. Warten Sie bis zum Ende, nurwait
xgwang

Dadurch wird der Exit-Status des Jobs verpasst, der fehlgeschlagen ist, bevor Jobs -p aufgerufen werden
Erik Aronesty

50

Wenn Sie GNU Parallel installiert haben, können Sie Folgendes tun:

# If doCalculations is a function
export -f doCalculations
seq 0 9 | parallel doCalculations {}

GNU Parallel gibt Ihnen den Exit-Code:

  • 0 - Alle Jobs wurden ohne Fehler ausgeführt.

  • 1-253 - Einige der Jobs sind fehlgeschlagen. Der Exit-Status gibt die Anzahl der fehlgeschlagenen Jobs an

  • 254 - Mehr als 253 Jobs sind fehlgeschlagen.

  • 255 - Anderer Fehler.

Sehen Sie sich die Intro-Videos an, um mehr zu erfahren: http://pi.dk/1


1
Vielen Dank! Aber Sie haben vergessen, das "Verwirrungs" -Problem
bin

1
Dies sieht nach einem großartigen Tool aus, aber ich denke nicht, dass das oben Genannte in einem Bash-Skript so doCalculationsfunktioniert, wie es in demselben Skript definiert ist (obwohl das OP über diese Anforderung nicht klar war). Wenn ich es versuche, parallelsagt /bin/bash: doCalculations: command not found(sagt es 10 mal für das seq 0 9obige Beispiel). Sehen Sie hier für dieses Problem zu umgehen.
Nobar

3
Ebenfalls von Interesse: xargshat einige Möglichkeiten, Jobs über die -POption parallel zu starten . Von hier aus : export -f doCalculations ; seq 0 9 |xargs -P 0 -n 1 -I{} bash -c "doCalculations {}". Einschränkungen von xargssind in der Manpage für aufgeführt parallel.
Nobar

Wenn Sie doCalculationssich auf andere skriptinterne Umgebungsvariablen (benutzerdefiniert PATHusw.) stützen , müssen diese wahrscheinlich exportvor dem Start explizit bearbeitet werden parallel.
Nobar

4
@nobar Die Verwirrung ist darauf zurückzuführen, dass einige Packager die Dinge für ihre Benutzer durcheinander bringen. Wenn Sie mit installieren, werden wget -O - pi.dk/3 | shSie keine Verwirrungen bekommen. Wenn Ihr Packager die Dinge für Sie durcheinander gebracht hat, empfehle ich Ihnen, das Problem mit Ihrem Packager anzusprechen. Variablen und Funktionen sollten exportiert werden (export -f), damit GNU Parallel sie sehen kann (siehe man parallel: gnu.org/software/parallel/… )
Ole Tange

46

Wie wäre es einfach:

#!/bin/bash

pids=""

for i in `seq 0 9`; do
   doCalculations $i &
   pids="$pids $!"
done

wait $pids

...code continued here ...

Aktualisieren:

Wie von mehreren Kommentatoren erwähnt, wartet das Obige darauf, dass alle Prozesse abgeschlossen sind, bevor es fortgesetzt wird. Es wird jedoch nicht beendet und schlägt fehl, wenn einer von ihnen fehlschlägt. Dies kann mit der folgenden von @Bryan, @SamBrightman und anderen vorgeschlagenen Änderung geschehen ::

#!/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 ...

1
Laut Warteseiten gibt das Warten mit mehreren PIDs nur den Rückgabewert des letzten Prozesses zurück, auf den gewartet wurde. Sie benötigen also eine zusätzliche Schleife und warten separat auf jede PID, wie in der akzeptierten Antwort (in Kommentaren) vorgeschlagen.
Vlad Frolov

1
Da es nirgendwo anders auf dieser Seite angegeben zu sein scheint, werde ich hinzufügen, dass die Schleife wärefor pid in $pids; do wait $pid; done
Bryan

1
@bisounours_tronconneuse ja, das tust du. Siehe help wait- mit mehreren IDs wird nur waitder Exit-Code des letzten zurückgegeben, wie @ vlad-frolov oben sagte.
Sam Brightman

1
Bryan, @SamBrightman Ok. Ich habe es mit Ihren Empfehlungen geändert.
Patapouf_ai

4
Ich hatte ein offensichtliches Problem mit dieser Lösung: Was ist, wenn ein bestimmter Prozess beendet wird, bevor der entsprechende waitaufgerufen wird? Es stellt sich heraus, dass dies kein Problem ist: Wenn Sie waiteinen Prozess beenden, der bereits beendet wurde, waitwird er sofort mit dem Status des bereits beendeten Prozesses beendet. (Danke, bashAutoren!)
Daniel Griscom

39

Folgendes habe ich mir bisher ausgedacht. Ich würde gerne sehen, wie der Schlafbefehl unterbrochen werden kann, wenn ein Kind beendet wird, damit man sich nicht WAITALL_DELAYauf seine Verwendung einstellen muss.

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

Man könnte möglicherweise WAITALL_DELAY überspringen oder sehr niedrig einstellen, da keine Prozesse innerhalb der Schleife gestartet werden. Ich denke nicht, dass es zu teuer ist.
Marian

21

Um dies zu parallelisieren ...

for i in $(whatever_list) ; do
   do_something $i
done

Übersetzen Sie es in diese ...

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 {}
      }' 
   )
  • Wenn in einem Prozess ein Fehler auftritt , werden die anderen Prozesse nicht unterbrochen, es wird jedoch ein Exit-Code ungleich Null aus der gesamten Sequenz erstellt .
  • Das Exportieren von Funktionen und Variablen kann in einem bestimmten Fall erforderlich sein oder auch nicht.
  • Sie können festlegen --max-procs, wie viel Parallelität Sie möchten ( 0bedeutet "alles auf einmal").
  • GNU Parallel bietet einige zusätzliche Funktionen, wenn es anstelle von verwendet wird xargs- es wird jedoch nicht immer standardmäßig installiert.
  • Die forSchleife ist in diesem Beispiel nicht unbedingt erforderlich, da echo $iim Grunde nur die Ausgabe von $(whatever_list) neu generiert wird . Ich denke nur, dass die Verwendung des forSchlüsselworts es ein wenig einfacher macht, zu sehen, was los ist.
  • Die Behandlung von Bash-Strings kann verwirrend sein. Ich habe festgestellt, dass die Verwendung von einfachen Anführungszeichen am besten zum Umschließen von nicht trivialen Skripten geeignet ist.
  • Sie können den gesamten Vorgang leicht unterbrechen (mit ^ C oder ähnlichem), im Gegensatz zu der direkteren Herangehensweise an die Bash-Parallelität .

Hier ist ein vereinfachtes Arbeitsbeispiel ...

for i in {0..5} ; do echo $i ; done |xargs -I{} --max-procs 2 bash -c '
   {
   echo sleep {}
   sleep 2s
   }'


7

Ich glaube nicht, dass es mit Bashs eingebauter Funktionalität möglich ist.

Sie können eine Benachrichtigung erhalten, wenn ein Kind beendet wird:

#!/bin/sh
set -o monitor        # enable script job control
trap 'echo "child died"' CHLD

Es gibt jedoch keine offensichtliche Möglichkeit, den Ausgangsstatus des Kindes im Signalhandler abzurufen.

Das Abrufen dieses waituntergeordneten Status ist normalerweise Aufgabe der Funktionsfamilie in den POSIX-APIs der unteren Ebene. Leider ist die Unterstützung von Bash dafür begrenzt - Sie können auf einen bestimmten untergeordneten Prozess warten (und dessen Beendigungsstatus erhalten) oder Sie können auf alle warten und erhalten immer ein 0-Ergebnis.

Was unmöglich erscheint, ist das Äquivalent von waitpid(-1), das blockiert, bis ein untergeordneter Prozess zurückkehrt.


7

Ich sehe viele gute Beispiele hier aufgelistet, wollte auch meine einwerfen.

#! /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

Ich verwende etwas sehr Ähnliches, um Server / Dienste parallel zu starten / stoppen und jeden Exit-Status zu überprüfen. Funktioniert gut für mich. Hoffe das hilft jemandem!


Wenn ich es mit Strg + CI stoppe, werden immer noch Prozesse im Hintergrund ausgeführt.
Karsten

2
@ Karsten - das ist ein anderes Problem. Angenommen, Sie verwenden Bash, können Sie eine Beendigungsbedingung (einschließlich Strg + C) abfangen und den aktuellen und alle untergeordneten Prozesse mittrap "kill 0" EXIT
Phil

@Phil ist richtig. Da es sich um Hintergrundprozesse handelt, werden beim Beenden des übergeordneten Prozesses nur alle untergeordneten Prozesse ausgeführt. In meinem Beispiel werden keine Signale abgefangen, die bei Bedarf hinzugefügt werden können, wie Phil angegeben hat.
Jason Slobotski

6

Dies ist etwas, das ich benutze:

#wait for jobs
for job in `jobs -p`; do wait ${job}; done

5

Der folgende Code wartet auf den Abschluss aller Berechnungen und gibt den Exit-Status 1 zurück, wenn eine der doCalculations fehlschlägt.

#!/bin/bash
for i in $(seq 0 9); do
   (doCalculations $i >&2 & wait %1; echo $?) &
done | grep -qv 0 && exit 1

5

Speichern Sie die Ergebnisse einfach aus der Shell, z. B. in einer Datei.

#!/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

5

Hier ist meine Version, die für mehrere Pids funktioniert, Warnungen protokolliert, wenn die Ausführung zu lange dauert, und die Unterprozesse stoppt, wenn die Ausführung länger als ein bestimmter Wert dauert.

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
}

Warten Sie beispielsweise, bis alle drei Prozesse abgeschlossen sind, protokollieren Sie eine Warnung, wenn die Ausführung länger als 5 Sekunden dauert, und stoppen Sie alle Prozesse, wenn die Ausführung länger als 120 Sekunden dauert. Beenden Sie das Programm nicht bei Fehlern.

function something {

    sleep 10 &
    pids="$!"
    sleep 12 &
    pids="$pids;$!"
    sleep 9 &
    pids="$pids;$!"

    WaitForTaskCompletion $pids 5 120 ${FUNCNAME[0]} false
}
# Launch the function
someting

4

Wenn Sie Bash 4.2 oder höher zur Verfügung haben, kann Folgendes für Sie hilfreich sein. Es verwendet assoziative Arrays, um Aufgabennamen und ihren "Code" sowie Aufgabennamen und ihre Pids zu speichern. Ich habe auch eine einfache Methode zur Ratenbegrenzung eingebaut, die nützlich sein kann, wenn Ihre Aufgaben viel CPU- oder E / A-Zeit beanspruchen und Sie die Anzahl gleichzeitiger Aufgaben begrenzen möchten.

Das Skript startet alle Aufgaben in der ersten Schleife und verwendet die Ergebnisse in der zweiten.

Dies ist ein bisschen übertrieben für einfache Fälle, aber es erlaubt ziemlich ordentliche Sachen. Beispielsweise kann man Fehlermeldungen für jede Aufgabe in einem anderen assoziativen Array speichern und drucken, nachdem sich alles beruhigt hat.

#! /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

4

Ich habe gerade ein Skript an den Hintergrund angepasst und einen Prozess parallelisiert.

Ich habe einige Experimente durchgeführt (unter Solaris mit bash und ksh) und festgestellt, dass 'wait' den Exit-Status ausgibt, wenn er nicht Null ist, oder eine Liste von Jobs, die einen Exit ungleich Null zurückgeben, wenn kein PID-Argument angegeben wird. Z.B

Bash:

$ 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

Diese Ausgabe wird in stderr geschrieben, daher könnte eine einfache Lösung für das OP-Beispiel sein:

#!/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

Während dies:

wait 2> >(wc -l)

gibt auch eine Zählung zurück, jedoch ohne die tmp-Datei. Dies kann auch folgendermaßen verwendet werden, zum Beispiel:

wait 2> >(if [ `wc -l` -gt 0 ] ; then echo "ERROR"; fi)

Dies ist jedoch nicht viel nützlicher als die tmp-Datei IMO. Ich konnte keinen nützlichen Weg finden, um die tmp-Datei zu vermeiden und gleichzeitig zu vermeiden, dass das "Warten" in einer Subshell ausgeführt wird, was überhaupt nicht funktioniert.


3

Ich habe es versucht und die besten Teile aus den anderen Beispielen hier kombiniert. Dieses Skript führt die checkpidsFunktion aus, wenn ein Hintergrundprozess beendet wird, und gibt den Beendigungsstatus aus, ohne auf Abfragen zurückzugreifen.

#!/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

3
#!/bin/bash
set -m
for i in `seq 0 9`; do
  doCalculations $i &
done
while fg; do true; done
  • set -m ermöglicht es Ihnen, fg & bg in einem Skript zu verwenden
  • fghat nicht nur den letzten Prozess in den Vordergrund gestellt, sondern auch den gleichen Beendigungsstatus wie der Prozess, den er in den Vordergrund stellt
  • while fgstoppt die Schleife, wenn fgExits mit einem Exit-Status ungleich Null beendet werden

Leider wird dies nicht behandelt, wenn ein Prozess im Hintergrund mit einem Exit-Status ungleich Null beendet wird. (Die Schleife wird nicht sofort beendet. Sie wartet, bis die vorherigen Prozesse abgeschlossen sind.)


3

Hier gibt es bereits viele Antworten, aber ich bin überrascht, dass niemand die Verwendung von Arrays vorgeschlagen hat ... Also hier ist, was ich getan habe - dies könnte für einige in Zukunft nützlich sein.

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

3

Dies funktioniert, sollte genauso gut sein, wenn nicht besser als die Antwort von @ 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

und natürlich habe ich dieses Skript in einem NPM-Projekt verewigt, mit dem Sie Bash-Befehle parallel ausführen können, was zum Testen nützlich ist:

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


trap $? $$scheint den Exit-Code auf 0 und die PID auf die aktuell laufende Bash-Shell zu setzen, jedes Mal für mich
inetknght

Bist du dir da absolut sicher? Ich bin mir nicht sicher, ob das Sinn macht.
Alexander Mills

2

Falle ist dein Freund. Sie können ERR in vielen Systemen abfangen. Sie können EXIT oder DEBUG abfangen, um nach jedem Befehl einen Code auszuführen.

Dies zusätzlich zu allen Standardsignalen.


1
Bitte können Sie Ihre Antwort anhand einiger Beispiele erläutern.
17οδεMεδιϲ

2
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

Die set -eoben macht Ihren Skript Stopp eines Fehler auftritt .

expectwird zurückgegeben, 1wenn ein Subjob fehlgeschlagen ist.


2

Genau zu diesem Zweck habe ich eine bashFunktion namens geschrieben :for.

Hinweis : :forDer Exit-Code der fehlerhaften Funktion wird nicht nur beibehalten und zurückgegeben, sondern auch alle parallel ausgeführten Instanzen werden beendet. Was in diesem Fall möglicherweise nicht benötigt wird.

#!/usr/bin/env bash

# Wait for pids to terminate. If one pid exits with
# a non zero exit code, send the TERM signal to all
# processes and retain that exit code
#
# usage:
# :wait 123 32
function :wait(){
    local pids=("$@")
    [ ${#pids} -eq 0 ] && return $?

    trap 'kill -INT "${pids[@]}" &>/dev/null || true; trap - INT' INT
    trap 'kill -TERM "${pids[@]}" &>/dev/null || true; trap - RETURN TERM' RETURN TERM

    for pid in "${pids[@]}"; do
        wait "${pid}" || return $?
    done

    trap - INT RETURN TERM
}

# Run a function in parallel for each argument.
# Stop all instances if one exits with a non zero
# exit code
#
# usage:
# :for func 1 2 3
#
# env:
# FOR_PARALLEL: Max functions running in parallel
function :for(){
    local f="${1}" && shift

    local i=0
    local pids=()
    for arg in "$@"; do
        ( ${f} "${arg}" ) &
        pids+=("$!")
        if [ ! -z ${FOR_PARALLEL+x} ]; then
            (( i=(i+1)%${FOR_PARALLEL} ))
            if (( i==0 )) ;then
                :wait "${pids[@]}" || return $?
                pids=()
            fi
        fi
    done && [ ${#pids} -eq 0 ] || :wait "${pids[@]}" || return $?
}

Verwendungszweck

for.sh::

#!/usr/bin/env bash
set -e

# import :for from gist: https://gist.github.com/Enteee/c8c11d46a95568be4d331ba58a702b62#file-for
# if you don't like curl imports, source the actual file here.
source <(curl -Ls https://gist.githubusercontent.com/Enteee/c8c11d46a95568be4d331ba58a702b62/raw/)

msg="You should see this three times"

:(){
  i="${1}" && shift

  echo "${msg}"

  sleep 1
  if   [ "$i" == "1" ]; then sleep 1
  elif [ "$i" == "2" ]; then false
  elif [ "$i" == "3" ]; then
    sleep 3
    echo "You should never see this"
  fi
} && :for : 1 2 3 || exit $?

echo "You should never see this"
$ ./for.sh; echo $?
You should see this three times
You should see this three times
You should see this three times
1

Verweise


1

Ich habe das kürzlich benutzt (danke an 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=$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

Von dort aus kann man leicht extrapolieren und einen Auslöser haben (eine Datei berühren, ein Signal senden) und die Zählkriterien ändern (berührte Zähldateien oder was auch immer), um auf diesen Auslöser zu reagieren. Oder wenn Sie nur 'irgendeinen' RC ungleich Null wollen, beenden Sie einfach die Sperre von save_status.


1

Ich brauchte das, aber der Zielprozess war kein Kind der aktuellen Shell. In diesem Fall wait $PIDfunktioniert es nicht. Ich habe stattdessen die folgende Alternative gefunden:

while [ -e /proc/$PID ]; do sleep 0.1 ; done

Dies hängt vom Vorhandensein von Prozessen ab , die möglicherweise nicht verfügbar sind (Mac bietet sie beispielsweise nicht an). Aus Gründen der Portabilität können Sie stattdessen Folgendes verwenden:

while ps -p $PID >/dev/null ; do sleep 0.1 ; done

1

Das Einfangen des CHLD-Signals funktioniert möglicherweise nicht, da Sie einige Signale verlieren können, wenn sie gleichzeitig ankommen.

#!/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 !!!

1

Die Lösung zum Warten auf mehrere Unterprozesse und zum Beenden, wenn einer von ihnen mit einem Statuscode ungleich Null beendet wird, besteht in der Verwendung von '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

Der Statuscode '127' steht für einen nicht vorhandenen Prozess, was bedeutet, dass das Kind möglicherweise beendet wurde.


1

Warten Sie auf alle Jobs und geben Sie den Exit-Code des letzten fehlgeschlagenen Jobs zurück. Im Gegensatz zu den oben genannten Lösungen erfordert dies keine PID-Einsparung. Einfach weg und warten.

function wait_ex {
    # this waits for all jobs and returns the exit code of the last failing job
    ecode=0
    while true; do
        wait -n
        err="$?"
        [ "$err" == "127" ] && break
        [ "$err" != "0" ] && ecode="$err"
    done
    return $ecode
}

Dies funktioniert und gibt zuverlässig den ersten Fehlercode aus Ihren ausgeführten Befehlen aus, es sei denn, es handelt sich um "Befehl nicht gefunden" (Code 127).
Drevicko

0

Es kann vorkommen, dass der Prozess abgeschlossen ist, bevor auf den Prozess gewartet wird. Wenn wir das Warten auf einen bereits abgeschlossenen Prozess auslösen, wird ein Fehler ausgelöst, da pid kein untergeordnetes Element dieser Shell ist. Um solche Fälle zu vermeiden, kann die folgende Funktion verwendet werden, um festzustellen, ob der Prozess abgeschlossen ist oder nicht:

isProcessComplete(){
PID=$1
while [ -e /proc/$PID ]
do
    echo "Process: $PID is still running"
    sleep 5
done
echo "Process $PID has finished"
}

0

Ich denke, dass die einfachste Möglichkeit, Jobs parallel auszuführen und den Status zu überprüfen, die Verwendung temporärer Dateien ist. Es gibt bereits einige ähnliche Antworten (zB Nietzche-jou und mug896).

#!/bin/bash
rm -f fail
for i in `seq 0 9`; do
  doCalculations $i || touch fail &
done
wait 
! [ -f fail ]

Der obige Code ist nicht threadsicher. Wenn Sie befürchten, dass der obige Code gleichzeitig mit sich selbst ausgeführt wird, ist es besser, einen eindeutigeren Dateinamen zu verwenden, z. B. fail. $$. Die letzte Zeile soll die Anforderung erfüllen: "Exit-Code 1 zurückgeben, wenn einer der Unterprozesse mit Code endet! = 0?" Ich warf eine zusätzliche Anforderung hinein, um aufzuräumen. Es mag klarer gewesen sein, es so zu schreiben:

#!/bin/bash
trap 'rm -f fail.$$' EXIT
for i in `seq 0 9`; do
  doCalculations $i || touch fail.$$ &
done
wait 
! [ -f fail.$$ ] 

Hier ist ein ähnliches Snippet zum Sammeln von Ergebnissen aus mehreren Jobs: Ich erstelle ein temporäres Verzeichnis, schreibe die Ausgaben aller Unteraufgaben in eine separate Datei und speichere sie dann zur Überprüfung. Dies passt nicht wirklich zu der Frage - ich werfe sie als Bonus ein:

#!/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

0

Ich bin fast in die Falle jobs -pgeraten, PIDs zu sammeln, was nicht funktioniert, wenn das Kind bereits beendet wurde, wie im folgenden Skript gezeigt. Die Lösung, die ich gewählt habe, war einfach wait -nN-mal anzurufen , wobei N die Anzahl meiner Kinder ist, die ich zufällig deterministisch kenne.

#!/usr/bin/env bash

sleeper() {
    echo "Sleeper $1"
    sleep $2
    echo "Exiting $1"
    return $3
}

start_sleepers() {
    sleeper 1 1 0 &
    sleeper 2 2 $1 &
    sleeper 3 5 0 &
    sleeper 4 6 0 &
    sleep 4
}

echo "Using jobs"
start_sleepers 1

pids=( $(jobs -p) )

echo "PIDS: ${pids[*]}"

for pid in "${pids[@]}"; do
    wait "$pid"
    echo "Exit code $?"
done

echo "Clearing other children"
wait -n; echo "Exit code $?"
wait -n; echo "Exit code $?"

echo "Waiting for N processes"
start_sleepers 2

for ignored in $(seq 1 4); do
    wait -n
    echo "Exit code $?"
done

Ausgabe:

Using jobs
Sleeper 1
Sleeper 2
Sleeper 3
Sleeper 4
Exiting 1
Exiting 2
PIDS: 56496 56497
Exiting 3
Exit code 0
Exiting 4
Exit code 0
Clearing other children
Exit code 0
Exit code 1
Waiting for N processes
Sleeper 1
Sleeper 2
Sleeper 3
Sleeper 4
Exiting 1
Exiting 2
Exit code 0
Exit code 2
Exiting 3
Exit code 0
Exiting 4
Exit code 0
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.