Estoy tratando de manejar SIGINT
( CtrlC) de tal manera que si un usuario presiona accidentalmente ctrl-c, aparecerá un mensaje: "¿Desea salir? (s/n)". Si ingresa sí, entonces salga del script. Si no, continúe desde donde ocurrió la interrupción. Básicamente, necesito CtrlCtrabajar de manera similar a SIGTSTP
( CtrlZ) pero de una manera ligeramente diferente. He probado varias formas de lograr esto pero no obtuve los resultados esperados. A continuación se muestran algunos escenarios que probé.
Caso 1
Guion:play.sh
#!/bin/sh
function stop()
{
while true; do
read -rep $'\nDo you wish to stop playing?(y/n)' yn
case $yn in
[Yy]* ) echo "Thanks for playing !!!"; exit 1;;
[Nn]* ) break;;
* ) echo "Please answer (y/n)";;
esac
done
}
trap 'stop' SIGINT
echo "going to sleep"
for i in {1..100}
do
echo "$i"
sleep 3
done
echo "end of sleep"
Cuando ejecuto el script anterior, obtengo los resultados esperados.
Producción:
$ play.sh
going to sleep
1
^C
Do you wish to stop playing?(y/n)y
Thanks for playing !!!
$ play.sh
going to sleep
1
2
^C
Do you wish to stop playing?(y/n)n
3
4
^C
Do you wish to stop playing?(y/n)y
Thanks for playing !!!
$
Caso: 2
Moví el for
bucle a un nuevo script loop.sh
, por lo que play.sh
se convierte en el proceso padre y loop.sh
el proceso hijo.
Guion:play.sh
#!/bin/sh
function stop()
{
while true; do
read -rep $'\nDo you wish to stop playing?(y/n)' yn
case $yn in
[Yy]* ) echo "Thanks for playing !!!"; exit 1;;
[Nn]* ) break;;
* ) echo "Please answer (y/n)";;
esac
done
}
trap 'stop' SIGINT
loop.sh
Guion:loop.sh
#!/bin/sh
echo "going to sleep"
for i in {1..100}
do
echo "$i"
sleep 3
done
echo "end of sleep"
El resultado en este caso no es el esperado.
Producción:
$ play.sh
going to sleep
1
2
^C
Do you wish to stop playing?(y/n)y
Thanks for playing !!!
$ play.sh
going to sleep
1
2
3
4
^C
Do you wish to stop playing?(y/n)n
$
Entiendo que cuando un proceso recibe una SIGINT
señal, la propaga a todos los procesos secundarios, por lo que mi segundo caso falla. ¿Hay alguna manera de evitar que SIGINT
se propague a procesos secundarios y así hacer que el loop.sh
trabajo funcione exactamente como funcionó en el primer caso?
Nota: Este es sólo un ejemplo de mi aplicación real. La aplicación en la que estoy trabajando tiene varios scripts secundarios en play.sh
y loop.sh
. Debo asegurarme de que la aplicación, al recibirla SIGINT
, no finalice, sino que muestre al usuario un mensaje.
Respuesta1
¡Gran pregunta clásica sobre la gestión de trabajos y señales con buenos ejemplos! He desarrollado un script de prueba simplificado para centrarme en la mecánica del manejo de la señal.
Para lograr esto, después de iniciar los hijos (loop.sh) en segundo plano, llame wait
y al recibir la señal INT, kill
el grupo de procesos cuyo PGID es igual a su PID.
Para el script en cuestión,
play.sh
esto se puede lograr de la siguiente manera:En la
stop()
función reemplazarexit 1
conkill -TERM -$$ # note the dash, negative PID, kills the process group
Comience
loop.sh
como un proceso en segundo plano (aquí se pueden iniciar y administrar múltiples procesos en segundo planoplay.sh
)loop.sh &
Agregue
wait
al final del guión esperar a todos los niños.wait
Cuando su secuencia de comandos inicia un proceso, ese hijo se convierte en miembro de un grupo de procesos con PGID igual al PID del proceso principal que se encuentra $$
en el shell principal.
Por ejemplo, el script trap.sh
inició tres sleep
procesos en segundo plano y ahora wait
los está ejecutando; observe que la columna de ID del grupo de procesos (PGID) es la misma que el PID del proceso principal:
PID PGID STAT COMMAND
17121 17121 T sh trap.sh
17122 17121 T sleep 600
17123 17121 T sleep 600
17124 17121 T sleep 600
En Unix y Linux, puede enviar una señal a cada proceso en ese grupo de procesos llamando kill
con el valor negativo de PGID
. Si proporciona kill
un número negativo, se utilizará como -PGID. Dado que el PID () del script $$
es el mismo que el PGID, puede crear kill
su grupo de procesos en el shell con
kill -TERM -$$ # note the dash before $$
debe proporcionar un número o nombre de señal; de lo contrario, algunas implementaciones de kill le indicarán "opción ilegal" o "especificación de señal no válida".
El código simple a continuación ilustra todo esto. Establece un trap
controlador de señales, genera 3 hijos, luego entra en un ciclo de espera sin fin, esperando suprimirse mediante el kill
comando del grupo de procesos en el controlador de señales.
$ cat trap.sh
#!/bin/sh
signal_handler() {
echo
read -p 'Interrupt: ignore? (y/n) [Y] >' answer
case $answer in
[nN])
kill -TERM -$$ # negative PID, kill process group
;;
esac
}
trap signal_handler INT
for i in 1 2 3
do
sleep 600 &
done
wait # don't exit until process group is killed or all children die
Aquí hay una ejecución de muestra:
$ ps -o pid,pgid,stat,args
PID PGID STAT COMMAND
8073 8073 Ss /bin/bash
17111 17111 R+ ps -o pid,pgid,stat,args
$
OK, no se están ejecutando procesos adicionales. Inicie el script de prueba, interrumpa ( ^C
), elija ignorar la interrupción y luego suspenda ( ^Z
):
$ sh trap.sh
^C
Interrupt: ignore? (y/n) [Y] >y
^Z
[1]+ Stopped sh trap.sh
$
Verifique los procesos en ejecución, anote los números de grupo de procesos ( PGID
):
$ ps -o pid,pgid,stat,args
PID PGID STAT COMMAND
8073 8073 Ss /bin/bash
17121 17121 T sh trap.sh
17122 17121 T sleep 600
17123 17121 T sleep 600
17124 17121 T sleep 600
17143 17143 R+ ps -o pid,pgid,stat,args
$
Traiga nuestro script de prueba al primer plano ( fg
) e interrumpa ( ^C
) nuevamente, esta vez elijanoignorar:
$ fg
sh trap.sh
^C
Interrupt: ignore? (y/n) [Y] >n
Terminated
$
Verifique los procesos en ejecución, no más dormir:
$ ps -o pid,pgid,stat,args
PID PGID STAT COMMAND
8073 8073 Ss /bin/bash
17159 17159 R+ ps -o pid,pgid,stat,args
$
Nota sobre su caparazón:
Tuve que modificar su código para que se ejecutara en mi sistema. Tiene #!/bin/sh
la primera línea en sus scripts, pero los scripts usan extensiones (de bash o zsh) que no están disponibles en /bin/sh.
Respuesta2
"Entiendo que cuando un proceso recibe una señal SIGINT, propaga la señal a todos los procesos hijos"
¿De dónde sacaste esta idea incorrecta?
$ perl -E '$SIG{INT}=sub { say "ouch $$" }; if (fork()) { say "parent $$"; sleep 3; kill 2, $$ } else { say "child $$"; sleep 99 }'
parent 25831
child 25832
ouch 25831
$
Si hubiera propagación, como se afirma, uno podría haber esperado un "ay" del proceso hijo.
En realidad:
"Cada vez que escribimos la tecla de interrupción de nuestro terminal (a menudo BORRAR o Control-C) o la tecla de salir (a menudo Control-barra invertida), esto provoca que la señal de interrupción o la señal de salida se envíe a todos los procesos en el grupo de procesos de primer plano" -- W .Richard Stevens. "Programación Avanzada en el Entorno UNIX®". Addison-Wesley. 1993. p.246.
Esto se puede observar a través de:
$ perl -E '$SIG{INT}=sub { say "ouch $$" }; fork(); sleep 99'
^Couch 25971
ouch 25972
$
Dado que todos los procesos del grupo de procesos de primer plano consumirán un SIGINT
del Control+C, deberá diseñar todos los procesos para manejar o ignorar adecuadamente esa señal, según corresponda, o posiblemente hacer que los subprocesos se conviertan en el nuevo grupo de procesos de primer plano para que el padre ( ej., el shell) no ve la señal, debido a que ya no está en el grupo de procesos en primer plano.
Respuesta3
En play.sh
la fuente el archivo de bucle es así:
source ./loop.sh
Una subcapa de la que se salió debido a una trampa no se puede volver a ella al salir de la trampa.
Respuesta4
El problema es sleep
.
Cuando ustedCTRL+C
matas a un sleep
- no a tush
(incluso si la sintaxis en tu #!/bin/sh
es un poco sospechosa). Sin embargo, todo el grupo de procesos recibe la señal, debido a que no instala su controlador loop.sh
, que es una subcapa, termina inmediatamente después.
Necesitas una trampa en loop.sh
. Las trampas se eliminan para cada subcapa iniciada a menos que se indique explícitamente.trap ''
SIG
ignorado por los padres.
También necesita una -m
supervisión controlada por el trabajo de su padre para que pueda realizar un seguimiento de sus hijos. wait
, por ejemplo, sólo funciona con control de tareas. -m
El modo monitor es cómo los shells interactúan con los terminales.
sh -cm ' . /dev/fd/3' 3<<"" # `-m`onitor is important
sh -c ' kill -INT "$$"'& # get the sig#
wait %%;INT=$? # keep it in $INT
trap " stty $(stty -g;stty -icanon)" EXIT # lose canonical input
loop()( trap exit INT # this is a child
while [ "$#" -le 100 ] # same as yours
do echo "$#" # writing the iterator
set "" "$@" # iterating
sleep 3 # sleeping
done
)
int(){ case $1 in # handler fn
($INT) printf "\33[2K\rDo you want to stop playing? "
case $(dd bs=1 count=1; echo >&3) in
([Nn]) return
esac; esac; exit "$1"
} 3>&2 >&2 2>/dev/null
while loop ||
int "$?"
do :; done
0
1
2
3
4
Do you want to stop playing? n
0
1
2
3
Do you want to stop playing? N
0
1
Do you want to stop playing? y
[mikeserv@desktop tmp]$