¿Qué es una técnica confiable para eliminar procesos en segundo plano en la terminación de script?

6

Utilizo scripts de shell para reaccionar a los eventos del sistema y actualizar las pantallas de estado en mi administrador de ventanas. Por ejemplo, un script determina el estado actual de wifi escuchando múltiples fuentes:

  1. asociar / disociar eventos de wpa_supplicant
  2. la dirección cambia de ip (entonces sé cuando dhcpcd ha asignado una dirección)
  3. un proceso de temporizador (para que la intensidad de la señal se actualice de vez en cuando)

Para lograr la multiplexación, termino generando procesos en segundo plano:

{ wpa_cli -p /var/run/wpa_supplicant -i wlan0 -a echo &
ip monitor address &
while sleep 30; do echo; done } |
while read line; do update_wifi_status; done &

es decir, la configuración es que cada vez que cualquiera de las fuentes de eventos genera una línea, mi estado de wifi se actualiza. Toda la canalización se ejecuta en segundo plano (el '&' final) porque también veo otro origen de eventos que hace que mi script finalice:

wait_for_termination
kill $!

Se supone que kill elimina los procesos en segundo plano, pero de esta forma no hace el trabajo. Los procesos 'wpa_cli' e 'ip' siempre sobreviven, al menos, y tampoco mueren en su próximo evento (en teoría deberían obtener un SIGPIPE; supongo que el proceso de lectura también debe estar vivo).

La pregunta es, ¿cómo limpiar de manera confiable [y elegante!] Todos los procesos en segundo plano generados?

sqweek
fuente
1
Todo el material de cgroups es precisamente para manejar tales situaciones. Quizás serverwatch.com/tutorials/article.php/3920051/… y serverwatch.com/tutorials/article.php/3921001/… son de ayuda.
vonbrand
¡Gracias! Los enlaces se centran en el rendimiento del escritorio, pero con un poco más de investigación se hizo obvio cómo usar cgroups para este caso
semana

Respuestas:

7

La solución súper simple es agregar esto al final del script:

kill -- -$$

Explicación:

$$nos da el PID del shell en ejecución. Entonces, kill $$enviaría un SIGTERM al proceso de shell. Sin embargo, si negamos el PID, killenvía una SIGTERM a cada proceso en el grupo de procesos . Necesitamos la información --previa para killsaber que -$$es una ID de grupo de proceso y no una bandera.

¡Tenga en cuenta que esto depende de que el shell en ejecución sea un líder de grupo de procesos! De lo contrario, $$(el PID) no coincidirá con la ID del grupo de proceso, y terminará enviando una señal a quién sabe dónde (bueno, probablemente en ninguna parte, ya que es poco probable que haya un grupo de proceso con una ID coincidente si no somos un grupo líder).

Cuando se inicia el shell, crea un nuevo grupo de procesos [1]. Cada proceso bifurcado se convierte en miembro de ese grupo de procesos, a menos que cambien explícitamente su grupo de procesos a través de syscall ( setpgid).

La forma más sencilla de garantizar un script en particular se ejecuta como un líder de grupo del proceso es poner en marcha usando setsid. Por ejemplo, tengo algunos de estos scripts de estado que ejecuto desde un script principal:

#!/bin/sh
wifi_status &
bat_status &

Escrito de esta manera, los scripts de wifi y batería se ejecutan con el mismo grupo de procesos que el script principal, y kill -- -$$no funciona. La solución es:

#!/bin/sh
setsid wifi_status &
setsid bat_status &

Encontré pstree -p -gútil para visualizar el proceso y las ID de grupo de proceso.

Gracias a todos los que contribuyeron y me hicieron profundizar un poco, ¡aprendí cosas! :)

[1] ¿Hay otras circunstancias en las que el shell crea un grupo de procesos? p.ej. al iniciar una subshell? no lo sé...

sqweek
fuente
¿ kill -- -$$También se mata? Estoy interesado en hacer un guión que pueda matar a sus hijos pero no a sí mismo.
Craig McQueen
Sí, también se suicida. Si solo quieres matar a los niños, deberás realizar un seguimiento de sus PID después de lanzarlos (o, ver la otra respuesta que impulsa la construcción del shell jobs). Si los niños también pueden bifurcar, aún puede encontrar esta técnica útil. es decir. use setsidpara que cada proceso secundario se convierta en un líder de grupo y luego kill -- -$CHILDpara matar al menor y a sus descendientes.
semana de
2

OK, se me ocurrió una solución bastante decente que no usa cgroups. No funcionará frente a los procesos de bifurcación, como señaló Leonardo Dagnino.

Uno de los problemas con el seguimiento manual de las ID de proceso a través de $! matarlos más tarde es la condición de carrera inherente: si el proceso finaliza antes de matarlo, el script enviará una señal a un proceso inexistente o posiblemente incorrecto.

Podemos verificar la finalización del proceso dentro del shell a través del sistema de espera incorporado, pero solo podemos esperar la finalización de todos los procesos en segundo plano o de un solo pid. En ambos casos, espere bloques, lo que lo hace inadecuado para la tarea de verificar si un PID determinado aún se está ejecutando.

Al buscar una solución a lo anterior, me topé con el comando jobs , que anteriormente pensaba que solo estaba disponible para shells interactivos. Resulta que funciona bien sus scripts, y realiza un seguimiento automático de los procesos en segundo plano que hemos lanzado; si un proceso ha finalizado, ya no aparecerá en la lista de trabajos.

Por lo tanto, el comando:

trap 'kill $(jobs -p)' EXIT

es suficiente para garantizar, en casos simples, la finalización de los procesos en segundo plano cuando sale el shell actual.

En mi caso, uno no es suficiente, porque también estoy iniciando el proceso en segundo plano desde una subshell, y se eliminan las trampas para cada nueva subshell. Entonces, necesito hacer la misma trampa dentro de la subshell:

{ trap 'kill $(jobs -p)' EXIT
wpa_cli -p /var/run/wpa_supplicant -i wlan0 -a echo &
ip monitor address &
while echo; do sleep 30; done } |
while read line; do update_wifi_status; done &

Finalmente, jobs -p solo da el pid del último proceso en la tubería (¡como $!). Como puede ver, estoy generando procesos en segundo plano en el primer proceso de la canalización en segundo plano, por lo que también quiero señalar ese pid.

El primer proceso 'pid se puede obtener de los trabajos , pero no estoy seguro de cómo se puede lograr esto. Usando bash, obtengo resultados en este estilo:

$ sleep 20 | sleep 20 &
$ jobs -l
[1]+ 25180 Running                 sleep 20
     25181                       | sleep 20 &

Entonces, al usar un comando kill ligeramente modificado del script principal, puedo señalar todos los procesos en la tubería:

wait_for_termination
kill $(jobs -l |awk '$2 == "|" {print $1; next} {print $2}')
sqweek
fuente
1
Primero, jobs -pda el PID del primer proceso en una tubería, el líder del grupo de procesos, no el último proceso. En segundo lugar, también jobs -lenumera los PID de procesos eliminados o salidos, por lo que su posible condición de carrera está allí nuevamente. ¿Por qué no poner la tubería en un verdadero subshell, abierto con (...)? jobs -pdevuelve el PID de esta subshell. O use el pscual, en la columna PPID, enumera los PID principales (es decir, su $$). Tenga en cuenta también la $PPIDvariable Bash.
Andreas Spindler
1
Gracias por las correcciones! No estoy seguro de lo que ganaría al usar una subshell real: no me acerca más a los procesos en segundo plano que se inician a la izquierda de la tubería. Para ser honesto, mi queja principal pses la utilidad grotescamente hinchada que es GNU ps, que nunca he aprendido a usar bien para crear secuencias de comandos a favor de herramientas más sanas. Sin embargo, parece un buen enfoque ... Para mi caso, parece que pgrep -g $$me da exactamente lo que quiero (PID de todos los procesos que pertenecen al mismo grupo de procesos que $$) ... Simplemente no estoy 100 $ seguro de dónde el límite del "grupo de procesos" es.
semana
En realidad, creo que es aún más simple ... el shell llama setpgid()cuando se inicia para crear un nuevo grupo de procesos, y puede usar el antiguo normal killpara señalar un grupo de procesos usando un número negativo. Entonces ... kill -$$debería matar todas las tareas en segundo plano, siempre y cuando ninguna de ellas haya comenzado su propio grupo de procesos :)
sqweek
0

Puede obtener el PID de cada uno poniendo algo como

PID1=$!

después del wpa_cli,

PID2=$!

después de la ip, y al final del script los matas a ambos:

kill $PID1
kill $PID2

Sin embargo, eso no funcionará si los procesos se bifurcan. Entonces cgroups sería la mejor solución.

Leonardo Dagnino
fuente
Correcto. La complicación en este caso es que los procesos en cuestión se inician en un subshell, que es (a) en segundo plano y (b) parte de una tubería. Por lo tanto, no es trivial propagar las variables PID guardadas al shell principal (que es el único shell que sabe cuándo terminar). Estoy pensando que una construcción basada en "trap EXIT" me permitiría limpiar con elegancia los procesos en segundo plano iniciados dentro de la subshell, y luego solo necesito una forma confiable de señalizar la terminación de la subshell (que puede ser tan simple como kill $ !, no es seguro). Buen punto sobre los procesos de bifurcación.
semana de