765 Stimmen

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

Wie kann man in einem Bash-Skript warten, bis mehrere von diesem Skript erzeugte Unterprozesse beendet sind, und dann einen Exit-Code zurückgeben !=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 gespawnten Unterprozesse, aber es gibt immer den Exit-Status 0 (siehe help wait ). Wie kann ich dieses Skript so ändern, dass es den Exit-Status der gespawnten Unterprozesse erkennt und den Exit-Code 1 wenn einer der Unterprozesse mit Code endet !=0 ?

Gibt es dafür eine bessere Lösung als das Sammeln der PIDs der Unterprozesse, das Warten auf sie in der Reihenfolge und die Summe der Exit-Status?

2 Stimmen

Dies könnte erheblich verbessert werden, um auf folgende Punkte einzugehen wait -n die in der modernen Bash verfügbar ist, um nur zurückzukehren, wenn der erste/nächste Befehl abgeschlossen ist.

0 Stimmen

Wenn Sie mit der Bash testen wollen, versuchen Sie dies: github.com/sstephenson/bats

3 Stimmen

Die aktive Entwicklung von BATS hat sich auf github.com/bats-core/bats-core

40voto

Mark Edgar Punkte 4467

Hier ist, was ich bis jetzt gefunden habe. Ich würde gerne sehen, wie man den Schlafbefehl unterbrechen kann, wenn ein Kind beendet wird, so dass man nicht WAITALL_DELAY für den eigenen Gebrauch.

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

23voto

Brent Bradburn Punkte 45995

Um dies zu parallelisieren...

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

Übersetzen Sie es folgendermaßen...

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 ein Fehler auftritt in einem Prozess, wird es die anderen Prozesse nicht unterbrechen, aber führt dies zu einem Exit-Code ungleich Null in der gesamten Sequenz .
  • Der Export von Funktionen und Variablen kann im Einzelfall notwendig sein, muss es aber nicht.
  • Sie können einstellen --max-procs je nachdem, wie viel Parallelität Sie wünschen ( 0 bedeutet "alles auf einmal").
  • GNU Parallel bietet einige zusätzliche Funktionen, wenn es anstelle von xargs -- aber es ist nicht immer standardmäßig installiert.
  • El for Schleife ist in diesem Beispiel nicht unbedingt notwendig, da echo $i ist im Grunde nur eine Regeneration der Ausgabe von $(whatever_list ). Ich denke nur, dass die Verwendung des for Das Schlüsselwort macht es ein wenig einfacher zu sehen, was vor sich geht.
  • Die Handhabung von Zeichenketten in der Bash kann verwirrend sein - ich habe herausgefunden, dass die Verwendung von einfachen Anführungszeichen am besten für die Umhüllung von nicht-trivialen Skripten funktioniert.
  • Sie können den gesamten Vorgang einfach unterbrechen (mit ^C oder ähnlichem), im Gegensatz zu dem direkteren Ansatz der 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
   }'

17voto

jplozier Punkte 161

Das ist etwas, das ich verwende:

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

12voto

Gabriel Staples Punkte 20228

Dies ist eine Erweiterung von die am häufigsten gewählte Antwort, von @Luca Tettamanti , um eine voll funktionsfähig Beispiel.

Diese Antwort hat mich stutzig gemacht :

Welcher Typ von Variable ist n_procs und was enthält es? Welcher Typ von Variable ist procs und was enthält sie? Kann bitte jemand diese Antwort aktualisieren, um sie lauffähig zu machen, indem er Definitionen für diese Variablen hinzufügt? Ich verstehe nicht, wie.

...und auch:

  • Wie erhalten Sie den Rückgabecode vom Unterprozess, wenn er abgeschlossen ist (was der Kernpunkt dieser Frage ist)?

Wie auch immer, ich habe es herausgefunden, also hier ist ein voll funktionsfähig Beispiel.

Anmerkungen:

  1. $! es wie man die PID (Prozess-ID) des zuletzt ausgeführten Unterprozesses erhält .
  2. Die Ausführung eines beliebigen Befehls mit der & danach, wie cmd & zum Beispiel bewirkt, dass er im Hintergrund als paralleler Suprozess zum Hauptprozess läuft.
  3. myarray=() ist, wie man ein Array in der Bash erstellt.
  4. Um ein wenig mehr über die wait eingebauter Befehl, siehe help wait . Siehe auch und insbesondere die offizielles Bash-Benutzerhandbuch auf Job Control built-ins, wie zum Beispiel wait y jobs , hier: https://www.gnu.org/software/bash/manual/html_node/Job-Control-Builtins.html#index-wait .

Vollständiges, lauffähiges Programm: Warten auf das Ende aller Prozesse

multi_process_program.sh (aus meinem eRCaGuy_hello_world repo):

#!/usr/bin/env bash

# This is a special sleep function which returns the number of seconds slept as
# the "error code" or return code" so that we can easily see that we are in
# fact actually obtaining the return code of each process as it finishes.
my_sleep() {
    seconds_to_sleep="$1"
    sleep "$seconds_to_sleep"
    return "$seconds_to_sleep"
}

# Create an array of whatever commands you want to run as subprocesses
procs=()  # bash array
procs+=("my_sleep 5")
procs+=("my_sleep 2")
procs+=("my_sleep 3")
procs+=("my_sleep 4")

num_procs=${#procs[@]}  # number of processes
echo "num_procs = $num_procs"

# run commands as subprocesses and store pids in an array
pids=()  # bash array
for (( i=0; i<"$num_procs"; i++ )); do
    echo "cmd = ${procs[$i]}"
    ${procs[$i]} &  # run the cmd as a subprocess
    # store pid of last subprocess started; see:
    # https://unix.stackexchange.com/a/30371/114401
    pids+=("$!")
    echo "    pid = ${pids[$i]}"
done

# OPTION 1 (comment this option out if using Option 2 below): wait for all pids
for pid in "${pids[@]}"; do
    wait "$pid"
    return_code="$?"
    echo "PID = $pid; return_code = $return_code"
done
echo "All $num_procs processes have ended."

Ändern Sie die obige Datei so, dass sie ausführbar ist, indem Sie chmod +x multi_process_program.sh und führen Sie es wie folgt aus:

time ./multi_process_program.sh 

Beispielhafte Ausgabe. Sehen Sie, wie die Ausgabe des time Befehl im Aufruf zeigt, dass die Ausführung 5,084 Sekunden dauerte. Wir konnten auch erfolgreich den Rückgabecode von jedem Unterprozess abrufen.

eRCaGuy_hello_world/bash$ time ./multi_process_program.sh 
num_procs = 4
cmd = my_sleep 5
    pid = 21694
cmd = my_sleep 2
    pid = 21695
cmd = my_sleep 3
    pid = 21697
cmd = my_sleep 4
    pid = 21699
PID = 21694; return_code = 5
PID = 21695; return_code = 2
PID = 21697; return_code = 3
PID = 21699; return_code = 4
All 4 processes have ended.
PID 21694 is done; return_code = 5; 3 PIDs remaining.
PID 21695 is done; return_code = 2; 2 PIDs remaining.
PID 21697 is done; return_code = 3; 1 PIDs remaining.
PID 21699 is done; return_code = 4; 0 PIDs remaining.

real    0m5.084s
user    0m0.025s
sys 0m0.061s

Weiter gehen: live bestimmen, wann jeder einzelne Prozess endet

Wenn Sie eine Aktion ausführen möchten, sobald ein Prozess beendet ist, und Sie nicht wissen, wann er beendet sein wird, können Sie eine unendliche Umfrage durchführen while Schleife, um zu sehen, wann jeder Prozess beendet ist, und dann die gewünschte Aktion durchführen.

Kommentieren Sie einfach den obigen Codeblock "OPTION 1" aus und ersetzen Sie ihn stattdessen durch diesen Block "OPTION 2":

# OR OPTION 2 (comment out Option 1 above if using Option 2): poll to detect
# when each process terminates, and print out when each process finishes!
while true; do
    for i in "${!pids[@]}"; do
        pid="${pids[$i]}"
        # echo "pid = $pid"  # debugging

        # See if PID is still running; see my answer here:
        # https://stackoverflow.com/a/71134379/4561887
        ps --pid "$pid" > /dev/null
        if [ "$?" -ne 0 ]; then
            # PID doesn't exist anymore, meaning it terminated

            # 1st, read its return code
            wait "$pid"
            return_code="$?"

            # 2nd, remove this PID from the `pids` array by `unset`ting the
            # element at this index; NB: due to how bash arrays work, this does
            # NOT actually remove this element from the array. Rather, it
            # removes its index from the `"${!pids[@]}"` list of indices,
            # adjusts the array count(`"${#pids[@]}"`) accordingly, and it sets
            # the value at this index to either a null value of some sort, or
            # an empty string (I'm not exactly sure).
            unset "pids[$i]"

            num_pids="${#pids[@]}"
            echo "PID $pid is done; return_code = $return_code;" \
                 "$num_pids PIDs remaining."
        fi
    done

    # exit the while loop if the `pids` array is empty
    if [ "${#pids[@]}" -eq 0 ]; then
        break
    fi

    # Do some small sleep here to keep your polling loop from sucking up
    # 100% of one of your CPUs unnecessarily. Sleeping allows other processes
    # to run during this time.
    sleep 0.1
done

Beispiellauf und Ausgabe des vollständigen Programms mit auskommentierter Option 1 und verwendeter Option 2:

eRCaGuy_hello_world/bash$ ./multi_process_program.sh 
num_procs = 4
cmd = my_sleep 5
    pid = 22275
cmd = my_sleep 2
    pid = 22276
cmd = my_sleep 3
    pid = 22277
cmd = my_sleep 4
    pid = 22280
PID 22276 is done; return_code = 2; 3 PIDs remaining.
PID 22277 is done; return_code = 3; 2 PIDs remaining.
PID 22280 is done; return_code = 4; 1 PIDs remaining.
PID 22275 is done; return_code = 5; 0 PIDs remaining.

Jede dieser PID XXXXX is done Zeilen direkt nach Beendigung des Prozesses live ausgibt! Beachten Sie, dass, obwohl der Prozess für sleep 5 (PID 22275 in diesem Fall) zuerst ausgeführt wurde, wurde er zuletzt beendet, und wir haben jeden Prozess direkt nach seiner Beendigung erfolgreich erkannt. Wir haben auch jeden Rückgabecode erfolgreich erkannt, genau wie bei Option 1.

Andere Referenzen:

  1. *****+ [SEHR HILFREICH] Exit-Code eines Hintergrundprozesses abrufen - diese Antwort lehrte mich das Schlüsselprinzip, dass (Hervorhebung hinzugefügt):

    wait <n> wartet, bis der Prozess mit der PID abgeschlossen ist (es wird blockiert, bis der Prozess abgeschlossen ist, Sie sollten diese Funktion also erst dann aufrufen, wenn Sie sicher sind, dass der Prozess abgeschlossen ist. ), und gibt dann den Exit-Code des abgeschlossenen Prozesses zurück.

    Mit anderen Worten: Es half mir zu wissen, dass auch nach Abschluss des Prozesses können Sie immer noch die wait auf, um den Rückgabecode zu erhalten!

  2. So prüfen Sie, ob eine Prozess-ID (PID) existiert

    1. meine Antwort
  3. Ein Element aus einem Bash-Array entfernen - Beachten Sie, dass Elemente in einem Bash-Array nicht wirklich gelöscht werden, sie werden nur "entschärft". Siehe meine Kommentare im obigen Code, was das bedeutet.

  4. Verwendung der ausführbaren Befehlszeile true um eine unendliche while-Schleife in der Bash zu erzeugen: https://www.cyberciti.biz/faq/bash-infinite-loop/

9voto

Jason Slobotski Punkte 1190

Ich sehe viele gute Beispiele, die hier aufgelistet sind, und wollte auch meins einbringen.

#! /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 bei mir hervorragend. Ich hoffe, das hilft jemandem weiter!

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X