¿Cómo elimino los procesos / trabajos en segundo plano cuando sale mi script de shell?

193

Estoy buscando una manera de limpiar el desorden cuando sale mi script de nivel superior.

Especialmente si quiero usar set -e, desearía que el proceso en segundo plano muriera cuando salga el script.

elmarco
fuente

Respuestas:

186

Para limpiar un poco de desorden, trapse puede utilizar. Puede proporcionar una lista de cosas ejecutadas cuando llega una señal específica:

trap "echo hello" SIGINT

pero también se puede usar para ejecutar algo si el shell sale:

trap "killall background" EXIT

Es un programa incorporado, por lo que help traple dará información (funciona con bash). Si solo desea eliminar trabajos en segundo plano, puede hacer

trap 'kill $(jobs -p)' EXIT

Tenga cuidado con el uso individual ', para evitar que el caparazón sustituya al $()inmediato.

Johannes Schaub - litb
fuente
entonces, ¿cómo matas solo a un niño ? (o me falta algo obvio)
elmarco
18
killall mata a tus hijos, pero no a ti
orip
44
kill $(jobs -p)no funciona en el tablero, ya que la sustitución de comandos se ejecuta en un subnivel (ver comando de sustitución en el hombre guión)
user1431317
8
se killall backgroundsupone que es un marcador de posición? backgroundno está en la página del manual ...
Evan Benn
170

Esto funciona para mí (mejorado gracias a los comentaristas):

trap "trap - SIGTERM && kill -- -$$" SIGINT SIGTERM EXIT
  • kill -- -$$envía una SIGTERM a todo el grupo de procesos, matando así también a los descendientes.

  • Especificar la señal EXITes útil cuando se usa set -e(más detalles aquí ).

tokland
fuente
1
Debería funcionar bien en general, pero los procesos secundarios pueden cambiar los grupos de procesos. Por otro lado, no requiere control de trabajo, y también puede hacer que algunos procesos de nietos se pierdan por otras soluciones.
michaeljt
55
Tenga en cuenta que "kill 0" también eliminará un script bash primario. Es posible que desee utilizar "kill - - $ BASHPID" para matar solo a los hijos del script actual. Si no tiene $ BASHPID en su versión bash, puede exportar BASHPID = $ (sh -c 'echo $ PPID')
ACyclic
2
¡Gracias por la solución agradable y clara! Desafortunadamente, segfaults Bash 4.3, que permite la recurrencia de trampas. Me encontré con esto en 4.3.30(1)-releaseOSX, y también se confirmó en Ubuntu . Sin embargo , hay un obvio obvio :)
skozin
44
No entiendo del todo -$$. Se evalúa a '- <PID> `por ejemplo -1234. En la página de manual de kill // página de manual integrada, un guión inicial especifica la señal que se enviará. Sin embargo, probablemente bloquea eso, pero el guión principal no está documentado de lo contrario. ¿Alguna ayuda?
Evan Benn
44
@EvanBenn: Marque man 2 kill, lo que explica que cuando un PID es negativo, la señal se envía a todos los procesos en el grupo de procesos con la ID proporcionada ( en.wikipedia.org/wiki/Process_group ). Es confuso que esto no se mencione en man 1 killo man bash, y podría considerarse un error en la documentación.
user001
111

Actualización: https://stackoverflow.com/a/53714583/302079 mejora esto al agregar el estado de salida y una función de limpieza.

trap "exit" INT TERM
trap "kill 0" EXIT

¿Por qué convertir INTy TERMsalir? Porque ambos deberían desencadenar elkill 0 sin entrar en un bucle infinito.

¿Por gatillo kill 0en EXIT? Debido a que las salidas de script normales deberían desencadenarsekill 0 .

¿Por qué kill 0? Porque las subcapas anidadas también deben ser eliminadas. Esto eliminará todo el árbol de procesos .

korkman
fuente
3
La única solución para mi caso en Debian.
MindlessRanger
3
Ni la respuesta de Johannes Schaub ni la proporcionada por tokland lograron eliminar los procesos en segundo plano que comenzó mi script de shell (en Debian). Esta solución funcionó. No sé por qué esta respuesta no está más votada. ¿Podría ampliar más sobre lo que kill 0significa / hace exactamente ?
josch
77
Esto es increíble, pero también mata a mi caparazón principal :-(
vidstige
55
Esta solución es literalmente exagerada. ¡kill 0 (dentro de mi script) arruinó toda mi sesión de X! Quizás en algunos casos matar 0 puede ser útil, pero esto no cambia el hecho de que no es una solución general y debe evitarse si es posible a menos que haya una buena razón para usarlo. ¡Sería bueno agregar una advertencia de que puede matar el shell principal o incluso la sesión X completa, no solo los trabajos en segundo plano de un script!
Lissanro Rayen
3
Si bien esta podría ser una solución interesante en algunas circunstancias, como lo señaló @vidstige, esto matará a todo el grupo de procesos que incluye el proceso de lanzamiento (es decir, el shell principal en la mayoría de los casos). Definitivamente no es algo que desea cuando ejecuta un script a través de un IDE.
madura
21

trampa 'kill $ (jobs -p)' EXIT

Solo haría cambios menores en la respuesta de Johannes y usaría jobs -pr para limitar la eliminación de los procesos en ejecución y agregar algunas señales más a la lista:

trap 'kill $(jobs -pr)' SIGINT SIGTERM EXIT
trazado de rayos
fuente
¿Por qué no matar también los trabajos detenidos? En Bash EXIT trap también se ejecutará en caso de SIGINT y SIGTERM, por lo que la trampa se llamaría dos veces en caso de tal señal.
jarno
14

La trap 'kill 0' SIGINT SIGTERM EXITsolución descrita en la respuesta de @tokland es realmente agradable, pero el último Bash se bloquea con una falla de segmentación cuando se usa. Esto se debe a que Bash, a partir de la versión 4.3, permite la recursividad de trampa, que en este caso se vuelve infinita:

  1. proceso de shell recibe SIGINTo SIGTERMo EXIT;
  2. la señal queda atrapada, ejecutándose kill 0, lo que envía SIGTERMa todos los procesos del grupo, incluido el propio shell;
  3. ir a 1 :)

Esto se puede solucionar eliminando manualmente el registro de la trampa:

trap 'trap - SIGTERM && kill 0' SIGINT SIGTERM EXIT

La forma más elegante, que permite imprimir la señal recibida y evita los mensajes "Terminado:":

#!/usr/bin/env bash

trap_with_arg() { # from https://stackoverflow.com/a/2183063/804678
  local func="$1"; shift
  for sig in "$@"; do
    trap "$func $sig" "$sig"
  done
}

stop() {
  trap - SIGINT EXIT
  printf '\n%s\n' "recieved $1, killing children"
  kill -s SIGINT 0
}

trap_with_arg 'stop' EXIT SIGINT SIGTERM SIGHUP

{ i=0; while (( ++i )); do sleep 0.5 && echo "a: $i"; done } &
{ i=0; while (( ++i )); do sleep 0.6 && echo "b: $i"; done } &

while true; do read; done

UPD : ejemplo mínimo agregado; stopfunción mejorada para evitar señales no necesarias y ocultar mensajes "Terminados:" de la salida. ¡Gracias Trevor Boyd Smith por las sugerencias!

skozin
fuente
en stop()usted proporciona el primer argumento como el número de señal pero luego codifica las señales que se están desregistrando. en lugar de codificar las señales que se están desregistrando, podría usar el primer argumento para anular el registro en la stop()función (hacerlo podría detener otras señales recursivas (que no sean las 3 codificadas)).
Trevor Boyd Smith
@TrevorBoydSmith, supongo que esto no funcionaría como se esperaba. Por ejemplo, el caparazón puede ser eliminado con SIGINT, pero kill 0envía SIGTERM, que quedará atrapado una vez más. Sin embargo, esto no producirá una recursión infinita, ya SIGTERMque se eliminará durante la segunda stopllamada.
skozin
Probablemente, trap - $1 && kill -s $1 0debería funcionar mejor. Probaré y actualizaré esta respuesta. Gracias por la buena idea! :)
skozin
No, trap - $1 && kill -s $1 0tampoco funcionará, ya que no podemos matar EXIT. Pero es realmente suficiente destrabar TERM, porque killenvía esta señal por defecto.
skozin
Probé la recursión con EXIT, el trapcontrolador de señal siempre se ejecuta solo una vez.
Trevor Boyd Smith
9

Para estar seguro, creo que es mejor definir una función de limpieza y llamarla desde la trampa:

cleanup() {
        local pids=$(jobs -pr)
        [ -n "$pids" ] && kill $pids
}
trap "cleanup" INT QUIT TERM EXIT [...]

o evitando la función por completo:

trap '[ -n "$(jobs -pr)" ] && kill $(jobs -pr)' INT QUIT TERM EXIT [...]

¿Por qué? Debido a que por el simple uso trap 'kill $(jobs -pr)' [...]se supone que no va a ser trabajos en segundo plano que se ejecutan cuando se señaliza la condición trampa. Cuando no hay trabajos, verá el siguiente mensaje (o similar):

kill: usage: kill [-s sigspec | -n signum | -sigspec] pid | jobspec ... or kill -l [sigspec]

porque jobs -prestá vacío: terminé en esa 'trampa' (juego de palabras).

tdaitx
fuente
Este caso de prueba [ -n "$(jobs -pr)" ]no funciona en mi bash. Uso GNU bash, versión 4.2.46 (2) -release (x86_64-redhat-linux-gnu). El mensaje "kill: use" sigue apareciendo.
Douwe van der Leest
Sospecho que tiene que ver con el hecho de que jobs -prno devuelve los PID de los hijos de los procesos en segundo plano. No destruye todo el árbol de procesos, solo corta las raíces.
Douwe van der Leest
2

Una buena versión que funciona bajo Linux, BSD y MacOS X. Primero intenta enviar SIGTERM, y si no tiene éxito, mata el proceso después de 10 segundos.

KillJobs() {
    for job in $(jobs -p); do
            kill -s SIGTERM $job > /dev/null 2>&1 || (sleep 10 && kill -9 $job > /dev/null 2>&1 &)

    done
}

TrapQuit() {
    # Whatever you need to clean here
    KillJobs
}

trap TrapQuit EXIT

Tenga en cuenta que los trabajos no incluyen procesos de nietos.

Orsiris de Jong
fuente
2
function cleanup_func {
    sleep 0.5
    echo cleanup
}

trap "exit \$exit_code" INT TERM
trap "exit_code=\$?; cleanup_func; kill 0" EXIT

# exit 1
# exit 0

Como https://stackoverflow.com/a/22644006/10082476 , pero con código de salida agregado

Delaware
fuente
¿De dónde exit_codeviene en la INT TERMtrampa?
jarno
1

Otra opción es que el script se establezca como el líder del grupo de procesos y atrape un killpg en su grupo de procesos al salir.

orip
fuente
¿Cómo se configura el proceso como líder del grupo de procesos? ¿Qué es "killpg"?
jarno
0

Entonces script la carga del script. Ejecute un killallcomando (o lo que esté disponible en su sistema operativo) que se ejecute tan pronto como finalice el script.

Oli
fuente
0

jobs -p no funciona en todos los shells si se llama en un sub-shell, posiblemente a menos que su salida se redirija a un archivo pero no a una tubería. (Supongo que originalmente estaba destinado solo para uso interactivo).

¿Qué pasa con lo siguiente:

trap 'while kill %% 2>/dev/null; do jobs > /dev/null; done' INT TERM EXIT [...]

La llamada a "trabajos" es necesaria con el shell dash de Debian, que no puede actualizar el trabajo actual ("%%") si falta.

michaeljt
fuente
Hmm enfoque interesante, pero no parece funcionar. Considere scipt trap 'echo in trap; set -x; trap - TERM EXIT; while kill %% 2>/dev/null; do jobs > /dev/null; done; set +x' INT TERM EXIT; sleep 100 & while true; do printf .; sleep 1; doneSi lo ejecuta en Bash (5.0.3) e intenta terminar, parece que hay un bucle infinito. Sin embargo, si lo termina de nuevo, funciona. Incluso con Dash (0.5.10.2-6) tienes que terminarlo dos veces.
jarno hace
0

Hice una adaptación de la respuesta de @tokland combinada con el conocimiento de http://veithen.github.io/2014/11/16/sigterm-propagation.html cuando noté que eso trapno se dispara si estoy ejecutando un proceso en primer plano (sin antecedentes &):

#!/bin/bash

# killable-shell.sh: Kills itself and all children (the whole process group) when killed.
# Adapted from http://stackoverflow.com/a/2173421 and http://veithen.github.io/2014/11/16/sigterm-propagation.html
# Note: Does not work (and cannot work) when the shell itself is killed with SIGKILL, for then the trap is not triggered.
trap "trap - SIGTERM && echo 'Caught SIGTERM, sending SIGTERM to process group' && kill -- -$$" SIGINT SIGTERM EXIT

echo $@
"$@" &
PID=$!
wait $PID
trap - SIGINT SIGTERM EXIT
wait $PID

Ejemplo de trabajo:

$ bash killable-shell.sh sleep 100
sleep 100
^Z
[1]  + 31568 suspended  bash killable-shell.sh sleep 100

$ ps aux | grep "sleep"
niklas   31568  0.0  0.0  19640  1440 pts/18   T    01:30   0:00 bash killable-shell.sh sleep 100
niklas   31569  0.0  0.0  14404   616 pts/18   T    01:30   0:00 sleep 100
niklas   31605  0.0  0.0  18956   936 pts/18   S+   01:30   0:00 grep --color=auto sleep

$ bg
[1]  + 31568 continued  bash killable-shell.sh sleep 100

$ kill 31568
Caught SIGTERM, sending SIGTERM to process group
[1]  + 31568 terminated  bash killable-shell.sh sleep 100

$ ps aux | grep "sleep"
niklas   31717  0.0  0.0  18956   936 pts/18   S+   01:31   0:00 grep --color=auto sleep
nh2
fuente
0

Solo por diversidad, publicaré una variación de https://stackoverflow.com/a/2173421/102484 , porque esa solución lleva al mensaje "Terminado" en mi entorno:

trap 'test -z "$intrap" && export intrap=1 && kill -- -$$' SIGINT SIGTERM EXIT
noonex
fuente