Recueillir les codes de sortie des processus d'arrière-plan parallèles (sous-coques)


18

Disons que nous avons un script bash comme ceci:

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

existe-t-il un moyen de collecter les codes de sortie des sous-shells / sous-processus? Vous cherchez un moyen de le faire et ne trouvez rien. J'ai besoin d'exécuter ces sous-coquilles en parallèle, sinon ce serait plus facile.

Je recherche une solution générique (j'ai un nombre inconnu / dynamique de sous-processus à exécuter en parallèle).


1
Je vais vous suggérer de comprendre ce que vous voulez, puis poser une nouvelle question, en essayant d'être clair sur le comportement que vous recherchez (peut-être avec un pseudocode ou un exemple plus large).
Michael Homer

3
En fait, je pense que la question est bonne maintenant - j'ai un nombre dynamique de sous-processus. J'ai besoin de collecter tous les codes de sortie. C'est tout.
Alexander Mills

Réponses:


6

La réponse d'Alexander Mills qui utilise handleJobs m'a donné un excellent point de départ, mais m'a également donné cette erreur

avertissement: run_pending_traps: mauvaise valeur dans trap_list [17]: 0x461010

Ce qui peut être un problème de condition de course bash

Au lieu de cela, je viens de stocker le pid de chaque enfant et d'attendre et d'obtenir le code de sortie pour chaque enfant spécifiquement. Je trouve cela plus propre en termes de sous-processus engendrant des sous-processus dans les fonctions et évitant le risque d'attendre un processus parent où je voulais attendre l'enfant. C'est plus clair ce qui se passe parce qu'il n'utilise pas le piège.

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

cool, je pense que ce for job in "${childen[@]}"; dodevrait être le cas for job in "${1}"; do, pour plus de clarté
Alexander Mills

la seule préoccupation que j'ai avec ce script, c'est si children_pids+=("$!")est en train de capturer le pid souhaité pour le sous-shell.
Alexander Mills

1
J'ai testé avec "$ {1}" et cela ne fonctionne pas. Je passe un tableau à la fonction, et apparemment, cela nécessite une attention particulière dans bash. $! est le pid du dernier travail généré, voir tldp.org/LDP/abs/html/internalvariables.html Il semble fonctionner correctement dans mes tests, et j'utilise maintenant le script in unRAID cache_dirs, et il semble faire son travail. J'utilise bash 4.4.12.
arberg

gentil yep semble avoir raison
Alexander Mills

20

À utiliser waitavec un PID qui :

Attendez la fin du processus enfant spécifié par chaque identifiant de processus pid ou spécification de travail jobspec et renvoyez l'état de sortie de la dernière commande attendue.

Vous devrez enregistrer le PID de chaque processus au fur et à mesure:

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

Vous pouvez également activer le contrôle des travaux dans le script avec set -met utiliser une %nspécification de travail, mais vous ne le souhaitez certainement pas - le contrôle des travaux a beaucoup d'autres effets secondaires .

waitrenverra le même code que le processus terminé. Vous pouvez utiliser wait $Xà tout moment (raisonnable) ultérieur pour accéder au code final sous $?ou simplement l'utiliser comme vrai / faux:

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

wait s'arrêtera jusqu'à ce que la commande se termine si ce n'est pas déjà fait.

Si vous voulez éviter de caler comme ça, vous pouvez définir un trapsurSIGCHLD , compter le nombre de terminaisons et gérer toutes les waits à la fois quand ils ont fini. Vous pouvez probablement vous en sortir waitseul presque tout le temps.


1
ughh, désolé, je dois exécuter ces sous-coquilles en parallèle, je préciserai que dans la question ...
Alexander Mills

peu importe, peut-être que cela fonctionne avec ma configuration ... où la commande d'attente entre-t-elle en jeu dans votre code? Je ne suis pas
Alexander Mills

1
@AlexanderMills Ils sont en cours d' exécution en parallèle. Si vous en avez un nombre variable, utilisez un tableau. (comme par exemple ici qui peut donc être un doublon).
Michael Homer, le

oui merci je vais vérifier cela, si la commande d'attente se rapporte à votre réponse, alors veuillez l'ajouter
Alexander Mills

Vous exécutez wait $Xà tout moment (raisonnable) ultérieur.
Michael Homer

5

Si vous aviez un bon moyen d'identifier les commandes, vous pouvez imprimer leur code de sortie dans un fichier tmp puis accéder au fichier spécifique qui vous intéresse:

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

N'oubliez pas de supprimer les fichiers tmp.


4

Utilisez a compound command- mettez l'instruction entre parenthèses:

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

donnera la sortie

x
X: 0
TRUE: 0
FALSE: 1

Une manière vraiment différente d'exécuter plusieurs commandes en parallèle est d'utiliser GNU Parallel . Faites une liste des commandes à exécuter et mettez-les dans le fichier list:

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

Exécutez toutes les commandes en parallèle et collectez les codes de sortie dans le fichier job.log:

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

et la sortie est:

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 merci, existe-t-il un moyen de générer cela? Je n'ai pas seulement 3 sous-processus, j'ai Z sous-processus.
Alexander Mills

J'ai mis à jour la question d'origine pour refléter que je cherche une solution générique, merci
Alexander Mills

Une façon de le générer pourrait être d'utiliser une construction en boucle?
Alexander Mills

Boucle? Avez-vous une liste fixe de commandes ou est-ce contrôlé par l'utilisateur? Je ne suis pas sûr de comprendre ce que vous essayez de faire, mais c'est peut PIPESTATUS- être quelque chose que vous devriez vérifier. Cela seq 10 | gzip -c > seq.gz ; echo ${PIPESTATUS[@]}renvoie 0 0(code de sortie de la première et de la dernière commande).
hschou

Ouais essentiellement contrôlé par l'utilisateur
Alexander Mills

2

c'est le script générique que vous recherchez. Le seul inconvénient est que vos commandes sont entre guillemets, ce qui signifie que la coloration syntaxique via votre IDE ne fonctionnera pas vraiment. Sinon, j'ai essayé quelques autres réponses et c'est la meilleure. Cette réponse intègre l'idée d'utiliser wait <pid>donnée par @Michael mais va plus loin en utilisant la trapcommande qui semble fonctionner le mieux.

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

merci à @michael homer de m'avoir mis sur la bonne voie, mais utiliser la trapcommande est la meilleure approche AFAICT.


1
Vous pouvez également utiliser l'interruption SIGCHLD pour traiter les enfants à leur sortie, comme imprimer l'état à ce moment-là. Ou mettre à jour un compteur de progression: déclarer une fonction puis utiliser "trap function_name CHLD" bien que cela puisse également nécessiter une option à activer dans un shell non interactif, comme éventuellement "set -m"
Chunko

1
"Wait -n" attendra également tout enfant, puis renverra l'état de sortie de cet enfant dans le $? variable. Vous pouvez donc imprimer la progression à la sortie de chacun. Notez cependant que si vous n'utilisez pas le piège CHLD, vous risquez de manquer certaines sorties pour enfants de cette façon.
Chunko

@Chunko merci! c'est une bonne info, pourriez-vous peut-être mettre à jour la réponse avec quelque chose que vous pensez être le meilleur?
Alexander Mills

merci @Chunko, trap fonctionne mieux, vous avez raison. Avec l'attente <pid>, je suis tombé.
Alexander Mills

Pouvez-vous expliquer comment et pourquoi vous croyez que la version avec le piège est meilleure que celle sans elle? (Je crois que ce n'est pas mieux, et donc que c'est pire, parce que c'est plus complexe sans aucun avantage.)
Scott

1

Une autre variante de la réponse de @rolf:

Une autre façon de sauvegarder le statut de sortie serait quelque chose comme

mkdir /tmp/status_dir

puis avoir chaque script

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

Cela vous donne un nom unique pour chaque fichier d'état, y compris le nom du script qui l'a créé et son identifiant de processus (dans le cas où plusieurs instances du même script sont en cours d'exécution) que vous pouvez enregistrer pour référence plus tard et les mettre toutes dans le même endroit afin que vous puissiez simplement supprimer le sous-répertoire entier lorsque vous avez terminé.

Vous pouvez même enregistrer plusieurs statuts de chaque script en faisant quelque chose comme

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

qui crée le fichier comme précédemment, mais y ajoute une chaîne aléatoire unique.

Ou, vous pouvez simplement ajouter plus d'informations sur l'état au même fichier.


1

script3ne seront exécutés que si script1et script2sont couronnées de succès et script1et script2seront exécutés en parallèle:

./script1 &
process1=$!

./script2 &
process2=$!

wait $process1
rc1=$?

wait $process2
rc2=$?

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

AFAICT, ce n'est rien d'autre qu'une reprise de la réponse de Michael Homer .
Scott
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.