Come uccidere un processo figlio dopo un determinato timeout in Bash?


178

Ho uno script bash che avvia un processo figlio che si blocca (in realtà, si blocca) di volta in volta e senza una ragione apparente (fonte chiusa, quindi non c'è molto che posso fare al riguardo). Di conseguenza, vorrei essere in grado di avviare questo processo per un determinato periodo di tempo e ucciderlo se non è tornato correttamente dopo un determinato periodo di tempo.

Esiste un modo semplice e robusto per raggiungere questo obiettivo usando bash?

PS: dimmi se questa domanda è più adatta a serverfault o superuser.



Risposte:


260

(Come visto in: BASH FAQ entry # 68: "Come posso eseguire un comando e farlo annullare (timeout) dopo N secondi?" )

Se non ti dispiace scaricare qualcosa, usa timeout( sudo apt-get install timeout) e usalo come: (la maggior parte dei sistemi lo ha già installato altrimenti usa sudo apt-get install coreutils)

timeout 10 ping www.goooooogle.com

Se non vuoi scaricare qualcosa, fai ciò che il timeout fa internamente:

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

Nel caso in cui si desideri eseguire un timeout per un codice bash più lungo, utilizzare la seconda opzione come tale:

( cmdpid=$BASHPID; 
    (sleep 10; kill $cmdpid) \
   & while ! ping -w 1 www.goooooogle.com 
     do 
         echo crap; 
     done )

8
La risposta di Re Ignacio nel caso in cui qualcun altro si chieda cosa ho fatto: cmdpid=$BASHPIDnon prenderà il pid della shell chiamante ma la (prima) subshell che viene avviata da (). La (sleepcosa ... chiama una seconda subshell all'interno della prima subshell per attendere 10 secondi in background e uccidere la prima subshell che, dopo aver avviato il processo della subshell killer, procede a eseguire il suo carico di lavoro ...
jamadagni,

17
timeoutfa parte dei coreutils GNU, quindi dovrebbe già essere installato in tutti i sistemi GNU.
Sameer,

1
@Sameer: ​​solo dalla versione 8.
Ignacio Vazquez-Abrams,

3
Non ne sono sicuro al 100%, ma per quanto ne so (e so cosa mi ha detto la mia manpage) timeoutora fa parte dei coreutils.
benaryorg,

5
Questo comando non "finisce presto". Ucciderà sempre il processo al timeout, ma non gestirà la situazione in cui non è scaduto.
falco

28
# Spawn a child process:
(dosmth) & pid=$!
# in the background, sleep for 10 secs then kill that process
(sleep 10 && kill -9 $pid) &

o per ottenere anche i codici di uscita:

# 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=$?

8
Non dovresti usare kill -9prima di provare i segnali che un processo può elaborare prima.
In pausa fino a nuovo avviso.

È vero, stavo comunque cercando una soluzione rapida e ho solo supposto che volesse il processo morto immediatamente perché ha detto che si blocca
Dan

8
Questa è in realtà una pessima soluzione. E se dosmthtermina in 2 secondi, un altro processo prende il vecchio pid e tu uccidi quello nuovo?
Teletrasporto Capra

Il riciclaggio PID funziona raggiungendo il limite e avvolgendolo. È molto improbabile che un altro processo riutilizzi il PID entro i restanti 8 secondi, a meno che il sistema non vada completamente in tilt.
kittydoor,

13
sleep 999&
t=$!
sleep 10
kill $t

Richiede un'attesa eccessiva. Cosa succede se un comando reale ( sleep 999qui) finisce spesso più velocemente del sonno imposto ( sleep 10)? E se volessi dargli una possibilità fino a 1 minuto, 5 minuti? E se avessi un sacco di casi del genere nella mia sceneggiatura :)
it3xl

3

Ho anche avuto questa domanda e ho trovato altre due cose molto utili:

  1. La variabile SECONDI in bash.
  2. Il comando "pgrep".

Quindi uso qualcosa di simile sulla riga di comando (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

Dato che questo è un loop, ho incluso un "sleep 0.2" per mantenere la CPU fresca. ;-)

(A proposito: il ping è comunque un cattivo esempio, useresti semplicemente l'opzione "-t" (timeout) incorporata.)


1

Supponendo che tu abbia (o possa facilmente creare) un file pid per tracciare il pid del bambino, potresti quindi creare uno script che controlla il modtime del file pid e uccide / respawn il processo secondo necessità. Quindi inserisci lo script in crontab per eseguirlo all'incirca nel periodo che ti serve.

Fammi sapere se hai bisogno di più dettagli. Se questo non suona come se fosse adatto alle tue esigenze, che dire di upstart?


1

Un modo consiste nell'eseguire il programma in una subshell e comunicare con la subshell tramite una pipe denominata con il readcomando. In questo modo è possibile verificare lo stato di uscita del processo in esecuzione e comunicarlo nuovamente attraverso la pipe.

Ecco un esempio di timeout del yescomando dopo 3 secondi. Ottiene il PID del processo utilizzando pgrep(forse funziona solo su Linux). C'è anche qualche problema con l'uso di una pipe in quanto un processo che apre una pipe per la lettura si bloccherà fino a quando non verrà aperto anche per la scrittura e viceversa. Quindi, per evitare che il readcomando si blocchi, ho "incuneato" l'apertura della pipe per la lettura con una sottostruttura di sottofondo. (Un altro modo per impedire che un congelamento apra la lettura / scrittura della pipe, ovvero read -t 5 <>finished.pipe, tuttavia, che potrebbe anche non funzionare se non con 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

0

Ecco un tentativo che cerca di evitare di uccidere un processo dopo che è già uscito, il che riduce la possibilità di uccidere un altro processo con lo stesso ID processo (sebbene sia probabilmente impossibile evitare completamente questo tipo di errore).

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
  )
}

Usa like run_with_timeout 3 sleep 10000, che viene eseguito sleep 10000ma lo termina dopo 3 secondi.

Questo è come altre risposte che utilizzano un processo di timeout in background per interrompere il processo figlio dopo un ritardo. Penso che sia quasi uguale alla risposta estesa di Dan ( https://stackoverflow.com/a/5161274/1351983 ), tranne per il fatto che la shell di timeout non verrà uccisa se è già terminata.

Al termine di questo programma, ci saranno ancora alcuni processi di "sospensione" persistenti in esecuzione, ma dovrebbero essere innocui.

Questa potrebbe essere una soluzione migliore rispetto alla mia altra risposta perché non utilizza la funzione di shell non portatile read -te non utilizza pgrep.


Qual è la differenza tra (exec sh -c "$*") &e sh -c "$*" &? In particolare, perché usare il primo anziché il secondo?
Giustino C,

0

Ecco la terza risposta che ho inviato qui. Questo gestisce gli interrupt di segnale e pulisce i processi in background quando SIGINTviene ricevuto. Usa il trucco $BASHPIDe execusato nella risposta in alto per ottenere il PID di un processo (in questo caso $$in una shchiamata). Utilizza un FIFO per comunicare con una subshell responsabile dell'uccisione e della pulizia. (Questo è come il pipe nella mia seconda risposta , ma avere un pipe chiamato significa che anche il gestore del segnale può scrivere in esso.)

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
}

Ho cercato di evitare le condizioni di gara per quanto posso. Tuttavia, una fonte di errore che non ho potuto rimuovere è quando il processo termina quasi contemporaneamente al timeout. Ad esempio, run_with_timeout 2 sleep 2oppure run_with_timeout 0 sleep 0. Per me, quest'ultimo fornisce un errore:

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

mentre sta cercando di uccidere un processo che è già uscito da solo.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.