¿Cómo matar un proceso secundario después de un tiempo de espera determinado en Bash?


178

Tengo un script bash que inicia un proceso secundario que se bloquea (en realidad, se bloquea) de vez en cuando y sin razón aparente (código cerrado, por lo que no hay mucho que pueda hacer al respecto). Como resultado, me gustaría poder iniciar este proceso durante un período de tiempo determinado y matarlo si no regresa con éxito después de un período de tiempo determinado.

¿Hay una manera simple y robusta de lograr eso usando bash?

PD: dime si esta pregunta es más adecuada para serverfault o superusuario.



Respuestas:


260

(Como se ve en: BASH FAQ entrada # 68: "¿Cómo ejecuto un comando y lo hago abortar (tiempo de espera) después de N segundos?" )

Si no le importa descargar algo, use timeout( sudo apt-get install timeout) y úselo como: (la mayoría de los sistemas ya lo tienen instalado, de lo contrario use sudo apt-get install coreutils)

timeout 10 ping www.goooooogle.com

Si no desea descargar algo, haga lo que el tiempo de espera hace internamente:

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

En caso de que desee hacer un tiempo de espera para un código bash más largo, use la segunda opción como tal:

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

8
La respuesta de Re Ignacio en caso de que alguien más se pregunte lo que hice: cmdpid=$BASHPIDno tomará el pid del shell de llamada sino el (primer) subshell que inicia (). Lo (sleep... llama a una segunda subshell dentro de la primera subshell para esperar 10 segundos en segundo plano y matar a la primera subshell que, después de haber lanzado el proceso de subshell asesino, procede a ejecutar su carga de trabajo ...
jamadagni

17
timeoutforma parte de los coreutils de GNU, por lo que ya debería estar instalado en todos los sistemas GNU.
Sameer

1
@Sameer: ​​solo a partir de la versión 8.
Ignacio Vázquez-Abrams

3
No estoy 100% seguro de eso, pero que yo sepa (y sé lo que me dijo mi página de manual) timeoutahora es parte de los coreutils.
benaryorg

55
Este comando no 'termina temprano'. Siempre matará el proceso en el tiempo de espera, pero no manejará la situación en la que no se agotó el tiempo de espera.
Hawkeye

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

o para obtener los códigos de salida también:

# 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
No debe usar kill -9antes de probar las señales de que un proceso puede procesar primero.
Pausado hasta nuevo aviso.

Es cierto, sin embargo, estaba buscando una solución rápida y simplemente asumí que quería que el proceso se interrumpiera instantáneamente porque dijo que se bloquea
Dan

8
Esa es realmente una muy mala solución. ¿Qué pasa si dosmthtermina en 2 segundos, otro proceso toma el viejo pid y matas al nuevo?
Teletransportando cabra el

El reciclaje PID funciona alcanzando el límite y envolviendo. Es muy poco probable que otro proceso reutilice el PID dentro de los 8 segundos restantes, a menos que el sistema se vuelva completamente loco.
kittydoor

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

Incurre en espera excesiva. ¿Qué pasa si un comando real ( sleep 999aquí) a menudo termina más rápido que el sueño impuesto ( sleep 10)? ¿Qué sucede si deseo darle una oportunidad de hasta 1 minuto, 5 minutos? ¿Qué pasa si tengo un montón de estos casos en mi secuencia de comandos :)
it3xl

3

También tuve esta pregunta y encontré dos cosas más muy útiles:

  1. La variable SEGUNDOS en bash.
  2. El comando "pgrep".

Entonces uso algo como esto en la línea de 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

Como se trata de un bucle, incluí un "sleep 0.2" para mantener fría la CPU. ;-)

(Por cierto: ping es un mal ejemplo de todos modos, solo usaría la opción incorporada "-t" (tiempo de espera)).


1

Suponiendo que tiene (o puede hacer fácilmente) un archivo pid para rastrear el pid del niño, puede crear un script que verifique el tiempo de modulación del archivo pid y elimine / reaparezca el proceso según sea necesario. Luego, simplemente coloque el script en crontab para ejecutarlo aproximadamente en el período que necesita.

Déjeme saber si usted necesita más detalles. Si eso no parece satisfacer sus necesidades, ¿qué pasa con el advenedizo?


1

Una forma es ejecutar el programa en una subshell y comunicarse con la subshell a través de una tubería con nombre read comando. De esta forma, puede verificar el estado de salida del proceso que se está ejecutando y comunicarlo nuevamente a través de la tubería.

Aquí hay un ejemplo de tiempo de espera del yescomando después de 3 segundos. Obtiene el PID del proceso usando pgrep(posiblemente solo funciona en Linux). También hay algún problema con el uso de una tubería, ya que un proceso que abre una tubería para la lectura se bloqueará hasta que también se abra para la escritura, y viceversa. Entonces, para evitar que el readcomando se cuelgue, he "acuñado" para abrir la tubería para leer con una subshell de fondo. (Otra forma de evitar una congelación para abrir la tubería de lectura-escritura, es decir read -t 5 <>finished.pipe, sin embargo, eso también puede no funcionar, excepto 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

Aquí hay un intento que intenta evitar matar un proceso después de que ya haya salido, lo que reduce la posibilidad de matar otro proceso con la misma ID de proceso (aunque probablemente sea imposible evitar este tipo de error por completo).

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

Use like run_with_timeout 3 sleep 10000, que se ejecutasleep 10000 pero termina después de 3 segundos.

Esto es como otras respuestas que usan un proceso de tiempo de espera en segundo plano para matar el proceso secundario después de un retraso. Creo que esto es casi lo mismo que la respuesta extendida de Dan ( https://stackoverflow.com/a/5161274/1351983 ), excepto que el shell de tiempo de espera no se eliminará si ya ha terminado.

Después de que este programa haya finalizado, todavía habrá algunos procesos de "suspensión" persistentes en ejecución, pero deberían ser inofensivos.

Esta puede ser una mejor solución que mi otra respuesta porque no usa la función de shell no portátil read -ty no la usa pgrep.


¿Cuál es la diferencia entre (exec sh -c "$*") &y sh -c "$*" &? Específicamente, ¿por qué usar el primero en lugar del segundo?
Justin C

0

Aquí está la tercera respuesta que he enviado aquí. Éste maneja las interrupciones de señal y limpia los procesos en segundo plano cuando SIGINTse recibe. Utiliza el truco $BASHPIDy executilizado en la respuesta superior para obtener el PID de un proceso (en este caso $$en una shinvocación). Utiliza un FIFO para comunicarse con un subshell que es responsable de matar y limpiar. (Esto es como la tubería en mi segunda respuesta , pero tener una tubería con nombre significa que el manejador de señales también puede escribir en ella).

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
}

He tratado de evitar las condiciones de carrera lo más que puedo. Sin embargo, una fuente de error que no pude eliminar es cuando el proceso finaliza casi al mismo tiempo que el tiempo de espera. Por ejemplo, run_with_timeout 2 sleep 2o run_with_timeout 0 sleep 0. Para mí, este último da un error:

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

ya que está tratando de matar un proceso que ya ha salido por sí mismo.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.