Manejo de señales Unix en Python

Los sistemas UNIX/Linux ofrecen mecanismos especiales para comunicarse entre cada proceso individual. Uno de estos mecanismos son las señales, y pertenecen a los diferentes met...

Los sistemas UNIX/Linux ofrecen mecanismos especiales para comunicarse entre cada proceso individual. Uno de estos mecanismos son las señales, y pertenecen a los diferentes métodos de comunicación entre procesos (Inter Process Communication, abreviado IPC).

En resumen, las señales son interrupciones de software que se envían al programa (o al proceso) para notificar al programa sobre eventos significativos o solicitudes al programa para ejecutar una secuencia de código especial. Un programa que recibe una señal detiene o continúa la ejecución de sus instrucciones, termina con o sin un volcado de memoria, o simplemente ignora la señal.

Aunque está definido en el estándar POSIX, la reacción en realidad depende de cómo el desarrollador escribió el script e implementó el manejo de las señales.

En este artículo explicamos qué son las señales, le mostramos cómo enviar una señal a otro proceso desde la línea de comandos y cómo procesar la señal recibida. Entre otros módulos, el código del programa se basa principalmente en el módulo de señal. Este módulo conecta los encabezados C correspondientes de su sistema operativo con el mundo de Python.

Introducción a las señales

En los sistemas basados ​​en UNIX, existen tres categorías de señales:

  • Señales del sistema (errores de hardware y sistema): SIGILL, SIGTRAP, SIGBUS, SIGFPE, SIGKILL, SIGSEGV, SIGXCPU, SIGXFSZ, SIGIO

  • Señales del dispositivo: SIGHUP, SIGINT, SIGPIPE, SIGALRM, SIGCHLD, SIGCONT, SIGSTOP, SIGTTIN, SIGTTOU, SIGURG, SIGWINCH, SIGIO

  • Señales definidas por el usuario: SIGQUIT, SIGABRT, SIGUSR1, SIGUSR2, SIGTERM

Cada señal está representada por un valor entero, y la lista de señales que están disponibles es comparativamente larga y no consistente entre las diferentes variantes de UNIX/Linux. En un sistema Debian GNU/Linux, el comando kill -l muestra la lista de señales de la siguiente manera:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
$ kill -l
 1) SIGHUP   2) SIGINT   3) SIGQUIT  4) SIGILL   5) SIGTRAP
 6) SIGABRT  7) SIGBUS   8) SIGFPE   9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG  24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF 28) SIGWINCH    29) SIGIO   30) SIGPWR
31) SIGSYS  34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

Las señales 1 a 15 están más o menos estandarizadas y tienen el siguiente significado en la mayoría de los sistemas Linux:

  • 1 (SIGHUP): terminar una conexión o recargar la configuración para demonios
  • 2 (SIGINT): interrumpir la sesión desde la estación de diálogo
  • 3 (SIGQUIT): terminar la sesión desde la estación de diálogo
  • 4 (SIGILL): se ejecutó instrucción ilegal
  • 5 (SIGTRAP): hacer una sola instrucción (trampa)
  • 6 (SIGABRT): terminación anormal
  • 7 (SIGBUS): error en el bus del sistema
  • 8 (SIGFPE): error de punto flotante
  • 9 (SIGKILL): terminar inmediatamente el proceso
  • 10 (SIGUSR1): señal definida por el usuario
  • 11 (SIGSEGV): falla de segmentación por acceso ilegal a un segmento de memoria
  • 12 (SIGUSR2): señal definida por el usuario
  • 13 (SIGPIPE): escribiendo en una tubería y nadie lee de ella
  • 14 (SIGALRM): el temporizador terminó (alarma)
  • 15 (SIGTERM): terminar el proceso de forma suave

Para enviar una señal a un proceso en una terminal de Linux, invoque el comando kill con el número de señal (o el nombre de la señal) de la lista anterior y la identificación del proceso (pid). El siguiente comando de ejemplo envía la señal 15 (SIGTERM) al proceso que tiene el pid 12345:

1
$ kill -15 12345

Una forma equivalente es usar el nombre de la señal en lugar de su número:

1
$ kill -SIGTERM 12345

El camino que elija depende de lo que sea más conveniente para usted. Ambas formas tienen el mismo efecto. Como resultado, el proceso recibe la señal SIGTERM y termina inmediatamente.

Uso de la biblioteca de señales de Python

Desde Python 1.4, la biblioteca signal es un componente regular de cada lanzamiento de Python. Para usar la biblioteca signal, primero importa la biblioteca a tu programa Python de la siguiente manera:

1
import signal

La captura y reacción adecuada de una señal recibida se realiza mediante una función de devolución de llamada, el llamado controlador de señal. Un manejador de señales bastante simple llamado receiveSignal() se puede escribir de la siguiente manera:

1
2
3
def receiveSignal(signalNumber, frame):
    print('Received:', signalNumber)
    return

Este manejador de señales no hace más que reportar el número de la señal recibida. El siguiente paso es registrar las señales que capta el manejador de señales. Para los programas de Python, todas las señales (excepto 9, SIGKILL) se pueden capturar en su secuencia de comandos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
if __name__ == '__main__':
    # register the signals to be caught
    signal.signal(signal.SIGHUP, receiveSignal)
    signal.signal(signal.SIGINT, receiveSignal)
    signal.signal(signal.SIGQUIT, receiveSignal)
    signal.signal(signal.SIGILL, receiveSignal)
    signal.signal(signal.SIGTRAP, receiveSignal)
    signal.signal(signal.SIGABRT, receiveSignal)
    signal.signal(signal.SIGBUS, receiveSignal)
    signal.signal(signal.SIGFPE, receiveSignal)
    #signal.signal(signal.SIGKILL, receiveSignal)
    signal.signal(signal.SIGUSR1, receiveSignal)
    signal.signal(signal.SIGSEGV, receiveSignal)
    signal.signal(signal.SIGUSR2, receiveSignal)
    signal.signal(signal.SIGPIPE, receiveSignal)
    signal.signal(signal.SIGALRM, receiveSignal)
    signal.signal(signal.SIGTERM, receiveSignal)

A continuación, agregamos la información del proceso para el proceso actual y detectamos la identificación del proceso usando el método getpid() del módulo os. En un ciclo ‘while’ sin fin, esperamos las señales entrantes. Implementamos esto usando dos módulos Python más: sistema operativo y [tiempo](https://docs.python.org/3/library /hora.html). También los importamos al comienzo de nuestro script de Python:

1
2
import os
import time

En el ciclo while de nuestro programa principal, la declaración de impresión genera "Esperando...". La llamada a la función time.sleep() hace que el programa espere tres segundos.

1
2
3
4
5
6
7
    # output current process id
    print('My PID is:', os.getpid())

    # wait in an endless loop for signals 
    while True:
        print('Waiting...')
        time.sleep(3)

Finalmente, tenemos que probar nuestro script. Habiendo guardado el script como signal-handling.py podemos invocarlo en una terminal de la siguiente manera:

1
2
3
4
$ python3 signal-handling.py 
My PID is: 5746
Waiting...
...

En una segunda ventana de terminal enviamos una señal al proceso. Identificamos nuestro primer proceso, la secuencia de comandos de Python, por la identificación del proceso como se muestra en la pantalla, arriba.

1
$ kill -1 5746

El controlador de eventos de señal en nuestro programa de Python recibe la señal que hemos enviado al proceso. Reacciona en consecuencia y simplemente confirma la señal recibida:

1
2
3
...
Received: 1
...

Ignorando señales

El módulo de señal define formas de ignorar las señales recibidas. Para hacer eso, la señal debe estar conectada con la función predefinida signal.SIG_IGN. El siguiente ejemplo demuestra eso y, como resultado, el programa Python ya no puede ser interrumpido por CTRL+C. Para detener el script de Python, se ha implementado una forma alternativa en el script de ejemplo: la señal SIGUSR1 finaliza el script de Python. Además, en lugar de un bucle sin fin, usamos el método signal.pause(). Sólo espera a que se reciba una señal.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import signal
import os
import time

def receiveSignal(signalNumber, frame):
    print('Received:', signalNumber)
    raise SystemExit('Exiting')
    return

if __name__ == '__main__':
    # register the signal to be caught
    signal.signal(signal.SIGUSR1, receiveSignal)

    # register the signal to be ignored
    signal.signal(signal.SIGINT, signal.SIG_IGN)

    # output current process id
    print('My PID is:', os.getpid())

    signal.pause()

Manejo de señales correctamente

El controlador de señal que hemos usado hasta ahora es bastante simple y solo informa una señal recibida. Esto nos muestra que la interfaz de nuestro script de Python funciona bien. Vamos a mejorarlo.

La captura de la señal ya es una buena base, pero requiere algunas mejoras para cumplir con las reglas del estándar POSIX. Para una mayor precisión, cada señal necesita una reacción adecuada (consulte la lista anterior). Esto significa que el controlador de señales en nuestro script de Python debe ampliarse con una rutina específica por señal. Esto funciona mejor si entendemos lo que hace una señal y cuál es una reacción común. Un proceso que recibe la señal 1, 2, 9 o 15 termina. En cualquier otro caso, también se espera que escriba un volcado del núcleo.

Hasta ahora hemos implementado una sola rutina que cubre todas las señales y las maneja de la misma manera. El siguiente paso es implementar una rutina individual por señal. El siguiente código de ejemplo demuestra esto para las señales 1 (SIGHUP) y 15 (SIGTERM).

1
2
3
4
5
6
7
def readConfiguration(signalNumber, frame):
    print ('(SIGHUP) reading configuration')
    return

def terminateProcess(signalNumber, frame):
    print ('(SIGTERM) terminating the process')
    sys.exit()

Las dos funciones anteriores están conectadas con las señales de la siguiente manera:

1
2
    signal.signal(signal.SIGHUP, readConfiguration)
    signal.signal(signal.SIGTERM, terminateProcess)

Ejecutar el script de Python y enviar la señal 1 (SIGHUP) seguida de una señal 15 (SIGTERM) mediante los comandos UNIX kill -1 16640 y kill -15 16640 da como resultado el siguiente resultado:

1
2
3
4
5
6
7
8
$ python3 daemon.py
My PID is: 16640
Waiting...
Waiting...
(SIGHUP) reading configuration
Waiting...
Waiting...
(SIGTERM) terminating the process

El script recibe las señales y las maneja correctamente. Para mayor claridad, este es el script completo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import signal
import os
import time
import sys

def readConfiguration(signalNumber, frame):
    print ('(SIGHUP) reading configuration')
    return

def terminateProcess(signalNumber, frame):
    print ('(SIGTERM) terminating the process')
    sys.exit()

def receiveSignal(signalNumber, frame):
    print('Received:', signalNumber)
    return

if __name__ == '__main__':
    # register the signals to be caught
    signal.signal(signal.SIGHUP, readConfiguration)
    signal.signal(signal.SIGINT, receiveSignal)
    signal.signal(signal.SIGQUIT, receiveSignal)
    signal.signal(signal.SIGILL, receiveSignal)
    signal.signal(signal.SIGTRAP, receiveSignal)
    signal.signal(signal.SIGABRT, receiveSignal)
    signal.signal(signal.SIGBUS, receiveSignal)
    signal.signal(signal.SIGFPE, receiveSignal)
    #signal.signal(signal.SIGKILL, receiveSignal)
    signal.signal(signal.SIGUSR1, receiveSignal)
    signal.signal(signal.SIGSEGV, receiveSignal)
    signal.signal(signal.SIGUSR2, receiveSignal)
    signal.signal(signal.SIGPIPE, receiveSignal)
    signal.signal(signal.SIGALRM, receiveSignal)
    signal.signal(signal.SIGTERM, terminateProcess)

    # output current process id
    print('My PID is:', os.getpid())

    # wait in an endless loop for signals 
    while True:
        print('Waiting...')
        time.sleep(3)

Lecturas adicionales {#lecturas adicionales}

Usando el módulo signal y un controlador de eventos correspondiente, es relativamente fácil captar señales. Conocer el significado de las diferentes señales y reaccionar adecuadamente como se define en el estándar POSIX es el siguiente paso. Requiere que el controlador de eventos distinga entre las diferentes señales y tenga una rutina separada para todas ellas.