El módulo signal permite reaccionar a eventos asíncronos enviados al proceso, como Ctrl+C, solicitudes de terminación del sistema, timers y notificaciones de procesos hijos. Herramientas de terminal, servidores y workers usan señales para liberar recursos y detenerse sin corromper datos.
Python aplica reglas importantes: los handlers se ejecutan posteriormente en el thread principal del intérprete principal, no dentro del handler nativo de bajo nivel. Además, solo el thread principal puede instalar nuevos handlers. Estas reglas determinan cómo coordinar un shutdown seguro.
Señales frecuentes
SIGINT suele venir de Ctrl+C y por defecto genera KeyboardInterrupt. SIGTERM es la solicitud habitual de cierre enviada por systemd, Docker, Kubernetes y kill. En Unix, SIGHUP puede indicar cierre del terminal o una petición de reload definida por la aplicación.
SIGKILL y SIGSTOP no pueden capturarse, bloquearse ni ignorarse. No existe oportunidad de limpieza después de SIGKILL.
Instalar un handler mínimo
import signal
solicitar_cierre = False
def pedir_cierre(signum, frame):
global solicitar_cierre
solicitar_cierre = True
signal.signal(signal.SIGINT, pedir_cierre)
if hasattr(signal, "SIGTERM"):
signal.signal(signal.SIGTERM, pedir_cierre)
El handler recibe el número y el frame actual. Mantén su trabajo mínimo: cambia una flag, escribe en un descriptor no bloqueante preparado o notifica un mecanismo de coordinación seguro.
No hagas limpieza pesada dentro del handler
Evita escribir archivos grandes, cerrar cientos de conexiones, adquirir locks o llamar servicios lentos directamente. La documentación advierte que primitivas como threading.Lock pueden producir deadlocks.
El handler debe notificar al flujo normal, que realizará la limpieza con orden y manejo de excepciones.
Loop con shutdown cooperativo
import signal
import time
cerrando = False
def pedir_cierre(signum, frame):
global cerrando
cerrando = True
signal.signal(signal.SIGINT, pedir_cierre)
if hasattr(signal, "SIGTERM"):
signal.signal(signal.SIGTERM, pedir_cierre)
while not cerrando:
procesar_siguiente_tarea()
time.sleep(0.2)
cerrar_recursos()
El loop revisa la flag entre unidades de trabajo. Las tareas largas deberían tener puntos de cancelación. Una función C extensa puede retrasar el handler hasta devolver control al intérprete.
Threads y señales
Aunque una señal llegue a otro thread, el handler Python ejecuta en el principal. Las señales no son un mecanismo de comunicación entre threads. Usa threading.Event, colas u otras primitivas.
import signal
import threading
evento_cierre = threading.Event()
def handler(signum, frame):
evento_cierre.set()
if hasattr(signal, "SIGTERM"):
signal.signal(signal.SIGTERM, handler)
Instala el handler desde el thread principal. Hacerlo desde un worker produce ValueError.
No bases toda la arquitectura en KeyboardInterrupt
Una excepción generada por señal puede aparecer después de casi cualquier instrucción de bytecode. Una aplicación compleja puede quedar interrumpida mientras actualiza estado o después de adquirir un recurso.
Un handler explícito para SIGINT que marca el shutdown permite terminar o cancelar trabajo, rechazar nuevas tareas y cerrar componentes de forma predecible.
Secuencia de cierre en servidores
Un servidor puede dejar de aceptar conexiones, esperar solicitudes activas dentro de un plazo, cancelar tareas restantes, cerrar pools, vaciar logs y salir.
El artículo de socketserver en Python explica que shutdown() debe llamarse desde otro thread cuando serve_forever() está activo. El handler debería notificar ese thread sin bloquearse.
SIGTERM en containers
Los orquestadores suelen enviar SIGTERM, esperar un grace period y después enviar SIGKILL. El servicio debe manejar SIGTERM, detener nuevas entradas y terminar la limpieza antes del plazo.
Usa el formato exec del comando del container para que Python reciba la señal directamente. Un shell intermedio puede no reenviarla correctamente.
SIGPIPE y BrokenPipeError
Python ignora SIGPIPE por defecto para convertir escrituras en pipes o sockets cerrados en BrokenPipeError. No restaures la disposición predeterminada solo para ocultar la excepción: una conexión interrumpida podría cerrar todo el proceso.
errno en Python explica EPIPE y otros fallos del sistema.
Timeouts con alarm()
En Unix, signal.alarm() programa SIGALRM después de segundos enteros. Solo existe un alarm por proceso; una llamada nueva sustituye la anterior.
import signal
class TiempoAgotado(TimeoutError):
pass
def timeout_handler(signum, frame):
raise TiempoAgotado("La operación superó el plazo")
signal.signal(signal.SIGALRM, timeout_handler)
signal.alarm(5)
try:
operacion_bloqueante()
finally:
signal.alarm(0)
Es específico de Unix, del thread principal y utiliza una excepción asíncrona. Prefiere el timeout nativo de sockets, clientes HTTP, bases de datos o subprocesses cuando exista.
Timers de mayor precisión
setitimer() acepta segundos fraccionarios e intervalos repetidos. ITIMER_REAL entrega SIGALRM; ITIMER_VIRTUAL mide CPU del proceso; ITIMER_PROF combina tiempo de proceso y kernel.
Los timers por señal son globales y pueden entrar en conflicto con bibliotecas. Documenta su propiedad y restaura valores anteriores.
Descubrir señales disponibles
import signal
for numero in sorted(signal.valid_signals(), key=int):
try:
nombre = signal.Signals(numero).name
descripcion = signal.strsignal(numero)
print(numero, nombre, descripcion)
except ValueError:
pass
El conjunto cambia por plataforma. Usa hasattr() y valid_signals() en vez de asumir que todas las señales Unix existen en Windows o WebAssembly.
Restaurar handlers anteriores
signal.signal() devuelve el handler previo. Una biblioteca que lo cambia temporalmente debería restaurarlo.
anterior = signal.signal(signal.SIGINT, handler)
try:
ejecutar_operacion()
finally:
signal.signal(signal.SIGINT, anterior)
Las bibliotecas reutilizables no deben sobrescribir permanentemente la política de la aplicación principal.
Despertar loops con set_wakeup_fd()
Un loop bloqueado en select o poll puede necesitar un descriptor legible al llegar una señal. set_wakeup_fd() escribe el número como un byte en un descriptor no bloqueante para señales con handler registrado.
import os
import signal
lectura, escritura = os.pipe()
os.set_blocking(lectura, False)
os.set_blocking(escritura, False)
signal.set_wakeup_fd(escritura)
if hasattr(signal, "SIGTERM"):
signal.signal(signal.SIGTERM, lambda signum, frame: None)
El loop debe drenar el descriptor. El buffer es finito, por lo que warn_on_full_buffer debe configurarse según si importan los bytes individuales. El siguiente artículo sobre select aplicará este patrón.
Bloquear y esperar señales en Unix
pthread_sigmask() cambia la máscara del thread. sigwait(), sigwaitinfo() y sigtimedwait() permiten que un thread dedicado acepte señales bloqueadas de forma síncrona.
La arquitectura puede simplificar servicios complejos: bloquea señales antes de iniciar workers y dedica un thread a convertirlas en eventos. Prueba la plataforma y bibliotecas reales.
No intentes recuperarte de SIGSEGV
Fallos síncronos como SIGSEGV, SIGBUS o SIGFPE causados por código nativo no pueden repararse con un handler Python normal. Al volver, probablemente se repite la instrucción inválida.
Usa faulthandler para diagnóstico. El artículo de ctypes en Python recomienda aislar bibliotecas inestables en subprocesses.
Subprocesos y grupos
Decide si la terminación alcanza solo al hijo directo o a todo el grupo. terminate() y kill() cambian entre Windows y Unix. Evita nietos huérfanos.
En Linux, pidfds reducen riesgos de reutilización de PID. No señales un PID reutilizado sin verificar identidad.
Manejo idempotente
El handler puede ejecutarse más de una vez. La primera señal inicia el cierre; las siguientes pueden reducir el plazo o ignorarse.
cerrando = False
def handler(signum, frame):
global cerrando
if cerrando:
return
cerrando = True
La limpieza también debe tolerar componentes ya cerrados.
Logs dentro del handler
El logging complejo puede adquirir locks. Registra el motivo desde el loop normal después de despertar. Mantén cualquier diagnóstico del handler al mínimo y pruébalo por plataforma.
Convertir señales en eventos normales
Una arquitectura robusta transforma la señal asíncrona en una transición ordinaria: el handler escribe al wakeup FD o cambia una flag, el selector despierta y el código normal ejecuta la máquina de estados de shutdown.
Para syscalls interrumpidas, consulta errno en Python. Para crashes y callbacks nativos, revisa ctypes en Python.
Pruebas recomendadas
Prueba SIGINT y SIGTERM durante espera, procesamiento, escritura y cierre; señales repetidas; una función C larga; registro fuera del thread principal; ausencia de señales Unix en Windows; wakeup buffer lleno; cancelación de alarm; grupos de procesos y grace period del container.
Ejecuta tests de señales en procesos hijos para no interrumpir el runner. Verifica código de salida, marcadores de limpieza y duración.
Errores comunes
Los fallos frecuentes son hacer limpieza pesada, adquirir locks, intentar recuperarse de SIGSEGV, depender solo de KeyboardInterrupt, olvidar SIGTERM en containers, asumir señales Unix en todas partes, no restaurar handlers y dejar el loop bloqueado sin wakeup FD.
Conclusión
signal convierte eventos del sistema en shutdown cooperativo, timers y notificaciones controladas. Mantén el handler mínimo y mueve el trabajo al flujo normal.
Respeta el thread principal, soporta SIGTERM y SIGINT, haz la limpieza idempotente y prueba en subprocesses. Consulta la documentación oficial de signal y el manual signal(7) de Linux.







