Como te comenté en el capítulo anterior, las redirecciones te permiten controlar el flujo de datos de tus scripts. Pero, ¿qué pasa cuando tu script necesita reaccionar a eventos externos? ¿Cuando alguien pulsa Ctrl+C, cuando cierras la sesión, o cuando otro proceso le pide que haga algo?
Las señales
Las señales son el mecanismo fundamental de notificación asíncrona en Linux. Cuando pulsas Ctrl+C en tu terminal, el sistema no mata tu proceso mágicamente: envía una señal (SIGINT) al proceso en primer plano. Cuando cierras la sesión SSH, el sistema envía SIGHUP. Cuando tu script escribe en un pipe roto, recibe SIGPIPE.
Cada señal es un número entero pequeño. Tu script puede ignorarla, capturarla (ejecutando código propio) o dejarla actuar con su comportamiento por defecto (que suele ser terminar el proceso).
El comando trap es la herramienta que Bash te da para instalar manejadores de señales funciones que se ejecutan cuando una señal concreta llega. Sin trap, tu script es un pasajero pasivo: las señales pasan y el shell reacciona con el comportamiento por defecto. Con trap, tú controlas qué ocurre.
Este capítulo te lleva desde los fundamentos de las señales hasta patrones profesionales: demonios que recargan configuración sin reiniciar, scripts que limpian archivos temporales incluso si los matan, coordinación entre procesos con señales de usuario, y timeouts implementados con señales reales.
Concepto clave: las señales no son una abstracción de Bash. Son un mecanismo real del kernel de Linux. Cuando usas trap, estás llamando a la llamada al sistema sigaction() bajo el capó. Lo que aprendas aquí te sirve para cualquier lenguaje que maneje señales en Unix.
Las señales esenciales
No necesitas memorizar las 30+ señales de Linux. Hay un puñado que usarás el 99% del tiempo:
- SIGINT (2): Capturable. Interrupción desde teclado (Ctrl+C). Sirve para preguntar confirmación o limpiar antes de terminar.
- SIGTERM (15): Capturable. Petición educada de terminación (señal por defecto de
kill). - SIGHUP (1): Capturable. Originalmente «colgado de terminal». Hoy se usa para recarga de configuración sin reiniciar.
- SIGUSR1 (10): Capturable. Definida por el usuario. Ideal para rotar logs, acciones personalizadas.
- SIGUSR2 (12): Capturable. Definida por el usuario. Ideal para mostrar estadísticas.
- SIGQUIT (3): Capturable. Terminación forzada con core dump (Ctrl+\).
- SIGPIPE (13): Capturable. Escribir en pipe roto. A menudo se ignora.
- SIGALRM (14): Capturable. Timeout o alarma temporizada.
- SIGKILL (9): No capturable. Último recurso. Mata siempre.
- SIGSTOP (19): No capturable. Pausa forzosa del proceso.
Las más importantes para tus scripts son SIGINT, SIGTERM y SIGHUP. Con esas tres cubres el 90% de los casos.
Sintaxis de trap
La sintaxis básica de trap tiene tres formas:
# Capturar: ejecuta 'código' cuando llegue la señal
trap 'código_a_ejecutar' SEÑAL
# Ignorar: la señal no hace nada
trap '' SEÑAL
# Restablecer: vuelve al comportamiento por defecto
trap - SEÑAL
El código a ejecutar puede ser una cadena con comandos o, mucho mejor, el nombre de una función:
#!/bin/bash
limpiar() {
echo "[$$] Limpiando recursos..."
rm -f /tmp/mi_script_*.tmp
echo "[$$] Recursos liberados"
}
trap limpiar SIGTERM SIGINT
El shell ejecuta el código del trap después de que el comando actual termine (normalmente), no interrumpiendo el comando a mitad.
Puedes capturar múltiples señales con el mismo manejador o manejadores diferentes:
trap 'echo "Adiós mundo cruel"' SIGINT SIGTERM
trap 'recargar_config' SIGHUP
trap 'mostrar_estado' SIGUSR1
trap 'echo "Signal SIGPIPE ignorada" >&2' SIGPIPE
trap EXIT: Limpieza garantizada al salir
trap ... EXIT es, con diferencia, el trap más usado. Se ejecuta siempre que el script termina: tanto si sale con exit 0, con exit 1, por una señal no capturada, o simplemente porque llegó al final.
#!/bin/bash
set -euo pipefail
ARCHIVO_TEMP=$(mktemp /tmp/miscript_XXXXXX)
LOCKFILE="/tmp/miscript.lock"
cleanup() {
local exit_code=$?
echo "[$(date)] Limpiando. Código de salida: $exit_code"
rm -f "$ARCHIVO_TEMP" "$LOCKFILE"
}
trap cleanup EXIT
# Crear lock
echo "$$" > "$LOCKFILE"
# Cuerpo del script
echo "Procesando..."
# ... trabajo ...
echo "Hecho"
¿Por qué EXIT y no SIGTERM o SIGINT?
EXIT se ejecuta incluso si el script termina por una señal no capturada. Si pones un manejador solo para SIGTERM y el script recibe SIGQUIT, el manejador no se ejecuta. Pero EXIT se ejecuta siempre. Por eso el patrón profesional es:
trap 'limpiar_exit' EXIT
trap 'echo "Recibida señal..."; exit 1' SIGINT SIGTERM SIGHUP
El manejador de EXIT garantiza la limpieza. Los manejadores específicos de señal piden salida ordenada y dejan que EXIT haga la limpieza real.
Capturar el código de salida en EXIT
trap 'echo "Salgo con código $?"' EXIT
La variable $? dentro del trap de EXIT contiene el código de salida del script.
trap ERR: Captura de errores profunda
trap ERR se ejecuta cuando un comando devuelve un código distinto de cero. Pero hay matices:
- Se ejecuta solo si el comando NO es la condición de un
if,while,until, o está en una lista&&/||— en esos casos no se ejecuta el trap ERR. Bash asume que ya estás manejando el error. - No se ejecuta dentro de un subshell a menos que tenga su propio trap ERR.
Trap ERR con pila de llamadas
#!/bin/bash
set -euo pipefail
trap_error_handler() {
local line=$1
local cmd=$2
local code=$3
echo "[ERROR] Línea $line: '$cmd' falló con código $code" >&2
echo "[ERROR] Función: ${FUNCNAME[1]:-principal}" >&2
echo "[ERROR] Script: $0" >&2
}
trap 'trap_error_handler $LINENO "$BASH_COMMAND" $?' ERR
funcion_que_falla() {
ls /no/existe
}
funcion_que_falla
echo "Esto no se ejecuta por el set -e"
Salida:
[ERROR] Línea 14: 'ls /no/existe' falló con código 2
[ERROR] Función: funcion_que_falla
[ERROR] Script: ./test.sh
Modo verbose en trap ERR
DEBUG=${DEBUG:-false}
if [[ "$DEBUG" == "true" ]]; then
trap 'echo "[ERR] $BASH_COMMAND (línea $LINENO)" >&2' ERR
fi
trap DEBUG: Depuración quirúrgica
DEBUG con pila completa
#!/bin/bash
trap 'echo "[DEBUG] Ln $LINENO | Func: ${FUNCNAME[@]:-principal} | Cmd: $BASH_COMMAND"' DEBUG
func_a() {
local x=10
func_b
}
func_b() {
echo "Hola"
}
func_a
Esto muestra no solo la línea y el comando, sino toda la pila de llamadas.
DEBUG condicional por tramos
Puedes activar y desactivar trap DEBUG dinámicamente:
#!/bin/bash
debug_on() {
trap 'echo "[DEBUG] $LINENO: $BASH_COMMAND"' DEBUG
}
debug_off() {
trap '' DEBUG
}
echo "Sección sin debug..."
debug_on
echo "Sección con debug"
x=$(( 2 + 2 ))
echo "x = $x"
debug_off
echo "Vuelta a modo normal..."
trap RETURN: Rastreo de funciones
trap ... RETURN se ejecuta cada vez que una función retorna. Es ideal para logging de entrada/salida de funciones:
#!/bin/bash
trap 'echo "[RETURN] Saliendo de ${FUNCNAME[0]} (línea $LINENO)"' RETURN
saludar() {
echo "Hola"
}
sumar() {
local total=$(( $1 + $2 ))
return $total
}
saludar
sumar 5 3
echo "Hecho"
Cada return dispara el trap, incluyendo el retorno implícito al final del script. Puedes usar esto para construir un perfilador de llamadas:
#!/bin/bash
declare -A TIEMPOS
INICIO=$(date +%s%N)
trap 'TIEMPOS["${FUNCNAME[1]:-main}"]=$(( $(date +%s%N) - INICIO ))' RETURN
func1() { sleep 0.2; }
func2() { sleep 0.3; }
func1
func2
for f in "${!TIEMPOS[@]}"; do
echo "$f: $(( TIEMPOS[$f] / 1000000 ))ms"
done
Ignorar señales (trap » SIGNAL)
Puedes hacer que tu script ignore una señal completamente con una cadena vacía:
#!/bin/bash
# Ignora Ctrl+C durante una operación crítica
trap '' SIGINT
echo "Operación crítica en marcha — Ctrl+C no funciona"
sleep 5
# Restablece Ctrl+C al terminar
trap - SIGINT
echo "Ya puedes usar Ctrl+C de nuevo"
sleep 5
¿Para qué sirve ignorar señales? Para operaciones atómicas (escribir un archivo de configuración sin que te interrumpan), bloqueo de cancelación (mientras adquieres un lock), o sesiones de red (mientras envías datos críticos).
No puedes ignorar SIGKILL (9) ni SIGSTOP (19). El kernel no permite capturarlas ni ignorarlas. Esa es su función: ser el último recurso.
Restablecer señales (trap – SIGNAL)
Para devolver una señal a su comportamiento por defecto, usas trap - SEÑAL:
#!/bin/bash
# Capturo SIGINT para hacer limpieza
trap 'echo "Limpiando..."; rm -f /tmp/*.tmp; exit 1' SIGINT
sleep 10 &
PID=$!
wait $PID
# Ya no necesito capturar SIGINT, restablezco
trap - SIGINT
echo "A partir de aquí Ctrl+C mata directamente"
sleep 10
Propagación de señales en subshells
Una de las confusiones más comunes: los subshells heredan los traps del shell padre, pero con independencia.
#!/bin/bash
trap 'echo "Padre: recibí SIGINT"' SIGINT
# Este subshell NO hereda el trap
(
echo "Subshell ejecutándose..."
sleep 5
echo "Subshell terminó"
) &
En este ejemplo, el subshell en background no hereda los traps del padre. Si quieres que un subshell tenga un manejador, debes instalarlo dentro de él.
Señales en procesos hijos vs. padre
Cuando haces comando & y luego matas al padre, los hijos en background no reciben la señal automáticamente. Para matarlos con el padre, necesitas un grupo de procesos:
#!/bin/bash
set -euo pipefail
trap 'echo "Terminando grupo..."; kill 0; exit' SIGINT SIGTERM
# Lanzar hijos en el mismo grupo de procesos
{
while true; do echo "Hijo 1 trabajando..."; sleep 2; done
} &
{
while true; do echo "Hijo 2 trabajando..."; sleep 3; done
} &
echo "Padre esperando. PID: $$"
wait
El comando kill 0 envía la señal a todos los procesos del mismo grupo.
El comando kill
El comando kill envía señales a procesos. Su nombre es engañoso: no solo mata, puede enviar cualquier señal.
# Sintaxis básica
kill [-SEÑAL] PID
# Enviar SIGTERM (la señal por defecto)
kill 1234
# Enviar señal específica
kill -SIGTERM 1234
kill -TERM 1234
kill -15 1234
# Las tres formas anteriores son equivalentes
Objetivos especiales
kill -TERM 0 # Envía a TODO el grupo de procesos actual
kill -TERM -1 # Envía a todos los procesos que puedas (¡cuidado!)
kill -TERM -1234 # Envía a todo el grupo de procesos 1234
kill con nombres de proceso
# Matar por nombre
kill $(pgrep -f "mi_demonio")
pkill -f "mi_demonio" # Más directo
killall mi_demonio # Mata todas las instancias
Verificar si un proceso existe
if kill -0 1234 2>/dev/null; then
echo "El proceso 1234 existe"
else
echo "El proceso 1234 NO existe"
fi
kill -0 no envía señal real; solo comprueba si el proceso existe y tienes permiso para señalarle.
Trap en funciones: ámbito y alcance
Los traps instalados dentro de una función afectan a todo el script, no solo a la función. Esto es diferente al ámbito de las variables locales:
#!/bin/bash
critico() {
# Esto NO es local — afecta a todo el script
trap '' SIGINT
echo "Sección crítica..."
sleep 5
trap - SIGINT # Restablece
}
echo "Ctrl+C funciona aquí"
critico
echo "Y aquí también"
Patrón seguro: guardar y restaurar
Para evitar efectos laterales, guarda el estado anterior del trap y restáuralo:
#!/bin/bash
funcion_con_su_propio_trap() {
local old_trap
old_trap=$(trap -p SIGINT) # Guardar estado actual
trap 'echo "Trap local en función"' SIGINT
echo "Dentro de la función..."
sleep 3
eval "$old_trap" # Restaurar estado anterior
}
trap 'echo "Trap global"' SIGINT
echo "Trap global activo"
funcion_con_su_propio_trap
echo "Trap global de nuevo"
Coordinación entre scripts con SIGUSR1/SIGUSR2
Una de las aplicaciones más potentes de las señales es la coordinación entre procesos. Con SIGUSR1 y SIGUSR2 puedes construir sistemas de señalización sencillos sin necesidad de colas de mensajes.
Procesador con rotación de logs
Imagina un script que procesa archivos continuamente. Un script de monitorización externo le dice «rota el log» (SIGUSR1) o «muestra estadísticas» (SIGUSR2).
#!/bin/bash
# procesador.sh — Demonio de procesamiento
PID_FILE="/tmp/procesador.pid"
CONTADOR=0
ROTAR_LOG=false
rotar_log() {
ROTAR_LOG=true
echo "[$$] Recibida orden de rotar log"
}
mostrar_estadisticas() {
echo "=== ESTADÍSTICAS ==="
echo "Archivos procesados: $CONTADOR"
echo "PID: $$"
echo "Uptime: $(ps -o etime= -p $$)"
echo "===================="
}
trap rotar_log SIGUSR1
trap mostrar_estadisticas SIGUSR2
trap 'echo "[$$] Terminando..."; exit 0' SIGTERM SIGINT
echo "$$" > "$PID_FILE"
echo "Procesador iniciado (PID: $$)"
while true; do
((CONTADOR++))
echo "Procesando archivo $CONTADOR..."
if $ROTAR_LOG; then
echo "[$(date)] Rotando log..."
ROTAR_LOG=false
fi
sleep 2
done
Y el script que lo controla:
#!/bin/bash
# controlador.sh
PID_FILE="/tmp/procesador.pid"
if [[ ! -f "$PID_FILE" ]]; then
echo "El procesador no está en ejecución" >&2
exit 1
fi
PID=$(cat "$PID_FILE")
case "${1:-}" in
rotar)
echo "Solicitando rotación de log..."
kill -SIGUSR1 "$PID"
;;
stats)
echo "Solicitando estadísticas..."
kill -SIGUSR2 "$PID"
;;
stop)
echo "Deteniendo procesador..."
kill -SIGTERM "$PID"
;;
*)
echo "Uso: $0 {rotar|stats|stop}"
exit 1
;;
esac
Este patrón de script + controlador es la base de muchos sistemas de servicios en producción.
Patrón timeout con señal
Una aplicación clásica de SIGALRM es implementar un timeout para comandos que podrían colgarse:
#!/bin/bash
timeout_exec() {
local timeout=$1
local cmd=("${@:2}")
(
# Lanzar el comando en background
"${cmd[@]}" &
CMD_PID=$!
# Sleep en paralelo como "alarma"
(
sleep "$timeout"
kill -TERM "$CMD_PID" 2>/dev/null
) &
SLEEP_PID=$!
# Esperar al comando
wait "$CMD_PID" 2>/dev/null
CMD_EXIT=$?
# Matar el sleep si el comando terminó antes
kill "$SLEEP_PID" 2>/dev/null
exit "$CMD_EXIT"
)
}
Uso:
if timeout_exec 5 ping -c 1 google.com; then
echo "Ping exitoso"
else
echo "Ping timeout o falló"
fi
SIGHUP y recarga de configuración
La señal SIGHUP es la señal tradicional de «recarga tu configuración». Los demonios profesionales (nginx, sshd) capturan SIGHUP para releer sus archivos de configuración sin reiniciar.
#!/bin/bash
# servidor_config.sh — Demonio que recarga configuración con SIGHUP
CONFIG_FILE="servidor.conf"
declare -A CONFIG
PID_FILE="/tmp/servidor_config.pid"
RUNNING=true
cargar_configuracion() {
source "$CONFIG_FILE" 2>/dev/null || {
echo "[ERROR] No se pudo cargar $CONFIG_FILE" >&2
return 1
}
echo "[$(date)] Configuración cargada: PUERTO=$PUERTO, LOG_LEVEL=$LOG_LEVEL, MAX_CONN=$MAX_CONN"
}
recargar_config() {
echo "[$(date)] Recibido SIGHUP — Recargando configuración..."
cargar_configuracion
}
terminar() {
echo "[$(date)] Terminando servidor..."
RUNNING=false
}
trap recargar_config SIGHUP
trap terminar SIGTERM SIGINT
trap 'rm -f "$PID_FILE"' EXIT
# Configuración por defecto
PUERTO=8080
LOG_LEVEL="info"
MAX_CONN=100
cargar_configuracion
echo "$$" > "$PID_FILE"
echo "Servidor iniciado (PID: $$)"
echo "Recarga: kill -SIGHUP $$"
while $RUNNING; do
echo "[$(date)] Procesando en puerto $PUERTO (level=$LOG_LEVEL, max=$MAX_CONN)..."
sleep 3
done
Y el archivo de configuración (servidor.conf):
# servidor.conf
PUERTO=9090
LOG_LEVEL="debug"
MAX_CONN=200
Para probarlo:
chmod +x servidor_config.sh
./servidor_config.sh &
# Edita servidor.conf cambiando valores
kill -SIGHUP $(cat /tmp/servidor_config.pid)
kill -SIGTERM $(cat /tmp/servidor_config.pid)
Patrón demonio con manejo de señales
Los demonios en Bash requieren un conjunto completo de prácticas de señalización para ser fiables ante cualquier interrupción. Te recomiendo usar esta plantilla como base:
#!/bin/bash
# plantilla_demonio.sh — Plantilla de demonio profesional
set -euo pipefail
NOMBRE="$(basename "$0")"
PID_FILE="/tmp/${NOMBRE}.pid"
LOG_FILE="/tmp/${NOMBRE}.log"
RUNNING=true
PAUSA=5
CONTADOR=0
log() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}
limpiar_pid() {
rm -f "$PID_FILE"
}
recargar() {
log "Recibida señal de recarga (SIGHUP)"
log "Configuración recargada"
}
mostrar_estado() {
log "=== ESTADO ==="
log "PID: $$"
log "Contador: $CONTADOR"
log "Tiempo activo: $(ps -o etime= -p $$ 2>/dev/null || echo 'N/A')"
log "=============="
}
terminar() {
log "Recibida señal de terminación (${1:-})"
RUNNING=false
}
# Configurar manejadores
trap 'terminar SIGTERM' SIGTERM
trap 'terminar SIGINT' SIGINT
trap 'terminar SIGQUIT' SIGQUIT
trap recargar SIGHUP
trap mostrar_estado SIGUSR1
trap 'log "Debug activado" ; set -x' SIGUSR2
trap limpiar_pid EXIT
# Crear PID file
echo "$$" > "$PID_FILE"
log "Demonio $NOMBRE iniciado (PID: $$)"
# Bucle principal
while $RUNNING; do
((CONTADOR++))
log "Iteración $CONTADOR — trabajando..."
# Simular trabajo
sleep "$PAUSA"
done
log "Demonio finalizado normalmente (total iteraciones: $CONTADOR)"
Limpieza de archivos temporales y locks con trap EXIT
El patrón definitivo para scripts que crean archivos temporales, locks y recursos efímeros:
#!/bin/bash
# limpieza_segura.sh
set -euo pipefail
# Arrays para rastrear recursos creados
declare -a ARCHIVOS_TEMP
declare -a DIRECTORIOS_TEMP
declare -a LOCKS_CREADOS
crear_temp() {
local tmp
tmp=$(mktemp /tmp/miscript_XXXXXX)
ARCHIVOS_TEMP+=("$tmp")
echo "$tmp"
}
crear_dir_temp() {
local dir
dir=$(mktemp -d /tmp/miscript_dir_XXXXXX)
DIRECTORIOS_TEMP+=("$dir")
echo "$dir"
}
adquirir_lock() {
local lockfile=$1
if ! mkdir "$lockfile" 2>/dev/null; then
echo "Error: no se pudo adquirir lock $lockfile (ya existe PID $(cat "$lockfile/pid" 2>/dev/null))" >&2
return 1
fi
echo "$$" > "$lockfile/pid"
LOCKS_CREADOS+=("$lockfile")
echo "$lockfile"
}
cleanup_completo() {
local exit_code=$?
echo "[$(date)] Iniciando limpieza global (código de salida: $exit_code)..."
# Liberar locks
for lock in "${LOCKS_CREADOS[@]}"; do
if [[ -d "$lock" ]]; then
rm -rf "$lock"
echo " Lock liberado: $lock"
fi
done
# Borrar directorios temporales
for dir in "${DIRECTORIOS_TEMP[@]}"; do
if [[ -d "$dir" ]]; then
rm -rf "$dir"
echo " Directorio temporal borrado: $dir"
fi
done
# Borrar archivos temporales
for tmp in "${ARCHIVOS_TEMP[@]}"; do
if [[ -f "$tmp" ]]; then
rm -f "$tmp"
echo " Archivo temporal borrado: $tmp"
fi
done
echo "[$(date)] Limpieza completada"
exit "$exit_code"
}
trap cleanup_completo EXIT
trap 'echo "Señal recibida — saliendo ordenadamente..."; exit 1' SIGINT SIGTERM SIGHUP
# === USO ===
LOCK=$(adquirir_lock "/tmp/miscript.lock")
TEMP1=$(crear_temp)
TEMP2=$(crear_temp)
DIR1=$(crear_dir_temp)
echo "Recursos creados:"
echo " Lock: $LOCK"
echo " Temp1: $TEMP1"
echo " Temp2: $TEMP2"
echo " Dir: $DIR1"
# Simular trabajo
echo "Escribiendo datos temporales..."
echo "datos importantes" > "$TEMP1"
echo "más datos" > "$TEMP2"
# Simular error
if (( RANDOM % 3 == 0 )); then
echo "Error simulado..."
exit 1
fi
echo "Script completado con éxito"
Este patrón es a prueba de balas: tanto si el script termina bien, mal, o lo matan con SIGTERM/SIGINT/SIGHUP, los recursos se limpian.
Funciones profesionales
setup_cleanup: Registro centralizado de limpieza
# Registra un callback de limpieza que se ejecuta al salir
declare -a _CLEANUP_HOOKS
setup_cleanup() {
local hook=$1
_CLEANUP_HOOKS+=("$hook")
}
_run_cleanup_hooks() {
local exit_code=$?
for hook in "${_CLEANUP_HOOKS[@]}"; do
$hook "$exit_code" 2>/dev/null || true
done
}
trap _run_cleanup_hooks EXIT
# Uso:
# setup_cleanup 'rm -f /tmp/mi_archivo'
# setup_cleanup 'my_cleanup_function'
trap_error_handler: Manejador de errores completo
trap_error_handler() {
local line=$1
local cmd=$2
local code=$3
local func="${FUNCNAME[1]:-main}"
echo "[ERROR] $(date)" >&2
echo "[ERROR] Script: $0" >&2
echo "[ERROR] Línea: $line" >&2
echo "[ERROR] Función: $func" >&2
echo "[ERROR] Comando: $cmd" >&2
echo "[ERROR] Código: $code" >&2
echo "[ERROR] Pila: ${FUNCNAME[@]}" >&2
}
trap 'trap_error_handler $LINENO "$BASH_COMMAND" $?' ERR
signal_handler: Instalación segura de múltiples señales
signal_handler() {
local handler=$1
shift
local signal
for signal in "$@"; do
trap "$handler" "$signal"
done
}
# Uso:
# signal_handler 'echo "Adiós"; exit' SIGINT SIGTERM SIGQUIT
# signal_handler recargar_config SIGHUP
timeout_exec: Ejecución con timeout
timeout_exec() {
local timeout=$1
shift
local -a cmd=("$@")
local exit_code=0
(
"${cmd[@]}" &
local pid=$!
trap 'kill "$pid" 2>/dev/null; exit 124' SIGALRM
(sleep "$timeout" && kill -ALRM $$ 2>/dev/null) &
wait "$pid" 2>/dev/null
exit $?
)
return $?
}
# Uso:
# if timeout_exec 5 ping -c 1 8.8.8.8; then
# echo "Respuesta recibida"
# else
# echo "Timeout"
# fi
Script completo: servicio_monitoreo.sh
Este script implementa un demonio de monitoreo completo que maneja SIGTERM (terminación ordenada), SIGHUP (recarga de configuración), SIGUSR1 (generar informe inmediato), SIGUSR2 (rotar log) y usa trap EXIT para limpieza garantizada.
#!/bin/bash
# servicio_monitoreo.sh — Demonio de monitoreo con manejo completo de señales
set -euo pipefail
# === Configuración por defecto ===
NOMBRE="servicio_monitoreo"
DIR_MONITOREO="/tmp/monitor"
LOG_DIR="/var/log/${NOMBRE}"
LOG_FILE="${LOG_DIR}/${NOMBRE}.log"
PID_FILE="/tmp/${NOMBRE}.pid"
CONFIG_FILE="/etc/${NOMBRE}.conf"
INTERVALO=5
MAX_ARCHIVOS=100
DEBUG=false
# === Variables de estado ===
RUNNING=true
CONTADOR_PROCESADOS=0
CONTADOR_ERRORES=0
ARCHIVOS_ENCONTRADOS=0
FECHA_INICIO=$(date '+%Y-%m-%d %H:%M:%S')
ROTAR_LOG=false
declare -a ARCHIVOS_PROCESADOS
# === Funciones ===
log() {
local nivel=$1
shift
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [${nivel}] [PID:$$] $*" >> "$LOG_FILE"
if [[ "$nivel" == "ERROR" ]] || [[ "${DEBUG:-false}" == "true" ]]; then
echo "[${nivel}] $*" >&2
fi
}
cargar_configuracion() {
if [[ -f "$CONFIG_FILE" ]]; then
source "$CONFIG_FILE"
log "INFO" "Configuración cargada: INTERVALO=$INTERVALO, MAX_ARCHIVOS=$MAX_ARCHIVOS, DEBUG=$DEBUG"
else
log "WARN" "Archivo de configuración $CONFIG_FILE no encontrado. Usando valores por defecto."
fi
}
inicializar_directorios() {
mkdir -p "$DIR_MONITOREO" "$LOG_DIR"
}
limpiar_recursos() {
local exit_code=$?
log "INFO" "Iniciando limpieza (código de salida: $exit_code)..."
if [[ -f "$PID_FILE" ]]; then
rm -f "$PID_FILE"
log "INFO" "PID file eliminado"
fi
if $ROTAR_LOG; then
log "INFO" "Rotación de log pendiente completada"
fi
log "INFO" "Limpieza completada. Archivos procesados: $CONTADOR_PROCESADOS"
}
generar_informe() {
local tiempo_activo
tiempo_activo=$(ps -o etime= -p $$ 2>/dev/null || echo "N/A")
{
echo "=========================================="
echo "INFORME DEL SERVICIO DE MONITOREO"
echo "=========================================="
echo "PID: $$"
echo "Inicio: $FECHA_INICIO"
echo "Tiempo activo: $tiempo_activo"
echo "Archivos evaluados: $ARCHIVOS_ENCONTRADOS"
echo "Archivos procesados: $CONTADOR_PROCESADOS"
echo "Errores: $CONTADOR_ERRORES"
echo "Directorio monitor: $DIR_MONITOREO"
echo "Archivos recientes:"
for f in "${ARCHIVOS_PROCESADOS[@]: -5}"; do
echo " - $f"
done
echo "=========================================="
} | tee -a "$LOG_FILE"
}
procesar_archivo() {
local archivo=$1
local nombre_base
nombre_base=$(basename "$archivo")
log "INFO" "Procesando archivo: $nombre_base"
if wc -l "$archivo" > "/dev/null" 2>&1; then
local lineas
lineas=$(wc -l < "$archivo")
log "INFO" "Archivo $nombre_base procesado: $lineas líneas"
ARCHIVOS_PROCESADOS+=("$nombre_base")
return 0
else
log "ERROR" "No se pudo procesar $nombre_base"
return 1
fi
}
rotar_log_ahora() {
if [[ -f "$LOG_FILE" ]]; then
local backup="${LOG_FILE}.$(date '+%Y%m%d%H%M%S')"
cp "$LOG_FILE" "$backup"
: > "$LOG_FILE" # Vaciar el log original
log "INFO" "Log rotado: $backup"
fi
}
mostrar_estadisticas() {
generar_informe
}
recargar_config() {
log "INFO" "SIGHUP recibido — Recargando configuración"
cargar_configuracion
}
terminar() {
local signal=$1
log "INFO" "Señal $signal recibida. Finalizando servicio..."
RUNNING=false
}
tarea_periodica() {
# Esta función se ejecuta en cada iteración del bucle principal
log "DEBUG" "Explorando $DIR_MONITOREO..."
local encontrados=0
while IFS= read -r -d '' archivo; do
((ARCHIVOS_ENCONTRADOS++))
if (( ARCHIVOS_ENCONTRADOS > MAX_ARCHIVOS )); then
log "WARN" "Límite de archivos alcanzado ($MAX_ARCHIVOS)"
break
fi
if procesar_archivo "$archivo"; then
((CONTADOR_PROCESADOS++))
else
((CONTADOR_ERRORES++))
fi
encontrados=$(( encontrados + 1 ))
done < <(find "$DIR_MONITOREO" -maxdepth 1 -type f -name "*.txt" -print0 2>/dev/null)
if (( encontrados > 0 )); then
log "INFO" "Iteración completada: $encontrados archivos nuevos"
fi
if $ROTAR_LOG; then
rotar_log_ahora
ROTAR_LOG=false
fi
}
# === Configuración de señales ===
trap 'terminar SIGTERM' SIGTERM
trap 'terminar SIGINT' SIGINT
trap recargar_config SIGHUP
trap 'ROTAR_LOG=true' SIGUSR1
trap mostrar_estadisticas SIGUSR2
trap limpiar_recursos EXIT
# === Inicialización ===
# Cargar configuración
if [[ "$1" == "-c" && -n "${2:-}" ]]; then
CONFIG_FILE="$2"
shift 2
fi
cargar_configuracion
inicializar_directorios
# Verificar que no haya otra instancia
if [[ -f "$PID_FILE" ]]; then
local old_pid
old_pid=$(cat "$PID_FILE")
if kill -0 "$old_pid" 2>/dev/null; then
log "ERROR" "Ya hay una instancia ejecutándose (PID: $old_pid)"
exit 1
else
log "WARN" "PID file obsoleto eliminado (PID: $old_pid ya no existe)"
fi
fi
echo "$$" > "$PID_FILE"
log "INFO" "=== Servicio de Monitoreo iniciado ==="
log "INFO" "Directorio: $DIR_MONITOREO"
log "INFO" "Intervalo: ${INTERVALO}s"
log "INFO" "Log: $LOG_FILE"
log "INFO" "PID: $$"
log "INFO" "Comandos de control:"
log "INFO" " kill -SIGTERM $$ — Detener"
log "INFO" " kill -SIGHUP $$ — Recargar config"
log "INFO" " kill -SIGUSR1 $$ — Rotar log"
log "INFO" " kill -SIGUSR2 $$ — Estadísticas"
# Crear archivo de ejemplo para probar
echo "archivo de prueba" > "${DIR_MONITOREO}/ejemplo_inicial.txt"
# === Bucle principal ===
while $RUNNING; do
tarea_periodica
sleep "$INTERVALO"
done
log "INFO" "Servicio detenido. Total procesados: $CONTADOR_PROCESADOS, errores: $CONTADOR_ERRORES"
exit 0
Errores comunes
Error 1: Olvidar que EXIT se ejecuta SIEMPRE
# Doble mensaje de salida
trap 'echo "Terminado"' EXIT
trap 'echo "Terminado por señal"; exit 1' SIGTERM
# Un solo mensaje — EXIT maneja la limpieza, SIGTERM solo pide salida
trap 'limpiar; echo "Adiós"' EXIT
trap 'exit 1' SIGTERM SIGINT
Cuando pones exit dentro de un trap de señal, y también tienes un trap ... EXIT, ambos se ejecutan. No dupliques mensajes ni limpiezas.
Error 2: Asumir que los traps se heredan en subshells
# Este subshell no tiene el trap
trap 'echo "Adiós"' EXIT
( sleep 5; echo "Subshell termina sin el trap" )
Error 3: Usar variables globales sin proteger en señales
# Posible condición de carrera
CONTADOR=0
trap '((CONTADOR++))' SIGUSR1
# Si SIGUSR1 llega mientras se modifica CONTADOR en otro lugar, hay race condition
Error 4: No restablecer traps después de ignorar una señal
# Ignoras SIGINT para toda la ejecución
trap '' SIGINT
# ... mucho código ...
# Olvidas restablecer: Ctrl+C nunca funciona
# Bloque seguro
trap '' SIGINT
# sección crítica
trap - SIGINT # Restableces
Error 5: Capturar SIGKILL o SIGSTOP
# No funciona: SIGKILL no se puede capturar
trap 'echo "No me mates"' SIGKILL
trap '' SIGKILL
kill -9 $$ # Mata igualmente
Error 6: Asumir que kill -9 es lo mismo que kill -15
kill 1234 # envía SIGTERM — el proceso puede capturarlo y limpiar
kill -9 1234 # SIGKILL — no puede capturarse, el proceso muere al instante sin limpiar
Siempre prefiere kill (SIGTERM) primero. Guarda SIGKILL como último recurso.
Error 7: No manejar SIGPIPE en tuberías largas
# Si el pipe se rompe, el script puede morir silenciosamente
comando_producir_datos | head -n 5
# comando_producir_datos recibe SIGPIPE cuando head cierra el pipe
# Manejar SIGPIPE
trap '' SIGPIPE # O capturarlo y registrar
Error 8: No usar kill -0 para verificar existencia de procesos
# Propenso a errores si el PID se recicla
if ps -p "$PID" > /dev/null 2>&1; then
echo "Proceso existe"
fi
# kill -0 es más eficiente y correcto
if kill -0 "$PID" 2>/dev/null; then
echo "Proceso existe"
fi
Error 9: Hacer demasiado dentro del manejador de señal
# Demasiado código en el manejador
trap 'rm -rf /tmp/*; sync; kill 0; logger "Adiós"; exit 1' SIGTERM
# Mínimo: solo cambiar una flag
RUNNING=true
trap 'RUNNING=false' SIGTERM
# El bucle principal comprueba la flag y hace la limpieza real
Los manejadores de señal deberían ser rápidos y simples. No hagas E/S pesada ni operaciones largas dentro del trap. Usa una flag para señalizar y maneja la lógica en el bucle principal.
Tabla resumen de señales
- SIGINT (2): Capturable. Capturar Ctrl+C para preguntar confirmación o limpiar.
- SIGTERM (15): Capturable. Petición de terminación educada (por defecto en
kill). - SIGHUP (1): Capturable. Recarga de configuración sin reiniciar.
- SIGUSR1 (10): Capturable. Señal de usuario libre (rotar log, acción personalizada).
- SIGUSR2 (12): Capturable. Señal de usuario libre (estadísticas, acción personalizada).
- SIGQUIT (3): Capturable. Terminación forzada con core dump (Ctrl+\).
- SIGPIPE (13): Capturable. Escribir en pipe roto.
- SIGALRM (14): Capturable. Timeout / alarma temporizada.
- SIGKILL (9): No capturable. Último recurso.
- SIGSTOP (19): No capturable. Pausa forzosa.
Tabla resumen de tipos de trap
trap 'código' SEÑAL: Cuando llega la señal. Manejadores personalizados.trap 'código' EXIT: Cuando el script termina (por cualquier razón). Limpieza garantizada.trap 'código' ERR: Cuando un comando devuelve error (excepto en if/while/until/&&/||). Captura de errores.trap 'código' DEBUG: Antes de cada comando. Depuración paso a paso.trap 'código' RETURN: Cuando una función retorna. Perfilado, logging de funciones.trap '' SEÑAL: Nunca (ignora la señal). Secciones críticas no interrumpibles.trap - SEÑAL: Comportamiento por defecto. Restablecer señal tras ignorarla.
Conclusión
Las señales y los traps son el sistema nervioso de tus scripts. Sin ellos, tu script es una caja negra que solo sabe ejecutar código de principio a fin. Con ellos, tu script reacciona al entorno se limpia cuando lo matan, recarga configuración sin reiniciar, se coordina con otros procesos, y respeta al usuario que pulsa Ctrl+C.
Principios clave:
- EXIT es tu red de seguridad: instala siempre un
trap ... EXITpara limpiar recursos. Cubre todos los casos de salida. - Señales específicas para flujo: usa SIGTERM para detener, SIGHUP para recargar, SIGUSR1/SIGUSR2 para acciones personalizadas.
- Manejadores ligeros: el código del trap debe ser mínimo. Usa una flag booleana y maneja la lógica real en el bucle principal.
- SIGKILL no se captura: no intentes
trap 'algo' SIGKILL. No funciona. - Guarda y restaura traps: si cambias un trap dentro de una función, guarda el estado anterior con
trap -p SEÑALy restáuralo al salir. - Los subshells no heredan traps: instala traps explícitamente dentro de subshells si los necesitas.
- Usa
kill -0para comprobar si un proceso existe sin enviarle señal.
Las señales transforman tu script de un programa secuencial a un sistema reactivo. El salto cualitativo es enorme: dejas de escribir scripts que «se ejecutan y terminan» y empiezas a escribir procesos que «viven, responden y se comunican». Eso es lo que separa un script avanzado de uno profesional.
En el próximo capítulo veremos cómo consumir APIs REST desde Bash. Te enseñaré a consumir servicios web, procesar JSON y automatizar interacciones con tus servicios favoritos directamente desde el shell.
Más información.
Páginas de manual
- signal(7) — Lista completa de señales en Linux
- sigaction(2) — Llamada al sistema subyacente a trap
- kill(1) — El comando kill
- bash(1) — SIGNALS — Sección de señales en el manual de Bash