Raccogli i codici di uscita dei processi in background paralleli (sotto shell)


18

Supponiamo che abbiamo uno script bash in questo modo:

echo "x" &
echo "y" &
echo "z" &
.....
echo "Z" &
wait

c'è un modo per raccogliere i codici di uscita delle sotto shell / sottoprocessi? Alla ricerca di un modo per farlo e non riesco a trovare nulla. Devo eseguire queste sottotitoli in parallelo, altrimenti sì, sarebbe più facile.

Sto cercando una soluzione generica (ho un numero sconosciuto / dinamico di sottoprocessi da eseguire in parallelo).


1
Ti suggerirò di capire cosa vuoi e poi fare una nuova domanda, cercando di chiarire esattamente il comportamento che stai cercando (forse con pseudocodice o un esempio più ampio).
Michael Homer,

3
In realtà penso che la domanda sia buona ora: ho un numero dinamico di sottoprocessi. Devo raccogliere tutti i codici di uscita. È tutto.
Alexander Mills,

Risposte:


6

La risposta di Alexander Mills che utilizza handleJobs mi ha dato un ottimo punto di partenza, ma mi ha anche dato questo errore

attenzione: run_pending_traps: valore errato in trap_list [17]: 0x461010

Che potrebbe essere un problema di condizioni di gara bash

Invece ho semplicemente archiviato il pid di ogni bambino e ho aspettato e ho ottenuto il codice di uscita per ogni bambino in particolare. Trovo questo più pulito in termini di sottoprocessi che generano sottoprocessi nelle funzioni ed evitano il rischio di aspettare un processo genitore in cui intendevo aspettare il figlio. È più chiaro cosa succede perché non usa la trappola.

#!/usr/bin/env bash

# it seems it does not work well if using echo for function return value, and calling inside $() (is a subprocess spawned?) 
function wait_and_get_exit_codes() {
    children=("$@")
    EXIT_CODE=0
    for job in "${children[@]}"; do
       echo "PID => ${job}"
       CODE=0;
       wait ${job} || CODE=$?
       if [[ "${CODE}" != "0" ]]; then
           echo "At least one test failed with exit code => ${CODE}" ;
           EXIT_CODE=1;
       fi
   done
}

DIRN=$(dirname "$0");

commands=(
    "{ echo 'a'; exit 1; }"
    "{ echo 'b'; exit 0; }"
    "{ echo 'c'; exit 2; }"
    )

clen=`expr "${#commands[@]}" - 1` # get length of commands - 1

children_pids=()
for i in `seq 0 "$clen"`; do
    (echo "${commands[$i]}" | bash) &   # run the command via bash in subshell
    children_pids+=("$!")
    echo "$i ith command has been issued as a background job"
done
# wait; # wait for all subshells to finish - its still valid to wait for all jobs to finish, before processing any exit-codes if we wanted to
#EXIT_CODE=0;  # exit code of overall script
wait_and_get_exit_codes "${children_pids[@]}"

echo "EXIT_CODE => $EXIT_CODE"
exit "$EXIT_CODE"
# end

bello, penso che for job in "${childen[@]}"; dodovrebbe essere for job in "${1}"; do, per chiarezza
Alexander Mills

l'unica preoccupazione che ho con questo script, è se children_pids+=("$!")sta effettivamente catturando il pid desiderato per la sub shell.
Alexander Mills,

1
Ho provato con "$ {1}" e non funziona. Sto passando un array alla funzione e, a quanto pare, ha bisogno di un'attenzione speciale in bash. $! è il pid dell'ultimo lavoro generato, vedi tldp.org/LDP/abs/html/internalvariables.html Sembra funzionare correttamente nei miei test, e ora sto usando lo script cache_dirs in unRAID, e sembra farlo il suo lavoro. Sto usando bash 4.4.12.
Arberg,

bello sì, sembra che tu abbia ragione
Alexander Mills,

20

Utilizzare waitcon un PID, che :

Attendere fino a quando il processo figlio specificato da ogni pid ID processo o specifica processo jobpec si chiude e restituisce lo stato di uscita dell'ultimo comando atteso.

Dovrai salvare il PID di ogni processo mentre procedi:

echo "x" & X=$!
echo "y" & Y=$!
echo "z" & Z=$!

Puoi anche abilitare il controllo del lavoro nello script con set -me usare %njobspec, ma quasi sicuramente non lo vuoi - il controllo del lavoro ha molti altri effetti collaterali .

waitrestituirà lo stesso codice del processo terminato con. È possibile utilizzare wait $Xin qualsiasi momento (ragionevole) in un secondo momento per accedere al codice finale come $?o semplicemente utilizzarlo come vero / falso:

echo "x" & X=$!
echo "y" & Y=$!
...
wait $X
echo "job X returned $?"

wait verrà messo in pausa fino al completamento del comando se non lo ha già fatto.

Se si desidera evitare lo stallo in questo modo, è possibile impostare un traponSIGCHLD , contare il numero di terminazioni e gestire tuttowait s contemporaneamente quando hanno finito tutte. Probabilmente puoi cavartela usando waitda solo quasi sempre.


1
uh, scusa, ho bisogno di eseguire queste sottigliezze in parallelo, lo specificherò nella domanda ...
Alexander Mills,

non importa, forse questo funziona con la mia configurazione ... dove entra in gioco il comando wait nel tuo codice? Non seguo
Alexander Mills, il

1
@AlexanderMills Essi sono in esecuzione in parallelo. Se ne hai un numero variabile, usa un array. (come ad esempio qui che può quindi essere un duplicato).
Michael Homer,

sì, lo verificherò, se il comando wait riguarda la tua risposta, quindi per favore aggiungilo
Alexander Mills,

Corri wait $Xin qualsiasi (ragionevole) punto successivo.
Michael Homer,

5

Se hai avuto un buon modo per identificare i comandi, puoi stampare il loro codice di uscita in un file tmp e quindi accedere al file specifico che ti interessa:

#!/bin/bash

for i in `seq 1 5`; do
    ( sleep $i ; echo $? > /tmp/cmd__${i} ) &
done

wait

for i in `seq 1 5`; do # or even /tmp/cmd__*
    echo "process $i:"
    cat /tmp/cmd__${i}
done

Non dimenticare di rimuovere i file tmp.


4

Usa a compound command- metti la frase tra parentesi:

( echo "x" ; echo X: $? ) &
( true ; echo TRUE: $? ) &
( false ; echo FALSE: $? ) &

darà l'output

x
X: 0
TRUE: 0
FALSE: 1

Un modo davvero diverso di eseguire diversi comandi in parallelo è usando GNU Parallel . Crea un elenco di comandi da eseguire e inseriscili nel file list:

cat > list
sleep 2 ; exit 7
sleep 3 ; exit 55
^D

Esegui tutti i comandi in parallelo e raccogli i codici di uscita nel file job.log:

cat list | parallel -j0 --joblog job.log
cat job.log

e l'output è:

Seq     Host    Starttime       JobRuntime      Send    Receive Exitval Signal  Command
1       :       1486892487.325       1.976      0       0       7       0       sleep 2 ; exit 7
2       :       1486892487.326       3.003      0       0       55      0       sleep 3 ; exit 55

ok grazie, c'è un modo per generarlo? Non ho solo 3 processi secondari, ho processi secondari Z.
Alexander Mills,

Ho aggiornato la domanda originale per riflettere che sto cercando una soluzione generica, grazie
Alexander Mills,

Un modo per generarlo potrebbe essere quello di utilizzare un costrutto loop?
Alexander Mills,

Looping? Hai un elenco fisso di comandi o è controllato dall'utente? Non sono sicuro di capire cosa stai cercando di fare, ma forse PIPESTATUSè qualcosa che dovresti dare un'occhiata. Questo seq 10 | gzip -c > seq.gz ; echo ${PIPESTATUS[@]}ritorna 0 0(codice di uscita dal primo e ultimo comando).
hschou

Sì, essenzialmente controllato dall'utente
Alexander Mills

2

questo è lo script generico che stai cercando. L'unico aspetto negativo è che i tuoi comandi sono tra virgolette, il che significa che l'evidenziazione della sintassi tramite il tuo IDE non funzionerà davvero. Altrimenti, ho provato un paio di altre risposte e questa è la migliore. Questa risposta incorpora l'idea di usare wait <pid>data da @Michael ma fa un passo ulteriore usando il trapcomando che sembra funzionare meglio.

#!/usr/bin/env bash

set -m # allow for job control
EXIT_CODE=0;  # exit code of overall script

function handleJobs() {
     for job in `jobs -p`; do
         echo "PID => ${job}"
         CODE=0;
         wait ${job} || CODE=$?
         if [[ "${CODE}" != "0" ]]; then
         echo "At least one test failed with exit code => ${CODE}" ;
         EXIT_CODE=1;
         fi
     done
}

trap 'handleJobs' CHLD  # trap command is the key part
DIRN=$(dirname "$0");

commands=(
    "{ echo 'a'; exit 1; }"
    "{ echo 'b'; exit 0; }"
    "{ echo 'c'; exit 2; }"
)

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; # wait for all subshells to finish

echo "EXIT_CODE => $EXIT_CODE"
exit "$EXIT_CODE"
# end

grazie a @michael homer per avermi portato sulla strada giusta, ma usare il trapcomando è l'approccio migliore AFAICT.


1
Puoi anche usare la trap SIGCHLD per elaborare i bambini quando escono, come stampare lo stato in quel momento. O aggiornare un contatore di avanzamento: dichiarare una funzione quindi utilizzare "trap nome_funzione CHLD" sebbene ciò possa richiedere anche un'opzione da attivare in una shell non interattiva, come ad esempio "set -m"
Chunko,

1
Inoltre "wait -n" attenderà qualsiasi figlio e quindi restituirà lo stato di uscita di quel figlio in $? variabile. In questo modo puoi stampare l'avanzamento all'uscita di ognuno. Tuttavia nota che, a meno che tu non usi la trappola CHLD, potresti perdere alcune uscite figlio in quel modo.
Chunko,

@Chunko grazie! sono buone informazioni, potresti forse aggiornare la risposta con qualcosa che ritieni sia il migliore?
Alexander Mills,

grazie @Chunko, trap funziona meglio, hai ragione. Con wait <pid>, sono arrivato al fallimento.
Alexander Mills,

Puoi spiegare come e perché credi che la versione con la trappola sia migliore di quella senza di essa? (Credo che non sia meglio, e quindi che sia peggio, perché è più complesso senza alcun beneficio.)
Scott

1

Un'altra variante della risposta di @rolf:

Un altro modo per salvare lo stato di uscita sarebbe qualcosa di simile

mkdir /tmp/status_dir

e quindi avere ogni script

script_name="${0##*/}"  ## strip path from script name
tmpfile="/tmp/status_dir/${script_name}.$$"
do something
rc=$?
echo "$rc" > "$tmpfile"

Questo ti dà un nome univoco per ogni file di stato incluso il nome dello script che lo ha creato e il suo ID di processo (nel caso in cui sia in esecuzione più di un'istanza dello stesso script) che puoi salvare per riferimento in seguito e inserirli tutti nella stesso posto in modo da poter eliminare l'intera sottodirectory al termine.

Puoi anche salvare più di uno stato da ogni script facendo qualcosa del genere

tmpfile="$(/bin/mktemp -q "/tmp/status_dir/${script_name}.$$.XXXXXX")"

che crea il file come prima, ma aggiunge una stringa casuale univoca.

In alternativa, puoi semplicemente aggiungere più informazioni sullo stato allo stesso file.


1

script3verrà eseguito solo se script1e script2hanno successo e script1e script2sarà eseguito in parallelo:

./script1 &
process1=$!

./script2 &
process2=$!

wait $process1
rc1=$?

wait $process2
rc2=$?

if [[ $rc1 -eq 0 ]] && [[ $rc2 -eq 0  ]];then
./script3
fi

AFAICT, questo non è altro che un ripasso della risposta di Michael Homer .
Scott,
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.