Comment attendre dans un script bash plusieurs sous-processus générés à partir de ce script pour terminer et renvoyer le code de sortie! = 0 lorsque l'un des sous-processus se termine par le code! = 0?
Script simple:
#!/bin/bash
for i in `seq 0 9`; do
doCalculations $i &
done
wait
Le script ci-dessus attendra les 10 sous-processus générés, mais donnera toujours le statut de sortie 0 (voir help wait
). Comment puis-je modifier ce script pour qu'il détecte les statuts de sortie des sous-processus générés et renvoie le code de sortie 1 lorsqu'un des sous-processus se termine par le code! = 0?
Existe-t-il une meilleure solution que de collecter les PID des sous-processus, de les attendre dans l’ordre et de faire la somme des statuts de sortie?
wait
aussi (facultatif) prend l'attribut PID du processus à attendre, et avec $! vous obtenez le PID de la dernière commande lancée en arrière-plan . Modifiez la boucle pour stocker le PID de chaque sous-processus généré dans un tableau, puis effectuez une nouvelle boucle d'attente sur chaque PID.
# run processes and store pids in array
for i in $n_procs; do
./procs[${i}] &
pids[${i}]=$!
done
# wait for all pids
for pid in ${pids[*]}; do
wait $pid
done
http://jeremy.zawodny.com/blog/archives/010717.html :
#!/bin/bash
FAIL=0
echo "starting"
./sleeper 2 0 &
./sleeper 2 1 &
./sleeper 3 0 &
./sleeper 2 0 &
for job in `jobs -p`
do
echo $job
wait $job || let "FAIL+=1"
done
echo $FAIL
if [ "$FAIL" == "0" ];
then
echo "YAY!"
else
echo "FAIL! ($FAIL)"
fi
Si vous avez GNU Parallel installé, vous pouvez faire:
# If doCalculations is a function
export -f doCalculations
seq 0 9 | parallel doCalculations {}
GNU Parallel vous donnera le code de sortie:
0 - Tous les travaux ont été exécutés sans erreur.
1-253 - Certains des travaux ont échoué. Le statut de sortie indique le nombre de tâches ayant échoué.
254 - Plus de 253 emplois ont échoué.
255 - Autre erreur.
Regardez les vidéos d'introduction pour en savoir plus: http://pi.dk/1
Voici ce que j'ai imaginé jusqu'à présent. Je voudrais voir comment interrompre la commande de veille si un enfant se termine, pour ne pas avoir à ajuster WAITALL_DELAY
à son utilisation.
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
Pourquoi pas simplement:
#!/bin/bash
pids=""
for i in `seq 0 9`; do
doCalculations $i &
pids="$pids $!"
done
wait $pids
...code continued here ...
Mettre à jour:
Comme l'ont souligné de nombreux commentateurs, tous les processus ci-dessus attendent d'être terminés avant de continuer, mais ne s'arrêtent pas et échouent si l'un d'entre eux échoue. Cela peut être fait avec la modification suivante suggérée par @Bryan, @SamBrightman :
#!/bin/bash
pids=""
RESULT=0
for i in `seq 0 9`; do
doCalculations $i &
pids="$pids $!"
done
for pid in $pids; do
wait $pid || let "RESULT=1"
done
if [ "$RESULT" == "1" ];
then
exit 1
fi
...code continued here ...
Voici un exemple simple utilisant wait
.
Exécuter des processus:
$ sleep 10 &
$ sleep 10 &
$ sleep 20 &
$ sleep 20 &
Puis attendez-les avec la commande wait
:
$ wait < <(jobs -p)
Ou simplement wait
(sans arguments) pour tous.
Cela attend que tous les travaux en arrière-plan soient terminés.
Si l'option -n
est fournie, attend la fin du travail suivant et renvoie son statut de sortie.
Voir: help wait
et help jobs
pour la syntaxe.
Toutefois, l'inconvénient est que cela ne renvoie que le statut du dernier ID. Vous devez donc vérifier le statut de chaque sous-processus et le stocker dans la variable.
Ou demandez à votre fonction de calcul de créer un fichier en cas d’échec (journal vide ou avec échec), puis vérifiez si ce fichier existe, par exemple.
$ sleep 20 && true || tee fail &
$ sleep 20 && false || tee fail &
$ wait < <(jobs -p)
$ test -f fail && echo Calculation failed.
Pour paralléliser cela ...
for i in $(whatever_list) ; do
do_something $i
done
Traduis-le en ceci ...
for i in $(whatever_list) ; do echo $i ; done | ## execute in parallel...
(
export -f do_something ## export functions (if needed)
export PATH ## export any variables that are required
xargs -I{} --max-procs 0 bash -c ' ## process in batches...
{
echo "processing {}" ## optional
do_something {}
}'
)
--max-procs
en fonction du degré de parallélisme souhaité (0
signifie "tout à la fois").xargs
- mais il n'est pas toujours installé par défaut.for
n'est pas strictement nécessaire dans cet exemple car echo $i
ne fait que régénérer la sortie de $(whatever_list
). Je pense simplement que l’utilisation du mot clé for
facilite la compréhension de ce qui se passe.Voici un exemple de travail simplifié ...
for i in {0..5} ; do echo $i ; done |xargs -I{} --max-procs 2 bash -c '
{
echo sleep {}
sleep 2s
}'
Je ne crois pas que ce soit possible avec la fonctionnalité intégrée de Bash.
Vous pouvez recevoir une notification à la sortie d'un enfant:
#!/bin/sh
set -o monitor # enable script job control
trap 'echo "child died"' CHLD
Cependant, il n'y a aucun moyen apparent d'obtenir le statut de sortie de l'enfant dans le gestionnaire de signaux.
L'obtention de ce statut enfant correspond généralement au travail de la famille wait
dans les API POSIX de niveau inférieur. Malheureusement, le support de Bash pour cela est limité - vous pouvez attendre un processus enfant spécifique à un (et obtenir son statut de sortie) ou attendre tous et obtenir toujours le résultat 0.
Ce qui semble impossible est l'équivalent de waitpid(-1)
, qui bloque jusqu'au retour de tout processus enfant .
Je vois beaucoup de bons exemples énumérés ici, je voulais aussi ajouter le mien.
#! /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
J'utilise quelque chose de très similaire pour démarrer/arrêter les serveurs/services en parallèle et vérifier l'état de chaque sortie. Ça marche bien pour moi. J'espère que cela aide quelqu'un!
Le code suivant attend la fin de tous les calculs et renvoie le statut de sortie 1 si l’un des doCalculations échoue.
#!/bin/bash
for i in $(seq 0 9); do
(doCalculations $i >&2 & wait %1; echo $?) &
done | grep -qv 0 && exit 1
Voici ma version qui fonctionne pour plusieurs pids, enregistre les avertissements si l'exécution prend trop de temps et arrête les sous-processus si l'exécution prend plus de temps qu'une valeur donnée.
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
}
Par exemple, attendez que les trois processus soient terminés, enregistrez un avertissement si l'exécution prend plus de 5 secondes, arrêtez tous les processus si l'exécution prend plus de 120 secondes. Ne quittez pas le programme en cas d'échec.
function something {
sleep 10 &
pids="$!"
sleep 12 &
pids="$pids;$!"
sleep 9 &
pids="$pids;$!"
WaitForTaskCompletion $pids 5 120 ${FUNCNAME[0]} false
}
# Launch the function
someting
Stockez simplement les résultats dans Shell, par exemple. dans un fichier.
#!/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
Si vous disposez de bash 4.2 ou version ultérieure, les éléments suivants pourraient vous être utiles. Il utilise des tableaux associatifs pour stocker les noms de tâches et leur "code", ainsi que les noms de tâches et leurs pids. J'ai également intégré une méthode simple de limitation de débit qui peut s'avérer utile si vos tâches consomment beaucoup de temps processeur ou d'E/S et que vous souhaitez limiter le nombre de tâches simultanées.
Le script lance toutes les tâches dans la première boucle et utilise les résultats dans la seconde.
C'est un peu exagéré pour des cas simples, mais cela permet des choses très soignées. Par exemple, il est possible de stocker des messages d'erreur pour chaque tâche dans un autre tableau associatif et de les imprimer une fois que tout est réglé.
#! /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
Je viens de modifier un script en arrière-plan et de paralléliser un processus.
J'ai expérimenté (sous Solaris, bash et ksh) et découvert qu'attendre renvoie l'état de sortie s'il n'est pas nul ou une liste de travaux renvoyant une sortie non nulle lorsque aucun argument PID n'est fourni. Par exemple.
Frapper:
$ 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
Cette sortie est écrite dans stderr. Une solution simple à l'exemple des PO pourrait donc être:
#!/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
Alors que ce:
wait 2> >(wc -l)
renverra également un compte mais sans le fichier tmp. Cela pourrait aussi être utilisé de cette façon, par exemple:
wait 2> >(if [ `wc -l` -gt 0 ] ; then echo "ERROR"; fi)
Mais ce n’est pas beaucoup plus utile que le fichier tmp IMO. Je ne pouvais pas trouver un moyen utile d'éviter le fichier tmp tout en évitant également d'exécuter "wait" dans un sous-shell, ce qui ne fonctionnerait pas du tout.
#!/bin/bash
set -m
for i in `seq 0 9`; do
doCalculations $i &
done
while fg; do true; done
set -m
vous permet d'utiliser fg & bg dans un scriptfg
, en plus de placer le dernier processus au premier plan, a le même statut de sortie que le processus au premier planwhile fg
arrêtera la boucle quand une fg
quittera avec un statut de sortie différent de zéromalheureusement, cela ne résoudra pas le problème lorsqu'un processus en arrière-plan se termine avec un statut de sortie non nul. (la boucle ne se terminera pas immédiatement. elle attendra la fin des processus précédents.)
Cela fonctionne, devrait être tout aussi bon sinon meilleur que la réponse de @ 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
et bien sûr, j'ai immortalisé ce script dans un projet NPM qui vous permet d'exécuter des commandes bash en parallèle, utile pour les tests:
Je me suis essayé à cela et j'ai combiné toutes les meilleures parties des autres exemples ici. Ce script exécute la fonction checkpids
lorsque un processus d'arrière-plan se termine et génère l'état de sortie sans recourir à la scrutation.
#!/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
Il y a déjà beaucoup de réponses ici, mais je suis surpris que personne ne semble avoir suggéré d'utiliser des tableaux ... Alors voici ce que j'ai fait - cela pourrait être utile à certains à l'avenir.
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
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
Le set -e
en haut arrête votre script en cas d'échec.
expect
renverra 1
si un sous-travail a échoué.
le piège est ton ami. Vous pouvez piéger sur ERR dans beaucoup de systèmes. Vous pouvez intercepter EXIT ou sur DEBUG pour exécuter un morceau de code après chaque commande.
Ceci en plus de tous les signaux standard.
L'interception du signal CHLD peut ne pas fonctionner car vous pourriez perdre certains signaux s'ils arrivaient simultanément.
#!/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 !!!
la solution permettant d’attendre plusieurs sous-processus et de se fermer lorsque l’un d’eux se termine avec un code d’état différent de zéro est en utilisant '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
le code d'état '127' est destiné à un processus inexistant, ce qui signifie que l'enfant peut s'être arrêté.
J'ai utilisé cela récemment (merci à 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
À partir de là, on peut facilement extrapoler et avoir un déclencheur (toucher un fichier, envoyer un signal) et modifier les critères de comptage (nombre de fichiers touchés, etc.) pour répondre à ce déclencheur. Ou si vous voulez juste "n'importe quel" rc non nul, supprimez simplement le verrou de save_status.
J'avais besoin de cela, mais le processus cible n'était pas un enfant du shell actuel, auquel cas wait $PID
ne fonctionne pas. J'ai plutôt trouvé l'alternative suivante:
while [ -e /proc/$PID ]; do sleep 0.1 ; done
Cela repose sur la présence de procfs , qui peut ne pas être disponible (Mac ne le fournit pas par exemple). Donc, pour la portabilité, vous pouvez utiliser ceci à la place:
while ps -p $PID >/dev/null ; do sleep 0.1 ; done
C'est quelque chose que j'utilise:
#wait for jobs
for job in `jobs -p`; do wait ${job}; done
Je pense que le moyen le plus simple d'exécuter des tâches en parallèle et de vérifier le statut consiste à utiliser des fichiers temporaires. Il existe déjà quelques réponses similaires (par exemple Nietzche-jou et mug896).
#!/bin/bash
rm -f fail
for i in `seq 0 9`; do
doCalculations $i || touch fail &
done
wait
! [ -f fail ]
Le code ci-dessus n'est pas thread-safe. Si vous craignez que le code ci-dessus ne s'exécute en même temps, il est préférable d'utiliser un nom de fichier unique, tel que fail. $$. La dernière ligne doit satisfaire à l'exigence: "retourner le code de sortie 1 lorsque l'un des sous-processus se termine par le code! = 0?" J'ai jeté une exigence supplémentaire là-bas pour nettoyer. Il a peut-être été plus clair de l'écrire comme ceci:
#!/bin/bash
trap 'rm -f fail.$$' EXIT
for i in `seq 0 9`; do
doCalculations $i || touch fail.$$ &
done
wait
! [ -f fail.$$ ]
Voici un extrait similaire permettant de collecter les résultats de plusieurs travaux: je crée un répertoire temporaire, récapitule les résultats de toutes les sous-tâches dans un fichier séparé, puis les vide pour révision. Cela ne correspond pas vraiment à la question - je le lance en 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
Il peut y avoir un cas où le processus est terminé avant d'attendre le processus. Si nous déclenchons l'attente d'un processus déjà terminé, cela provoquera une erreur, car pid n'est pas un enfant de ce shell. Pour éviter de tels cas, la fonction suivante peut être utilisée pour déterminer si le processus est complet ou non:
isProcessComplete(){
PID=$1
while [ -e /proc/$PID ]
do
echo "Process: $PID is still running"
sleep 5
done
echo "Process $PID has finished"
}