Come attendere in bash il completamento e la restituzione di diversi sottoprocessi! = 0 quando un sottoprocesso termina con il codice! = 0?


563

Come attendere in uno script bash diversi sottoprocessi generati da quello script per terminare e restituire il codice di uscita! = 0 quando uno dei sottoprocessi termina con il codice! = 0?

Script semplice:

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

Lo script sopra attenderà tutti i 10 sottoprocessi generati, ma fornirà sempre lo stato di uscita 0 (vedere help wait ). Come posso modificare questo script in modo che possa scoprire gli stati di uscita dei sottoprocessi generati e restituire il codice di uscita 1 quando uno dei sottoprocessi termina con il codice! = 0?

Esiste una soluzione migliore per quella che raccogliere i PID dei sottoprocessi, aspettarli in ordine e sommare gli stati di uscita?


1
Questo potrebbe essere significativamente migliorato da toccare wait -n, disponibile in bash moderno per tornare solo quando il comando primo / successivo viene completato.
Charles Duffy,

se stai cercando di provare usando Bash, prova questo: github.com/sstephenson/bats
Alexander Mills il

2
Lo sviluppo attivo di BATS è stato spostato su github.com/bats-core/bats-core
Potherca,

3
@CharlesDuffy wait -nha un piccolo problema: se non rimangono lavori secondari (ovvero condizioni di gara), restituisce uno stato di uscita diverso da zero (errore) che può essere indistinguibile da un processo figlio non riuscito.
drevicko,

5
@CharlesDuffy: hai una visione meravigliosa e offri un servizio enorme a SO condividendolo. Sembra che circa l'80% dei post di SO che ho letto abbia condiviso meravigliosi piccoli diamanti di conoscenza nei commenti che devono provenire da un vasto oceano di esperienza. Grazie molto!
Brett Holman,

Risposte:


521

waitinoltre (facoltativamente) accetta il PID del processo e con $! ottieni il PID dell'ultimo comando lanciato in background. Modificare il ciclo per archiviare il PID di ciascun processo secondario generato in un array, quindi ripetere il ciclo in attesa di ciascun 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, dal momento che stai aspettando tutti i processi, non importa se, ad esempio, stai aspettando il primo mentre il secondo è già finito (il secondo verrà comunque selezionato alla successiva iterazione). È lo stesso approccio che useresti in C con wait (2).
Luca Tettamanti,

7
Ah, capisco - interpretazione diversa :) Ho letto la domanda come "restituisci immediatamente il codice di uscita 1 quando esce uno dei sottoprocessi".
Alnitak,

56
Il PID può essere effettivamente riutilizzato, ma non è possibile attendere un processo che non sia figlio del processo corrente (in questo caso l'attesa non riesce).
tkokoszka,

12
È inoltre possibile utilizzare% n per fare riferimento al lavoro in background n: th e %% per fare riferimento a quello più recente.
Conny,

30
@Nils_M: hai ragione, mi dispiace. Quindi sarebbe qualcosa del tipo: for i in $n_procs; do ./procs[${i}] & ; pids[${i}]=$!; done; for pid in ${pids[*]}; do wait $pid; done;giusto?
synack,

285

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

104
jobs -psta fornendo PID di sottoprocessi che si trovano nello stato di esecuzione. Salterà un processo se il processo termina prima che jobs -pvenga chiamato. Pertanto, se uno dei sottoprocessi termina prima jobs -p, lo stato di uscita di quel processo andrà perso.
tkokoszka,

15
Caspita, questa risposta è decisamente migliore di quella più votata. : /
e40

4
@ e40 e la risposta di seguito è probabilmente ancora migliore. E ancora meglio sarebbe probabilmente eseguire ogni comando con '(cmd; echo "$?" >> "$ tmpfile"), usare questa attesa e quindi leggere il file per gli errori. Anche annotate-output. ... o semplicemente usa questo script quando non ti interessa così tanto.
HoverHell,

Vorrei aggiungere che questa risposta è meglio di quella accettata
shurikk,

2
Per essere precisi, @tkokoszka jobs -pnon sta fornendo PID di sottoprocessi, ma piuttosto GPID . La logica di attesa sembra funzionare comunque, attende sempre sul gruppo se esiste tale gruppo e pid in caso contrario, ma è bene essere consapevoli .. specialmente se si dovesse costruire su questo e incorporare qualcosa come inviare messaggi al sottoprocesso in cui se la sintassi è diversa a seconda che tu abbia PID o GPID. cioè kill -- -$GPIDvskill $PID
Timo

59

Ecco un semplice esempio di utilizzo wait .

Esegui alcuni processi:

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

Quindi aspettali con il waitcomando:

$ wait < <(jobs -p)

O semplicemente wait(senza argomenti) per tutti.

Questo attenderà il completamento di tutti i lavori in background.

Se -nviene fornita l' opzione, attende la fine del lavoro successivo e restituisce lo stato di uscita.

Vedi: help waite help jobsper la sintassi.

Tuttavia, il rovescio della medaglia è che questo restituirà solo lo stato dell'ultimo ID, quindi è necessario controllare lo stato per ciascun sottoprocesso e memorizzarlo nella variabile.

Oppure fai la tua funzione di calcolo per creare un file in caso di errore (vuoto o con registro errori), quindi controlla se esiste quel file, ad es

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

2
Per i nuovi a bash, i due calcoli nell'esempio qui sono sleep 20 && truee sleep 20 && false- cioè: sostituisci quelli con le tue funzioni. Per capire &&e ||, esegui man bashe digita '/' (ricerca) quindi '^ * Elenchi' (una regex) quindi inserisci: man scorrerà verso il basso fino alla descrizione di &&e||
drevicko,

1
dovresti probabilmente verificare che il file 'fail' non esista all'inizio (o eliminarlo). A seconda dell'applicazione, potrebbe anche essere una buona idea aggiungere '2> & 1' prima ||di catturare anche STDERR in errore.
drevicko,

mi piace questo, qualche inconveniente? in realtà, solo quando voglio elencare tutti i sottoprocessi e intraprendere alcune azioni, ad es. invia un segnale, che proverò a fare la contabilità di pidi o a ripetere lavori. Aspetta il traguardo, solowait
xgwang

In questo modo mancherà lo stato di uscita del lavoro non riuscito prima che venga chiamato il comando -p
Erik Aronesty il

50

Se hai GNU Parallel installato puoi fare:

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

GNU Parallel ti darà il codice di uscita:

  • 0: tutti i lavori sono stati eseguiti senza errori.

  • 1-253 - Alcuni lavori non sono riusciti. Lo stato di uscita indica il numero di lavori non riusciti

  • 254 - Più di 253 lavori falliti.

  • 255 - Altro errore.

Guarda i video introduttivi per saperne di più: http://pi.dk/1


1
Grazie! Ma hai dimenticato di menzionare il problema della "confusione" in cui successivamente mi sono imbattuto
nobar

1
Sembra un ottimo strumento, ma non credo che quanto sopra funzioni così com'è in uno script Bash in cui doCalculationsè definita una funzione in quello stesso script (anche se l'OP non era chiaro su questo requisito). Quando provo, paralleldice /bin/bash: doCalculations: command not found(lo dice 10 volte per l' seq 0 9esempio sopra). Vedi qui per una soluzione alternativa.
nobar,

3
Anche di interesse: xargsha alcune capacità di avviare lavori in parallelo tramite l' -Popzione. Da qui : export -f doCalculations ; seq 0 9 |xargs -P 0 -n 1 -I{} bash -c "doCalculations {}". I limiti di xargssono elencati nella pagina man di parallel.
nobar,

E se doCalculationssi basa su altre variabili di ambiente interne allo script (personalizzate PATH, ecc.), Probabilmente devono essere esplicitamente modificate exportprima del lancio parallel.
nobar,

4
@nobar La confusione è dovuta ad alcuni pacchettizzatori che fanno casino per i loro utenti. Se installi usando wget -O - pi.dk/3 | shnon otterrai confusioni. Se il tuo packager ti ha incasinato le cose, ti incoraggio a sollevare il problema con il tuo packager. Le variabili e le funzioni dovrebbero essere esportate (export -f) per GNU Parallel per vederle (vedi man parallel: gnu.org/software/parallel/… )
Ole Tange,

46

Che ne dici semplicemente:

#!/bin/bash

pids=""

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

wait $pids

...code continued here ...

Aggiornare:

Come indicato da più commentatori, quanto sopra attende il completamento di tutti i processi prima di continuare, ma non si interrompe e fallisce se uno di essi fallisce, si può fare con la seguente modifica suggerita da @Bryan, @SamBrightman e altri :

#!/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
Secondo le pagine man di wait, wait con più PID restituisce solo il valore di ritorno dell'ultimo processo atteso. Quindi è necessario un loop aggiuntivo e attendere ciascun PID separatamente, come suggerito nella risposta accettata (nei commenti).
Vlad Frolov,

1
Poiché non sembra essere indicato altrove in questa pagina, aggiungerò che il ciclo sarebbefor pid in $pids; do wait $pid; done
Bryan

1
@bisounours_tronconneuse sì, sì. Vedi help wait- con più ID waitrestituisce solo il codice di uscita dell'ultimo, come detto sopra @ vlad-frolov.
Sam Brightman,

1
Bryan, @SamBrightman Ok. L'ho modificato con le tue raccomandazioni.
patapouf_ai,

4
Avevo un'ovvia preoccupazione per questa soluzione: e se un determinato processo dovesse uscire prima che waitvenga chiamato il corrispondente ? Si scopre che questo non è un problema: se sei waitsu un processo che è già uscito, uscirai waitimmediatamente con lo stato del processo già uscito. (Grazie, bashautori!)
Daniel Griscom,

39

Ecco cosa ho escogitato finora. Vorrei vedere come interrompere il comando sleep se un bambino termina, in modo che non si debba sintonizzarsi WAITALL_DELAYsul proprio uso.

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

Si potrebbe probabilmente saltare quel WAITALL_DELAY o impostarlo su un valore molto basso, dal momento che nessun processo viene avviato all'interno del ciclo, non penso che sia troppo costoso.
Marian,

21

Per parallelizzare questo ...

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

Traduci in questo ...

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 {}
      }' 
   )
  • Se si verifica un errore in un processo, non interromperà gli altri processi, ma si tradurrà in un codice di uscita diverso da zero dalla sequenza nel suo insieme .
  • L'esportazione di funzioni e variabili può essere o non essere necessaria, in ogni caso particolare.
  • Puoi impostare in --max-procsbase a quanto parallelismo vuoi ( 0significa "tutto in una volta").
  • GNU Parallel offre alcune funzionalità aggiuntive se usato al posto di xargs- ma non è sempre installato di default.
  • Il forloop non è strettamente necessario in questo esempio poiché echo $ifondamentalmente sta solo rigenerando l'output di $(whatever_list). Penso solo che l'uso della forparola chiave renda un po 'più facile vedere cosa sta succedendo.
  • La gestione delle stringhe di Bash può essere fonte di confusione: ho scoperto che l'uso di virgolette singole funziona meglio per avvolgere script non banali.
  • Puoi facilmente interrompere l'intera operazione (usando ^ C o simile), a differenza dell'approccio più diretto al parallelismo di Bash .

Ecco un esempio di lavoro semplificato ...

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


7

Non credo sia possibile con la funzionalità integrata di Bash.

È possibile ottenere una notifica quando un bambino esce:

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

Tuttavia, non esiste un modo apparente per ottenere lo stato di uscita del bambino nel gestore del segnale.

Ottenere lo stato figlio di solito è il lavoro della waitfamiglia di funzioni nelle API POSIX di livello inferiore. Sfortunatamente il supporto di Bash per questo è limitato: puoi aspettare un processo figlio specifico (e ottenere il suo stato di uscita) o puoi aspettare tutto e ottenere sempre un risultato 0.

Ciò che sembra impossibile da fare è l'equivalente di waitpid(-1), che si blocca fino a quando non ritorna alcun processo figlio.


7

Vedo un sacco di buoni esempi elencati qui, ho voluto buttare anche il mio.

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

Uso qualcosa di molto simile per avviare / arrestare server / servizi in parallelo e controllare ogni stato di uscita. Funziona benissimo per me. Spero che questo aiuti qualcuno!


Quando lo interrompo con Ctrl + CI vedo ancora i processi in esecuzione in background.
Karsten,

2
@karsten - questo è un problema diverso. Supponendo che tu stia usando bash puoi intrappolare una condizione di uscita (incluso Ctrl + C) e far uccidere i processi correnti e tutti i figli usandotrap "kill 0" EXIT
Phil

@Phil è corretto. Poiché si tratta di processi in background, l'uccisione del processo genitore lascia semplicemente in esecuzione tutti i processi figlio. Il mio esempio non intrappola alcun segnale, che può essere aggiunto se necessario, come ha affermato Phil.
Jason Slobotski l'

6

Questo è qualcosa che uso:

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

5

Il codice seguente attenderà il completamento di tutti i calcoli e restituirà lo stato di uscita 1 se uno dei doCalculation non riesce.

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

5

Basta archiviare i risultati fuori dalla shell, ad esempio in un file.

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

Ecco la mia versione che funziona per più pid, registra avvisi se l'esecuzione richiede troppo tempo e arresta i sottoprocessi se l'esecuzione richiede più tempo di un determinato valore.

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
}

Esempio, attendere il completamento di tutti e tre i processi, registrare un avviso se l'esecuzione richiede il logger di 5 secondi, arrestare tutti i processi se l'esecuzione richiede più di 120 secondi. Non uscire dal programma in caso di errori.

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

Se hai bash 4.2 o successivo disponibile, ti potrebbe essere utile quanto segue. Utilizza matrici associative per memorizzare i nomi delle attività e il loro "codice", nonché i nomi delle attività e i loro pid. Ho anche incorporato un semplice metodo di limitazione della velocità che potrebbe tornare utile se le attività richiedono molto tempo di CPU o I / O e si desidera limitare il numero di attività simultanee.

Lo script avvia tutte le attività nel primo ciclo e consuma i risultati nel secondo.

Questo è un po 'eccessivo per casi semplici, ma consente cose piuttosto ordinate. Ad esempio, è possibile archiviare i messaggi di errore per ciascuna attività in un altro array associativo e stamparli dopo che tutto si è stabilizzato.

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

Ho appena modificato uno script in background e parallelizzato un processo.

Ho fatto alcuni esperimenti (su Solaris sia con bash che con ksh) e ho scoperto che 'wait' genera lo stato di uscita se non è zero, o un elenco di lavori che restituiscono un'uscita diversa da zero quando non viene fornito alcun argomento PID. Per esempio

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

Questo output è scritto su stderr, quindi una semplice soluzione all'esempio dei PO potrebbe essere:

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

Mentre questo:

wait 2> >(wc -l)

restituirà anche un conteggio ma senza il file tmp. Questo potrebbe anche essere usato in questo modo, ad esempio:

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

Ma questo non è molto più utile del file tmp IMO. Non sono riuscito a trovare un modo utile per evitare il file tmp evitando anche di eseguire "wait" in una subshell, che non funzionerà affatto.


3

Ci ho provato e ho combinato tutte le parti migliori degli altri esempi qui. Questo script eseguirà la checkpidsfunzione all'uscita da qualsiasi processo in background e genererà lo stato di uscita senza ricorrere al polling.

#!/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 ti permette di usare fg & bg in uno script
  • fg, oltre a mettere in primo piano l'ultimo processo, ha lo stesso stato di uscita del processo in primo piano
  • while fginterromperà il ciclo quando qualsiasi fguscita con uno stato di uscita diverso da zero

sfortunatamente questo non gestirà il caso quando un processo in background esce con uno stato di uscita diverso da zero. (il ciclo non terminerà immediatamente. Aspetterà il completamento dei processi precedenti.)


3

Ci sono già molte risposte qui, ma sono sorpreso che nessuno sembra aver suggerito di usare array ... Quindi, ecco cosa ho fatto - questo potrebbe essere utile per alcuni in futuro.

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

Funziona, dovrebbe essere altrettanto buono se non migliore della risposta di @ 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

e, naturalmente, ho immortalato questo script, in un progetto NPM che ti permette di eseguire comandi bash in parallelo, utili per testare:

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


trap $? $$sembra impostare il codice di uscita su 0 e PID sulla shell bash attualmente in esecuzione, ogni volta per me
inetknght

ne sei assolutamente sicuro? Non sono sicuro che abbia senso.
Alexander Mills,

2

la trappola è tua amica. Puoi intercettare ERR in molti sistemi. Puoi intercettare EXIT o su DEBUG per eseguire un pezzo di codice dopo ogni comando.

Questo in aggiunta a tutti i segnali standard.


1
Per favore, puoi elaborare la tua risposta con alcuni esempi.
Ϲοδε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

Nella parte set -esuperiore, il tuo script si interrompe in caso di errore.

expectritornerà 1se qualche subjob fallito.


2

Proprio per questo scopo ho scritto una bashfunzione chiamata :for.

Nota : :fornon solo conserva e restituisce il codice di uscita della funzione non funzionante, ma termina anche tutte le istanze in esecuzione parallele. Che potrebbe non essere necessario in questo caso.

#!/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 $?
}

uso

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

Riferimenti


1

L'ho usato di recente (grazie ad 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

Da lì si può facilmente estrapolare e avere un trigger (toccare un file, inviare un segnale) e modificare i criteri di conteggio (contare i file toccati o qualsiasi altra cosa) per rispondere a quel trigger. O se vuoi semplicemente "qualsiasi" diverso da zero rc, basta uccidere il blocco da save_status.


1

Ne avevo bisogno, ma il processo di destinazione non era figlio dell'attuale shell, nel qual caso wait $PIDnon funziona. Ho trovato invece la seguente alternativa:

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

Ciò si basa sulla presenza di procfs , che potrebbero non essere disponibili (ad esempio, Mac non lo fornisce). Quindi per la portabilità, è possibile utilizzare questo invece:

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

1

Il segnale CHLD trapping potrebbe non funzionare perché è possibile perdere alcuni segnali se sono arrivati ​​contemporaneamente.

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

soluzione per attendere diversi sottoprocessi e uscire quando uno di essi esce con un codice di stato diverso da zero usando "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

il codice di stato "127" indica un processo inesistente, il che significa che il figlio potrebbe essere uscito.


1

Attendere tutti i lavori e restituire il codice di uscita dell'ultimo lavoro non riuscito. A differenza delle soluzioni precedenti, ciò non richiede il salvataggio dei pid. Basta andare via e aspettare.

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
}

Ciò funzionerà e fornirà in modo affidabile il primo codice di errore dai comandi eseguiti, a meno che non sia "comando non trovato" (codice 127).
drevicko

0

Può esserci un caso in cui il processo è completo prima di attendere il processo. Se attiviamo l'attesa per un processo che è già terminato, verrà generato un errore come pid non è figlio di questa shell. Per evitare tali casi, è possibile utilizzare la seguente funzione per determinare se il processo è completo o meno:

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

0

Penso che il modo più semplice per eseguire lavori in parallelo e controllare lo stato sia usare file temporanei. Esistono già un paio di risposte simili (ad esempio Nietzche-jou e mug896).

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

Il codice sopra non è thread-safe. Se sei preoccupato che il codice sopra sarà in esecuzione contemporaneamente a se stesso, è meglio usare un nome file univoco, come fail. $$. L'ultima riga deve soddisfare il requisito: "restituire il codice di uscita 1 quando uno dei sottoprocessi termina con il codice! = 0?" Ho gettato un requisito in più per ripulire. Potrebbe essere stato più chiaro scriverlo in questo modo:

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

Ecco uno snippet simile per la raccolta dei risultati da più lavori: creo una directory temporanea, racconto gli output di tutte le attività secondarie in un file separato, quindi li scarico per la revisione. Questo non corrisponde alla domanda: la sto inserendo come bonus:

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

Sono quasi caduto nella trappola dell'utilizzo jobs -pper raccogliere i PID, che non funziona se il bambino è già uscito, come mostrato nello script seguente. La soluzione che ho scelto era semplicemente chiamare wait -nN volte, dove N è il numero di bambini che ho, che per caso conosco in modo deterministico.

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

Produzione:

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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.