linux - usuario - ¿Cómo matar un proceso secundario después de un tiempo de espera dado en Bash?
ver procesos de un usuario linux (8)
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.
¿Hay una manera simple y robusta de lograr eso usando bash?
PD: dime si esta pregunta es más adecuada para serverfault o superuser.
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 $BASHPID
y el truco de exec
utilizados en la respuesta superior 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 la tubería en mi segunda respuesta , pero tener una tubería con nombre significa que el manejador de señal 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
}
Intenté evitar las condiciones de carrera en la medida de lo posible. 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.
Aquí hay un intento que intenta evitar matar un proceso después de que 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
)
}
Úselo como run_with_timeout 3 sleep 10000
, que ejecuta sleep 10000
pero lo finaliza después de 3 segundos.
Esto es como otras respuestas que usan 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://.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 terminado, todavía habrá algunos procesos persistentes "en espera", pero deberían ser inofensivos.
Esta puede ser una solución mejor que mi otra respuesta porque no usa la característica de shell no portátil read -t
y no usa pgrep
.
Suponiendo que tiene (o puede hacer fácilmente) un archivo pid para rastrear el pid del niño, entonces podría crear un script que verifique el tiempo de modulació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.
Déjeme saber si usted necesita más detalles. Si eso no parece que se ajuste a tus necesidades, ¿qué tal upstart?
También tuve esta pregunta y encontré otras dos cosas muy útiles:
- La variable SECONDS en bash.
- El comando "pgrep".
Así que uso 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 este es un ciclo, incluí un "sleep 0.2" para mantener la CPU fresca. ;-)
(Por cierto, ping es un mal ejemplo de todos modos, solo usaría la opción incorporada "-t" (tiempo de espera)).
Una forma es ejecutar el programa en una subshell y comunicarse con la subshell a través de una tubería con nombre con el comando de 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 de temporización del comando yes
después de 3 segundos. Obtiene el PID del proceso utilizando 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. Así que para evitar que el comando de read
cuelgue, he "abierto" la tubería para leerla con una subshell en segundo plano. (Otra forma de evitar que una congelación abra la lectura-escritura de la tubería, 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
(Como se ve en: entrada de BASH FAQ # 68: "¿Cómo ejecuto un comando, y lo tengo abortar (tiempo de espera) después de N segundos?" )
Si no te importa descargar algo, utiliza timeout
( sudo apt-get install timeout
) y úsalo como: (la mayoría de los sistemas ya lo tienen instalado; de lo contrario, usa sudo apt-get install coreutils
)
timeout 10 ping www.goooooogle.com
Si no desea descargar algo, haga lo que el tiempo de espera lo 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 )
# 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=$?
sleep 999&
t=$!
sleep 10
kill $t