Cómo usar señales de Linux en scripts Bash

Publicado: 2022-08-09
Computadora portátil Linux que muestra un indicador de bash
fatmawati achmad zaenuri/Shutterstock.com

El kernel de Linux envía señales a los procesos sobre los eventos a los que deben reaccionar. Los scripts que se comportan bien manejan las señales con elegancia y solidez y pueden limpiarse por sí solos incluso si presiona Ctrl+C. Así es cómo.

Señales y Procesos

Las señales son mensajes breves, rápidos y unidireccionales que se envían a procesos como scripts, programas y demonios. Le informan al proceso sobre algo que ha sucedido. Es posible que el usuario haya presionado Ctrl+C o que la aplicación haya intentado escribir en la memoria a la que no tiene acceso.

Si el autor del proceso anticipó que se le podría enviar una determinada señal, puede escribir una rutina en el programa o script para manejar esa señal. Esta rutina se denomina manejador de señales . Atrapa o atrapa la señal y realiza alguna acción en respuesta a ella.

Cómo administrar procesos desde la terminal de Linux: 10 comandos que necesita saber
RELACIONADO Cómo administrar procesos desde la terminal de Linux: 10 comandos que necesita saber

Linux usa muchas señales, como veremos, pero desde el punto de vista de las secuencias de comandos, solo hay un pequeño subconjunto de señales que probablemente le interesen. En particular, en las secuencias de comandos no triviales, las señales que indican la la secuencia de comandos para apagar debe quedar atrapada (siempre que sea posible) y se debe realizar un apagado correcto.

Por ejemplo, los scripts que crean archivos temporales o abren puertos de firewall pueden tener la oportunidad de eliminar los archivos temporales o cerrar los puertos antes de que se apaguen. Si el script simplemente muere en el instante en que recibe la señal, su computadora puede quedar en un estado impredecible.

Así es como puede manejar las señales en sus propios scripts.

Conoce las Señales

Algunos comandos de Linux tienen nombres crípticos. No así el mando que atrapa señales. Se llama trap . También podemos usar trap con la opción -l (lista) para mostrarnos la lista completa de señales que usa Linux.

 trampa -l 

Listado de señales en Ubuntu con trap -l

Aunque nuestra lista numerada termina en 64, en realidad hay 62 señales. Faltan las señales 32 y 33. No están implementados en Linux. Han sido reemplazados por funcionalidad en el compilador gcc para manejar subprocesos en tiempo real. Todo, desde la señal 34, SIGRTMIN , hasta la señal 64, SIGRTMAX , son señales en tiempo real.

Verá diferentes listas en diferentes sistemas operativos similares a Unix. En OpenIndiana, por ejemplo, las señales 32 y 33 están presentes, junto con un montón de señales adicionales, lo que eleva el total a 73.

Listado de señales en OpenIndiana con trap -l

Las señales se pueden referenciar por nombre, número o por su nombre abreviado. Su nombre abreviado es simplemente su nombre sin el "SIG" inicial.

Las señales se emiten por muchas razones diferentes. Si puedes descifrarlos, su propósito está contenido en su nombre. El impacto de una señal cae en una de las siguientes categorías:

  • Terminar: El proceso se termina.
  • Ignorar: La señal no afecta el proceso. Esta es una señal de solo información.
  • Núcleo: se crea un archivo de volcado de núcleo. Esto generalmente se hace porque el proceso ha transgredido de alguna manera, como una violación de la memoria.
  • Detener: El proceso se detiene. Es decir, está en pausa , no terminado.
  • Continuar: le dice a un proceso detenido que continúe la ejecución.

Estas son las señales que encontrará con más frecuencia.

  • SIGHUP : señal 1. La conexión a un host remoto, como un servidor SSH, se interrumpió inesperadamente o el usuario cerró la sesión. Una secuencia de comandos que recibe esta señal puede terminar correctamente o puede optar por intentar volver a conectarse al host remoto.
  • SIGINT : Señal 2. El usuario ha presionado la combinación Ctrl+C para forzar el cierre de un proceso, o se ha utilizado el comando kill con la señal 2. Técnicamente, esta es una señal de interrupción, no una señal de terminación, sino un script interrumpido sin un controlador de señal generalmente terminará.
  • SIGQUIT : Señal 3. El usuario ha presionado la combinación Ctrl+D para forzar la salida de un proceso, o se ha utilizado el comando kill con la señal 3.
  • SIGFPE : Señal 8. El proceso intentó realizar una operación matemática ilegal (imposible), como la división por cero.
  • SIGKILL : Señal 9. Esta es la señal equivalente a una guillotina. No puedes atraparlo o ignorarlo, y sucede instantáneamente. El proceso se termina inmediatamente.
  • SIGTERM : Señal 15. Esta es la versión más considerada de SIGKILL . SIGTERM también le dice a un proceso que finalice, pero puede quedar atrapado y el proceso puede ejecutar sus procesos de limpieza antes de cerrarse. Esto permite un apagado elegante. Esta es la señal predeterminada emitida por el comando kill .

Señales en la línea de comando

Una forma de atrapar una señal es usar trap con el número o nombre de la señal y una respuesta que desea que suceda si se recibe la señal. Podemos demostrar esto en una ventana de terminal.

Este comando atrapa la señal SIGINT . La respuesta es imprimir una línea de texto en la ventana del terminal. Estamos usando la opción -e (habilitar escapes) con echo para que podamos usar el especificador de formato " \n ".

 trap 'echo -e "\nCtrl+c Detectado."' SIGINT 

Trapping Ctrl+C en la línea de comando

Nuestra línea de texto se imprime cada vez que pulsamos la combinación Ctrl+C.

Para ver si hay una trampa en una señal, use la opción -p (imprimir trampa).

 trampa -p SIGINT 

Comprobación de si hay una trampa en una señal

Usar trap sin opciones hace lo mismo.

Para restablecer la señal a su estado normal sin atrapar, use un guión " - " y el nombre de la señal atrapada.

 trampa - SIGINT
 trampa -p SIGINT 

Eliminar una trampa de una señal

Si no hay salida del comando trap -p , indica que no hay una trampa configurada en esa señal.

Señales de reventado en secuencias de comandos

Podemos usar el mismo comando trap de formato general dentro de un script. Este script atrapa tres señales diferentes, SIGINT , SIGQUIT y SIGTERM .

 #!/bin/bash

trap "echo I was SIGINT terminado; exit" SIGINT
trampa "echo estaba terminado con SIGQUIT; salir" SIGQUIT
trap "echo I was SIGTERM terminado; exit" SIGTERM

eco $$
contador=0

mientras que es cierto
hacer 
  echo "Número de bucle:" $((++contador))
  dormir 1
hecho

Las tres declaraciones de trap están en la parte superior del script. Tenga en cuenta que hemos incluido el comando de exit dentro de la respuesta a cada una de las señales. Esto significa que el script reacciona a la señal y luego sale.

Copie el texto en su editor y guárdelo en un archivo llamado "simple-loop.sh", y hágalo ejecutable usando el comando chmod . Deberá hacer eso con todos los scripts de este artículo si desea seguirlos en su propia computadora. Simplemente use el nombre del script apropiado en cada caso.

 chmod +x bucle-simple.sh 

Haciendo un script ejecutable con chmod

El resto del guión es muy simple. Necesitamos saber el ID de proceso del script, por lo que el script nos lo hace eco. La variable $$ contiene el ID de proceso del script.

Creamos una variable llamada counter y la ponemos a cero.

El bucle while se ejecutará para siempre a menos que se detenga a la fuerza. Incrementa la variable del counter , la repite en la pantalla y duerme por un segundo.

Ejecutemos el script y enviemos diferentes señales.

 ./bucle-simple.sh 

Un script que lo identifica ha sido terminado con Ctrl+C

Cuando presionamos "Ctrl + C", nuestro mensaje se imprime en la ventana del terminal y el script finaliza.

Ejecutémoslo de nuevo y enviemos la señal SIGQUIT usando el comando kill . Tendremos que hacerlo desde otra ventana de terminal. Tendrá que usar el ID de proceso informado por su propia secuencia de comandos.

 ./bucle-simple.sh
 matar -SIGQUIT 4575 

Un script que lo identifica ha sido terminado con SIGQUIT

Como se esperaba, el script informa que la señal llega y luego termina. Y finalmente, para probar el punto, lo haremos nuevamente con la señal SIGTERM .

 ./bucle-simple.sh
 matar -SIGTERM 4584 

Un script que lo identifica ha sido terminado con SIGTERM

Hemos verificado que podemos atrapar múltiples señales en un script y reaccionar a cada una de ellas de forma independiente. El paso que promueve todo esto de interesante a útil es agregar controladores de señal.

Manejo de señales en scripts

Podemos reemplazar la cadena de respuesta con el nombre de una función en su script. El comando trap luego llama a esa función cuando se detecta la señal.

Copie este texto en un editor y guárdelo como un archivo llamado "grace.sh", y hágalo ejecutable con chmod .

 #!/bin/bash

trap graceful_shutdown SIGINT SIGQUIT SIGTERM

cierre_agraciado()
{
  echo -e "\nEliminando archivo temporal:" $temp_file
  rm -rf "$archivo_temp"
  salida
}

archivo_temp=$(mktemp -p /tmp tmp.XXXXXXXXXX)
echo "Archivo temporal creado:" $temp_file

contador=0

mientras que es cierto
hacer 
  echo "Número de bucle:" $((++contador))
  dormir 1
hecho

El script establece una trampa para tres señales diferentes: SIGHUP , SIGINT y SIGTERM , utilizando una sola declaración de trap . La respuesta es el nombre de la función graceful_shutdown() . La función se llama cada vez que se recibe una de las tres señales atrapadas.

El script crea un archivo temporal en el directorio “/tmp”, usando mktemp . La plantilla de nombre de archivo es "tmp.XXXXXXXXXX", por lo que el nombre del archivo será "tmp". seguido de diez caracteres alfanuméricos aleatorios. El nombre del archivo se repite en la pantalla.

El resto del script es igual al anterior, con una variable counter y un ciclo while infinito.

 ./gracia.sh 

Una secuencia de comandos que realiza un cierre correcto al eliminar un archivo temporal

Cuando se envía una señal al archivo que hace que se cierre, se llama a la función graceful_shutdown() . Esto elimina nuestro único archivo temporal. En una situación del mundo real, podría realizar cualquier limpieza que requiera su secuencia de comandos.

Además, agrupamos todas nuestras señales atrapadas y las manejamos con una sola función. Puede atrapar señales individualmente y enviarlas a sus propias funciones de controlador dedicadas.

Copie este texto y guárdelo en un archivo llamado “triple.sh”, y hágalo ejecutable usando el comando chmod .

 #!/bin/bash

trampa sigint_handler SIGINT
trampa sigusr1_handler SIGUSR1
trampa exit_handler SALIR

función sigint_handler() {
  ((++sigint_count))

  echo -e "\nSIGINT recibió $sigint_count time(s)".

  if [[ "$sigint_count" -eq 3 ]]; después
    echo "Iniciando cierre".
    bucle_bandera=1
  fi
}

función sigusr1_handler() {
  echo "SIGUSR1 envió y recibió $((++sigusr1_count)) tiempo(s)."
}

función exit_handler() { 
  echo "Controlador de salida: el script se está cerrando..."
}

eco $$
sigusr1_count=0
sigint_count=0
bucle_bandera=0

while [[ $loop_flag -eq 0 ]]; hacer
  matar -SIGUSR1 $$
  dormir 1
hecho

Definimos tres trampas en la parte superior del script.

  • Uno atrapa SIGINT y tiene un controlador llamado sigint_handler() .
  • El segundo atrapa una señal llamada SIGUSR1 y usa un controlador llamado sigusr1_handler() .
  • La trampa número tres atrapa la señal de EXIT . Esta señal la emite el propio script cuando se cierra. Establecer un controlador de señal para EXIT significa que puede establecer una función que siempre se llamará cuando finalice el script (a menos que se elimine con la señal SIGKILL ). Nuestro controlador se llama exit_handler() .

SIGUSR1 y SIGUSR2 son señales proporcionadas para que pueda enviar señales personalizadas a sus scripts. La forma en que los interprete y reaccione ante ellos depende totalmente de usted.

Dejando a un lado los controladores de señales por ahora, el cuerpo del script debería resultarle familiar. Reproduce el ID del proceso en la ventana del terminal y crea algunas variables. La variable sigusr1_count registra la cantidad de veces que se manejó SIGUSR1 y sigint_count registra la cantidad de veces que se manejó SIGINT . La variable loop_flag se establece en cero.

El bucle while no es un bucle infinito. Detendrá el bucle si la variable loop_flag se establece en cualquier valor distinto de cero. Cada giro del bucle while usa kill para enviar la señal SIGUSR1 a este script, enviándola al ID de proceso del script. ¡Los scripts pueden enviarse señales a sí mismos!

La función sigusr1_handler() incrementa la variable sigusr1_count y envía un mensaje a la ventana del terminal.

Cada vez que se recibe la señal SIGINT , la función siguint_handler() incrementa la variable sigint_count y repite su valor en la ventana del terminal.

Si la variable sigint_count es igual a tres, la variable loop_flag se establece en uno y se envía un mensaje a la ventana del terminal para informar al usuario que el proceso de apagado ha comenzado.

Debido a que loop_flag ya no es igual a cero, el ciclo while termina y el script finaliza. Pero esa acción genera automáticamente la señal EXIT y se llama a la función exit_handler() .

 ./triple.sh 

Una secuencia de comandos que utiliza SIGUSR1, que requiere tres combinaciones de Ctrl+C para cerrarse y captar la señal EXIT al apagar

Después de presionar tres veces Ctrl+C, el script finaliza e invoca automáticamente la función exit_handler() .

Leer las señales

Al atrapar señales y manejarlas en funciones de controlador sencillas, puede hacer que sus scripts de Bash se ordenen detrás de sí mismos, incluso si se terminan inesperadamente. Eso le da un sistema de archivos más limpio. También evita la inestabilidad la próxima vez que ejecute la secuencia de comandos y, según cuál sea el propósito de la secuencia de comandos, incluso podría evitar brechas de seguridad.

RELACIONADO: Cómo auditar la seguridad de su sistema Linux con Lynis