web-dev-qa-db-fra.com

Comment tuer un processus enfant après un délai donné dans Bash?

J'ai un script bash qui lance un processus enfant qui se bloque (se bloque) de temps en temps et sans raison apparente (source fermée, donc je ne peux rien faire à ce sujet). En conséquence, j'aimerais pouvoir lancer ce processus pendant un laps de temps donné et le tuer s'il échouait après un certain laps de temps.

Existe-t-il un moyen simple et robuste pour y parvenir en utilisant bash?

PS: dites-moi si cette question convient mieux à serverfault ou au super-utilisateur.

157
Greg

(Comme on le voit dans: BASH FAQ entrée # 68: "Comment puis-je exécuter une commande et la faire abandonner (délai d'attente) après N secondes? " )

Si cela ne vous dérange pas de télécharger quelque chose, utilisez timeout (Sudo apt-get install timeout) et utilisez-le comme: (la plupart des systèmes l’ont déjà installé, sinon utilisez Sudo apt-get install coreutils)

timeout 10 ping www.goooooogle.com

Si vous ne voulez pas télécharger quelque chose, faites ce que timeout fait en interne:

( cmdpid=$BASHPID; (sleep 10; kill $cmdpid) & exec ping www.goooooogle.com )

Au cas où vous voudriez faire un timeout pour un code bash plus long, utilisez la deuxième option en tant que telle:

( cmdpid=$BASHPID; 
    (sleep 10; kill $cmdpid) \
   & while ! ping -w 1 www.goooooogle.com 
     do 
         echo crap; 
     done )
238
# Spawn a child process:
(dosmth) & pid=$!
# in the background, sleep for 10 secs then kill that process
(sleep 10 && kill -9 $pid) &

ou pour obtenir les codes de sortie également:

# Spawn a child process:
(dosmth) & pid=$!
# in the background, sleep for 10 secs then kill that process
(sleep 10 && kill -9 $pid) & waiter=$!
# wait on our worker process and return the exitcode
exitcode=$(wait $pid && echo $?)
# kill the waiter subshell, if it still runs
kill -9 $waiter 2>/dev/null
# 0 if we killed the waiter, cause that means the process finished before the waiter
finished_gracefully=$?
26
Dan
sleep 999&
t=$!
sleep 10
kill $t
11
DigitalRoss

J'avais aussi cette question et trouvais deux choses très utiles:

  1. La variable SECONDS dans bash.
  2. La commande "pgrep".

J'utilise donc quelque chose comme ceci sur la ligne de commande (OSX 10.9):

ping www.goooooogle.com & PING_PID=$(pgrep 'ping'); SECONDS=0; while pgrep -q 'ping'; do sleep 0.2; if [ $SECONDS = 10 ]; then kill $PING_PID; fi; done

Comme il s’agit d’une boucle, j’ai inclus un "sommeil 0.2" pour garder le CPU au frais. ;-)

(BTW: le ping est de toute façon un mauvais exemple, vous utiliseriez simplement l’option "-t" (timeout) intégrée.)

3
Ulrich

En supposant que vous avez (ou pouvez facilement créer) un fichier pid pour suivre le pid de l'enfant, vous pouvez ensuite créer un script qui vérifie l'heure de modification du fichier pid et tue/respawns le processus selon les besoins. Ensuite, placez le script dans crontab pour qu’il s’exécute approximativement à la période souhaitée.

Faites-moi savoir si vous avez besoin de plus de détails. Si cela ne vous convient pas, qu'en est-il de pstart?

1
kojiro

Une solution consiste à exécuter le programme dans un sous-shell et à communiquer avec ce dernier via un canal nommé à l'aide de la commande read. De cette façon, vous pouvez vérifier l’état de sortie du processus en cours d’exécution et le communiquer à travers le canal.

Voici un exemple de dépassement de la commande yes après 3 secondes. Il obtient le PID du processus en utilisant pgrep (ne fonctionne probablement que sous Linux). L’utilisation d’un canal pose également un problème, puisqu’un processus ouvrant un canal en lecture reste suspendu jusqu’à ce qu’il soit également ouvert en écriture, et inversement. Donc, pour éviter que la commande read ne soit suspendue, j'ai "bloqué" l'ouverture du tube pour lecture avec un sous-fond. (Un autre moyen d’empêcher le gel d’ouvrir le tuyau en lecture-écriture, c’est-à-dire read -t 5 <>finished.pipe - cependant, cela peut aussi ne pas fonctionner sauf avec Linux.)

rm -f finished.pipe
mkfifo finished.pipe

{ yes >/dev/null; echo finished >finished.pipe ; } &
SUBSHELL=$!

# Get command PID
while : ; do
    PID=$( pgrep -P $SUBSHELL yes )
    test "$PID" = "" || break
    sleep 1
done

# Open pipe for writing
{ exec 4>finished.pipe ; while : ; do sleep 1000; done } &  

read -t 3 FINISHED <finished.pipe

if [ "$FINISHED" = finished ] ; then
  echo 'Subprocess finished'
else
  echo 'Subprocess timed out'
  kill $PID
fi

rm finished.pipe
1
Gavin Smith

Voici une tentative visant à éviter de tuer un processus après sa sortie, ce qui réduit les chances de tuer un autre processus avec le même ID de processus (même s’il est probablement impossible d’éviter complètement ce type d’erreur).

run_with_timeout ()
{
  t=$1
  shift

  echo "running \"$*\" with timeout $t"

  (
  # first, run process in background
  (exec sh -c "$*") &
  pid=$!
  echo $pid

  # the timeout Shell
  (sleep $t ; echo timeout) &
  waiter=$!
  echo $waiter

  # finally, allow process to end naturally
  wait $pid
  echo $?
  ) \
  | (read pid
     read waiter

     if test $waiter != timeout ; then
       read status
     else
       status=timeout
     fi

     # if we timed out, kill the process
     if test $status = timeout ; then
       kill $pid
       exit 99
     else
       # if the program exited normally, kill the waiting Shell
       kill $waiter
       exit $status
     fi
  )
}

Utilisez comme run_with_timeout 3 sleep 10000, qui exécute sleep 10000 mais le termine au bout de 3 secondes.

Cela ressemble à d'autres réponses qui utilisent un processus de délai d'attente en arrière-plan pour tuer le processus enfant après un délai. Je pense que cela est presque identique à la réponse étendue de Dan ( https://stackoverflow.com/a/5161274/135198 ), sauf que le délai d'expiration Shell ne sera pas tué s'il est déjà terminé.

Une fois ce programme terminé, il restera quelques processus de "sommeil" en cours d'exécution, mais ils devraient être inoffensifs.

C'est peut-être une meilleure solution que mon autre réponse, car elle n'utilise pas la fonctionnalité non portable de Shell read -t et n'utilise pas pgrep.

0
Gavin Smith

Voici la troisième réponse que j'ai soumise ici. Celui-ci gère les interruptions de signal et nettoie les processus en arrière-plan lorsque SIGINT est reçu. Il utilise le $BASHPID et exec astuce utilisée dans le réponse du haut pour obtenir le PID d’un processus (dans ce cas, $$ dans une invocation sh). Il utilise un FIFO pour communiquer avec un sous-shell responsable de la destruction et du nettoyage. (Cela ressemble au tuyau dans ma deuxième réponse , mais avec un tuyau nommé signifie que le gestionnaire de signal peut aussi y écrire.)

run_with_timeout ()
{
  t=$1 ; shift

  trap cleanup 2

  F=$$.fifo ; rm -f $F ; mkfifo $F

  # first, run main process in background
  "$@" & pid=$!

  # sleeper process to time out
  ( sh -c "echo \$\$ >$F ; exec sleep $t" ; echo timeout >$F ) &
  read sleeper <$F

  # control Shell. read from fifo.
  # final input is "finished".  after that
  # we clean up.  we can get a timeout or a
  # signal first.
  ( exec 0<$F
    while : ; do
      read input
      case $input in
        finished)
          test $sleeper != 0 && kill $sleeper
          rm -f $F
          exit 0
          ;;
        timeout)
          test $pid != 0 && kill $pid
          sleeper=0
          ;;
        signal)
          test $pid != 0 && kill $pid
          ;;
      esac
    done
  ) &

  # wait for process to end
  wait $pid
  status=$?
  echo finished >$F
  return $status
}

cleanup ()
{
  echo signal >$$.fifo
}

J'ai essayé d'éviter autant que possible les conditions de course. Cependant, l'une des sources d'erreur que je n'ai pas pu supprimer est la fin du processus à peu près au même moment que l'expiration du délai. Par exemple, run_with_timeout 2 sleep 2 ou run_with_timeout 0 sleep 0. Pour moi, ce dernier donne une erreur:

timeout.sh: line 250: kill: (23248) - No such process

comme il essaie de tuer un processus qui est déjà sorti de lui-même.

0
Gavin Smith