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

178

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

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

PS: dites-moi si cette question est mieux adaptée à serverfault ou superuser.

Greg
la source
En relation: stackoverflow.com/q/601543/132382
Pilcrow
Réponse très complète ici: stackoverflow.com/a/58873049/2635443
Orsiris de Jong

Réponses:

260

(Comme vu dans: BASH FAQ entrée # 68: "Comment exécuter une commande et la faire abandonner (délai d'expiration) 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 )

Dans le cas où vous souhaitez 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 )
Ignacio Vazquez-Abrams
la source
8
Réponse de Re Ignacio au cas où quelqu'un d'autre se demanderait ce que j'ai fait: le cmdpid=$BASHPIDne prendra pas le pid du shell appelant mais le (premier) sous-shell qui est démarré (). La (sleepchose ... appelle un deuxième sous-shell dans le premier sous-shell pour attendre 10 secondes en arrière-plan et tuer le premier sous-shell qui, après avoir lancé le processus de sous-shell tueur, procède à l'exécution de sa charge de travail ...
jamadagni
17
timeoutfait partie de GNU coreutils, il devrait donc déjà être installé dans tous les systèmes GNU.
Sameer
1
@Sameer: ​​Seulement à partir de la version 8.
Ignacio Vazquez-Abrams
3
Je n'en suis pas sûr à 100%, mais pour autant que je sache (et je sais ce que ma page de manuel m'a dit) timeoutfait maintenant partie des coreutils.
benaryorg
5
Cette commande ne «se termine pas tôt». Il tuera toujours le processus à l'expiration du délai, mais ne gérera pas la situation où il n'a pas expiré.
hawkeye
28
# 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 également les codes de sortie:

# 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=$?
Dan
la source
8
Vous ne devez pas utiliser kill -9avant d'essayer les signaux qu'un processus peut traiter en premier.
Suspendu jusqu'à nouvel ordre.
Certes, j'allais pour une solution rapide cependant et je supposais simplement qu'il voulait que le processus soit mort instantanément parce qu'il a dit qu'il se bloque
Dan
8
C'est en fait une très mauvaise solution. Et si se dosmthtermine dans 2 secondes, un autre processus prend l'ancien pid et vous tuez le nouveau?
Teleporting Goat le
Le recyclage PID fonctionne en atteignant la limite et en s'enroulant. Il est très peu probable qu'un autre processus réutilise le PID dans les 8 secondes restantes, sauf si le système se détraque complètement.
kittydoor
13
sleep 999&
t=$!
sleep 10
kill $t
DigitalRoss
la source
Cela entraîne une attente excessive. Et si une vraie commande ( sleep 999ici) finissait souvent plus vite que le sleep imposé ( sleep 10)? Et si je souhaite lui donner une chance jusqu'à 1 minute, 5 minutes? Et si j'avais un tas de cas de ce genre dans mon script :)
it3xl
3

J'ai également eu cette question et j'ai trouvé deux autres choses très utiles:

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

J'utilise donc quelque chose comme ça 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 "sleep 0.2" pour garder le CPU au frais. ;-)

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

Ulrich
la source
1

En supposant que vous avez (ou pouvez facilement créer) un fichier pid pour suivre le pid de l'enfant, vous pouvez alors créer un script qui vérifie le modtime du fichier pid et tue / réapparaît le processus si nécessaire. Ensuite, placez simplement le script dans crontab pour qu'il s'exécute approximativement à la période dont vous avez besoin.

Faites-moi savoir si vous avez besoin de plus de détails. Si cela ne semble pas répondre à vos besoins, qu'en est-il du parvenu?

Kojiro
la source
1

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

Voici un exemple d'expiration de la yescommande après 3 secondes. Il obtient le PID du processus en utilisant pgrep(peut-être ne fonctionne que sous Linux). Il y a aussi un problème avec l'utilisation d'un tube en ce qu'un processus ouvrant un tube en lecture se bloque jusqu'à ce qu'il soit également ouvert en écriture, et vice versa. Donc, pour éviter que la readcommande ne se bloque, j'ai "calé" ouvert le tube pour la lecture avec un sous-shell d'arrière-plan. (Une autre façon d'éviter un gel pour ouvrir le tube en lecture-écriture, c'est read -t 5 <>finished.pipe-à- dire - cependant, cela peut également 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
Gavin Smith
la source
0

Voici une tentative qui essaie d'éviter de tuer un processus après sa sortie, ce qui réduit le risque de tuer un autre processus avec le même ID de processus (bien qu'il soit 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 like run_with_timeout 3 sleep 10000, qui s'exécute sleep 10000mais le termine après 3 secondes.

C'est comme d'autres réponses qui utilisent un processus de temporisation en arrière-plan pour tuer le processus enfant après un délai. Je pense que c'est presque la même chose que la réponse étendue de Dan ( https://stackoverflow.com/a/5161274/1351983 ), sauf que le shell timeout ne sera pas tué s'il est déjà terminé.

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

Cela peut être une meilleure solution que mon autre réponse car elle n'utilise pas la fonctionnalité de shell non portable read -tet ne l'utilise pas pgrep.

Gavin Smith
la source
Quelle est la différence entre (exec sh -c "$*") &et sh -c "$*" &? Plus précisément, pourquoi utiliser le premier au lieu du second?
Justin C
0

Voici la troisième réponse que j'ai soumise ici. Celui-ci gère les interruptions de signal et nettoie les processus d'arrière-plan lorsqu'il SIGINTest reçu. Il utilise l' astuce $BASHPIDet executilisée dans la réponse du haut pour obtenir le PID d'un processus (dans ce cas $$dans une shinvocation). Il utilise un FIFO pour communiquer avec un sous-shell qui est responsable de la mise à mort et du nettoyage. (C'est comme le tube dans ma deuxième réponse , mais avoir un tube nommé signifie que le gestionnaire de signal peut également 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 les conditions de course autant que possible. Cependant, une source d'erreur que je n'ai pas pu supprimer est lorsque le processus se termine presque en même temps que le délai d'expiration. Par exemple, run_with_timeout 2 sleep 2ou run_with_timeout 0 sleep 0. Pour moi, ce dernier donne une erreur:

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

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

Gavin Smith
la source