Escribí un script bash simple con un bucle para imprimir la fecha y hacer ping a una máquina remota:
#!/bin/bash
while true; do
# *** DATE: Thu Sep 17 10:17:50 CEST 2015 ***
echo -e "\n*** DATE:" `date` " ***";
echo "********************************************"
ping -c5 $1;
done
Cuando lo ejecuto desde una terminal, no puedo detenerlo con Ctrl+C .
Parece que envía el ^C a la terminal, pero el script no se detiene.
MacAir:~ tomas$ ping-tester.bash www.google.com
*** DATE: Thu Sep 17 23:58:42 CEST 2015 ***
********************************************
PING www.google.com (216.58.211.228): 56 data bytes
64 bytes from 216.58.211.228: icmp_seq=0 ttl=55 time=39.195 ms
64 bytes from 216.58.211.228: icmp_seq=1 ttl=55 time=37.759 ms
^C <= That is Ctrl+C press
--- www.google.com ping statistics ---
2 packets transmitted, 2 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 40.887/59.699/78.510/18.812 ms
*** DATE: Thu Sep 17 23:58:48 CEST 2015 ***
********************************************
PING www.google.com (216.58.211.196): 56 data bytes
64 bytes from 216.58.211.196: icmp_seq=0 ttl=55 time=37.460 ms
64 bytes from 216.58.211.196: icmp_seq=1 ttl=55 time=37.371 ms
No importa cuántas veces lo presione o qué tan rápido lo haga. No soy capaz de detenerlo.
Haz la prueba y date cuenta por ti mismo.
Como solución secundaria, lo detendré con Ctrl+Z , eso lo detiene y luego kill %1
.
¿Qué está pasando exactamente aquí con ^C? ?
Respuesta aceptada:
Lo que pasa es que tanto bash
y ping
recibir el SIGINT (bash
ser no interactivo, ambos ping
y bash
ejecutar en el mismo grupo de procesos que ha sido creado y configurado como el grupo de procesos de primer plano de la terminal por el shell interactivo desde el que ejecutó ese script).
Sin embargo, bash
maneja ese SIGINT de forma asincrónica, solo después de que el comando que se está ejecutando actualmente haya salido. bash
solo sale al recibir ese SIGINT si el comando que se está ejecutando muere debido a un SIGINT (es decir, su estado de salida indica que SIGINT lo ha eliminado).
$ bash -c 'sh -c "trap exit\ 0 INT; sleep 10; :"; echo here'
^Chere
Arriba, bash
, sh
y sleep
recibo SIGINT cuando presiono Ctrl-C, pero sh
sale normalmente con un código de salida 0, entonces bash
ignora el SIGINT, por lo que vemos "aquí".
ping
, al menos el de iputils, se comporta así. Cuando se interrumpe, imprime estadísticas y sale con un estado de salida 0 o 1 dependiendo de si sus pings fueron respondidos o no. Entonces, cuando presiona Ctrl-C mientras ping
se está ejecutando, bash
nota que ha presionado Ctrl-C
en sus controladores SIGINT, pero desde ping
sale normalmente, bash
no sale.
Si agrega un sleep 1
en ese ciclo y presiona Ctrl-C
mientras sleep
se está ejecutando, porque sleep
no tiene un controlador especial en SIGINT, morirá e informará a bash
que murió de un SIGINT, y en ese caso bash
saldrá (en realidad se matará a sí mismo con SIGINT para informar la interrupción a su padre).
En cuanto a por qué bash
se comporta así, no estoy seguro y observo que el comportamiento no siempre es determinista. Acabo de hacer la pregunta en bash
lista de correo de desarrollo (Actualizar :@Jilles ahora ha precisado el motivo en su respuesta).
El único otro shell que encontré que se comporta de manera similar es ksh93 (Actualización, como lo menciona @Jilles, también lo hace FreeBSD sh
). Allí, SIGINT parece ser claramente ignorado. Y ksh93
sale cada vez que SIGINT cancela un comando.
Obtienes el mismo comportamiento que bash
arriba pero también:
ksh -c 'sh -c "kill -INT \$\$"; echo test'
No muestra "prueba". Es decir, sale (suicidándose con SIGINT allí) si el comando que estaba esperando muere de SIGINT, incluso si él mismo no recibió ese SIGINT.
Una solución sería agregar un:
trap 'exit 130' INT
En la parte superior del script para forzar bash
para salir al recibir un SIGINT (tenga en cuenta que, en cualquier caso, SIGINT no se procesará sincrónicamente, solo después de que haya salido del comando que se está ejecutando actualmente).
Idealmente, nos gustaría informar a nuestros padres que morimos por un SIGINT (de modo que si es otro bash
script por ejemplo, que bash
el guión también se interrumpe). Haciendo una exit 130
no es lo mismo que morir de SIGINT (aunque algunos shells establecerán $?
al mismo valor para ambos casos), sin embargo, a menudo se usa para informar una muerte por SIGINT (en sistemas donde SIGINT es 2, que es la mayoría).
Sin embargo, para bash
, ksh93
o FreeBSD sh
, eso no funciona. Ese estado de salida 130 no es considerado como una muerte por SIGINT y una secuencia de comandos principal no cancelar allí.
Entonces, una alternativa posiblemente mejor sería suicidarnos con SIGINT al recibir SIGINT:
trap '
trap - INT # restore default INT handler
kill -s INT "$$"
' INT