code de retour fiable du processus d'arrière-plan

13

Supposons le morceau de code bash suivant:

foo > logfile 2>&1 &
foo_pid=$!

while ps -p$foo_pid
do
    ping -c 1 localhost
done

wait $foo_pid

if [[ $? == 0 ]]
then
    echo "foo success"
fi

Est-il sûr de supposer qu'il $?contient en effet le code retour de fooet non le code retour de ping? Si la réponse à cette question est: "Vous ne pouvez pas supposer cela." alors comment puis-je modifier ce morceau de code pour être sûr qu'il $?contient toujours le code retour de foo?

Christopher Schmidt
la source

Réponses:

12

Avec bash, vous aurez cette garantie à moins que vous n'ayez commencé un autre travail en arrière-plan (et sachez que les travaux en arrière-plan peuvent être démarrés avec &mais aussi avec coprocet avec la substitution de processus) entre le foo &et le wait.

POSIX requiert qu'un shell se souvienne de l'état de sortie d'au moins 25 tâches après leur disparition , mais s'en bashsouvient bien plus que cela.

Maintenant, si vous le faites:

foo & pid=$!
...
bar &
wait "$pid"

Vous n'avez aucune garantie qui barne recevra pas le même pid que foo(s'il fooa pris fin au moment où barcommence), donc même si c'est peu probable, il wait "$pid"peut vous donner le statut de sortie de bar.

Vous pouvez le reproduire avec:

bash -c '(exit 12; foo) & pid=$!
         while : bar & [ "$pid" != "$!" ]; do :;done
         wait "$pid"; echo "$?"'

qui vous donnera (éventuellement) 0au lieu de 12.

Pour éviter le problème, une façon serait de l'écrire comme suit:

{
  foo_pid=$!

  while ps -p "$foo_pid"
  do
      ping -c 1 localhost
  done

  bar &
  ...

  read <&3 ret
  if [ "$ret" = 0 ]; then
    echo foo was sucessful.
  fi
} 3< <(foo > logfile 2>&1; echo "$?")
Stéphane Chazelas
la source
4

Oui, vous pouvez compter sur wait "$!"pour obtenir le statut d'un travail d'arrière-plan. Lors de l'exécution en tant que script, bash ne collecte pas automatiquement les travaux d'arrière-plan terminés. Ainsi, si vous exécutez wait, il rassemblera le travail au moment de l' waitappel.

Vous pouvez tester cela avec un script simple:

#!/bin/bash
sh -c 'sleep 1; exit 22' &
sleep 5
echo "FG: $?"
wait %1
echo "BG: $?"

Qui produira:

FG: 0
BG: 22
Patrick
la source
La partie clé de cette déclaration était le début, "lors de l'exécution en tant que script". Lorsqu'il est interactif, waitne fonctionne pas. Le processus est collecté et l'état de sortie supprimé juste avant que l'invite ne s'affiche (par défaut).
Patrick
Je viens de l'essayer sur bash 4.2.37, 4.1.2 et 3.2.48. Tous se comportent exactement de la même manière (copier / coller littéral du code dans ma réponse). Le wait %1échoue avec «aucun travail» car le processus d'arrière-plan est collecté immédiatement après la fin du «sommeil 5».
Patrick
Ah ok, désolé, je comprends maintenant. Je vous avais manqué %1à la place de $!.
Stéphane Chazelas
Notez que bash -c '(sleep 1;exit 5) & sleep 2; wait %1; echo $?'(donc non interactif également) ne parvient pas à obtenir le statut de sortie de ce travail mort. Cela ressemble à un bug.
Stéphane Chazelas
cela n'a pas fonctionné pour moi dans une recette Makefile jusqu'à ce que je l'aie inclus set +e. Il semble que la set -efonction de bash tue le script dès qu'un mauvais code de sortie est généré parwait
user5359531
0

Je crois que votre hypothèse est correcte. Voici un extrait de l' man bashattente des processus d'arrière-plan.

Si n spécifie un processus ou un travail inexistant, l'état de retour est 127. Sinon, l'état de retour est l'état de sortie du dernier processus ou travail attendu.

Alors peut-être devriez-vous vérifier 127

Il y a une question similaire avec une réponse complètement différente de celle qui pourrait aider.

Le script Bash attend les processus et obtient le code retour

modifier 1

Inspiré par les commentaires et les réponses de @ Stephane, j'ai développé son script. Je peux démarrer environ 34 processus d'arrière-plan avant qu'il ne commence à perdre sa trace.

tback

$ cat tback 
plist=()
elist=()
slist=([1]=12 [2]=15 [3]=17 [4]=19 [5]=21 [6]=23)
count=30

#start background tasksto monitor
for i in 1 2 3 4
do
  #echo pid $i ${plist[$i]} ${slist[$i]}
  (echo $BASHPID-${slist[$i]} running; exit ${slist[$i]}) & 
  plist[$i]=$!
done

echo starting $count background echos to test history
for i in `eval echo {1..$count}`
do
  echo -n "." &
  elist[$i]=$! 
done
# wait for each background echo to complete
for i in `eval echo {1..$count}`
do
  wait ${elist[$i]}
  echo -n $? 
done
echo ""
# Now wait for each monitored process and check return status with expected
failed=0
for i in 1 2 3 4
do
  wait ${plist[$i]}
  rv=$?
  echo " pid ${plist[$i]} returns $rv should be ${slist[$i]}"
  if [[ $rv != ${slist[$i]} ]] 
  then
    failed=1
  fi
done

wait
echo "Complete $failed"
if [[ $failed = "1" ]]
then
  echo Failed
else
  echo Success
fi
exit $failed
$ 

sur mon système produit

$ bash tback
14553-12 running
14554-15 running
14555-17 running
starting 30 background echos to test history
14556-19 running
..............................000000000000000000000000000000
 pid 14553 returns 12 should be 12
 pid 14554 returns 15 should be 15
 pid 14555 returns 17 should be 17
 pid 14556 returns 19 should be 19
Complete 0
Success
X Tian
la source
1
Non, voyez mon commentaire sur la réponse du tréma , et essayez-le par vous-même avecbash -c '(exit 12) & sleep 1; wait "$!"; echo "$?"'
Stéphane Chazelas
Je n'ai jamais vu de bashpiste lâche (même après avoir commencé des milliers d'emplois), mon exemple démontrait que le pid était réutilisé, ce qui peut être ce que vous avez également observé dans votre cas.
Stéphane Chazelas