2011-03-01 11 views
116

Tengo un script bash que inicia un proceso secundario que se cuelga (en realidad, se cuelga) de vez en cuando y sin razón aparente (fuente cerrada, 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 regresó correctamente después de un período de tiempo determinado.Cómo matar un proceso secundario después de un tiempo de espera determinado en Bash?

¿Hay simple y robusta forma de lograr eso con bash?

P.S .: dime si esta pregunta es más adecuada para serverfault o superuser.

+0

Relacionado: http://stackoverflow.com/q/601543/132382 – pilcrow

Respuesta

179

(Como se ve en: BASH FAQ entry #68: "How do I run a command, and have it abort (timeout) after N seconds?")

Si no le importa a descargar algo, utiliza timeout (sudo apt-get install timeout) y usarlo como:

timeout 10 ping www.goooooogle.com 

Si no quiere descargar algo, no lo hace internamente tiempo de espera:

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

En caso de que usted quiere hacer un tiempo de espera para el código de fiesta más largo, utilizar la segunda opción como tal:

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

Respuesta de Re Ignacio en caso de que alguien más se pregunte lo que hice: el 'cmdpid = $ BASHPID' no tomará el pid de la shell * calling * pero la (primera) subshell que se inicia con '()'. El '' sueño' ... llama a una segunda subcadena dentro de la primera subshell para esperar 10 segundos en el fondo y matar la primera subshell que, después de haber lanzado el proceso de subshell asesino, procede a ejecutar su carga de trabajo ... – jamadagni

+13

'timeout 'es parte de GNU coreutils, por lo que ya debería estar instalado en todos los sistemas GNU. – Sameer

+1

@Sameer: ​​solo a partir de la versión 8. –

1

Suponiendo que tiene (o puede hacer fácilmente) un archivo pid para rastrear el pid del hijo, puede crear un script que compruebe el tiempo de modificación del archivo pid y elimine/vuelva a generar el proceso según sea necesario. Luego, simplemente ponga la secuencia de comandos en crontab para que se ejecute aproximadamente en el período que necesita.

Deseo saber si necesita más información. Si eso no suena como que sería adapte a sus necesidades, lo que acerca upstart?

21
# 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, así:

# 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 -9' antes de probar las señales que un proceso puede procesar primero. –

+0

Cierto, estaba buscando una solución rápida y simplemente asumí que quiere que el proceso se cancele instantáneamente porque dijo que se bloquea – Dan

+6

. Esa es realmente una muy mala solución. ¿Qué pasa si 'dosmth' termina en 2 segundos, otro proceso toma el viejo pid, y matas el nuevo? –

1

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

Aquí hay un ejemplo del tiempo de espera del comando yes después de 3 segundos. Obtiene el PID del proceso usando pgrep (posiblemente solo funcione en Linux). También hay algún problema con el uso de una tubería porque un proceso que abre una tubería para lectura se bloqueará hasta que también se abra para escritura, y viceversa. Por lo tanto, para evitar que se cuelgue el comando read, he "encajado" el tubo para leerlo con una subcadena de fondo. (Otra forma de evitar la congelación para abrir el tubo de lectura-escritura, es decir read -t 5 <>finished.pipe - sin embargo, que 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 
3

También tuve esta pregunta y encontré dos cosas más de gran utilidad:

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

Así que usar algo como esto en la línea de comandos (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í una "dormir 0,2" para mantener la CPU fresco. ;-)

(Por cierto:. Ping es un mal ejemplo de todos modos, sólo utilizaría la opción incorporada "-t" (tiempo de espera))

0

Aquí es un intento que trata de evitar la muerte de un proceso después de ya ha salido, lo que reduce la posibilidad de matar a otro proceso con el mismo ID de proceso (aunque es probable que 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 
) 
} 

uso como run_with_timeout 3 sleep 10000, que se extiende sleep 10000 pero termina después de 3 segundos.

Esto es como otras respuestas que utilizan un proceso de tiempo de espera de fondo 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 finalizado.

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.

Esto puede ser una solución mejor que mi otra respuesta, ya que no utiliza la función de shell no portátil read -t y no utiliza pgrep.

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 se recibe SIGINT. Utiliza el truco $BASHPID y exec utilizado en el top answer para obtener el PID de un proceso (en este caso $$ en una invocación sh). Utiliza un FIFO para comunicarse con una subshell que es responsable de matar y limpiar. (Esto es como el tubo en mi second answer, pero que tiene una tubería con nombre significa que el controlador de señal puede escribir en él también.)

run_with_timeout() 
{ 
    t=$1 ; shift 

    trap cleanup 2 

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

    # first, run main process in background 
    "[email protected]" & 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 condiciones de carrera tan lejos como pueda. 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 2 o 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 a un proceso que ya ha salido por sí mismo.

Cuestiones relacionadas