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 señales, y pertenecen a los diferentes métodos de comunicación entre procesos (Inter Process Communication, abreviado como IPC).

    En resumen, las señales son interrupciones de software que se envían al programa (o al proceso) para notificar al programa de eventos importantes 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 incluso simplemente ignora la señal.

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

    En este artículo te explicamos qué son las señales, te mostramos cómo enviar una señal a otro proceso desde la línea de comandos, además de 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, hay tres categorías de señales:

    • Señales del sistema (errores de hardware y del 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:

    $ 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 aproximadamente estandarizadas y tienen el siguiente significado en la mayoría de los sistemas Linux:

    • 1 (SIGHUP): termina una conexión o vuelve a cargar la configuración para los demonios
    • 2 (SIGINT): interrumpe la sesión desde la estación de diálogo
    • 3 (SIGQUIT): finaliza la sesión desde la estación de diálogo
    • 4 (SIGILL): se ejecutó una instrucción ilegal
    • 5 (SIGTRAP): hacer una sola instrucción (trap)
    • 6 (SIGABRT): terminación anormal
    • 7 (SIGBUS): error en el bus del sistema
    • 8 (SIGFPE): error de punto flotante
    • 9 (SIGKILL): finalice inmediatamente el proceso
    • 10 (SIGUSR1): señal definida por el usuario
    • 11 (SIGSEGV): fallo 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): finalizar el proceso de forma suave

    Para enviar una señal a un proceso en un terminal Linux, invoca el kill comando con el número de señal (o 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:

    $ kill -15 12345
    

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

    $ kill -SIGTERM 12345
    

    La forma que elija dependerá 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.

    Usando la biblioteca de señales de Python

    Desde Python 1.4, el signal La biblioteca es un componente regular de cada versión de Python. Para utilizar el signal biblioteca, importe la biblioteca a su programa Python de la siguiente manera, primero:

    import signal
    

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

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

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

    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 utilizando el método getpid() desde el os módulo. En un sin fin while bucle esperamos las señales entrantes. Implementamos esto usando dos módulos de Python más: os y hora. También los importamos al comienzo de nuestro script Python:

    import os
    import time
    

    En el while bucle de nuestro programa principal, la declaración de impresión produce “Esperando …”. los time.sleep() La llamada a la función hace que el programa espere tres segundos.

        # 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 guión. Habiendo guardado el guión como signal-handling.py podemos invocarlo en una terminal de la siguiente manera:

    $ 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 que aparece en la pantalla, arriba.

    $ kill -1 5746
    

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

    ...
    Received: 1
    ...
    

    Ignorando señales

    El módulo de señales 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 que, y como resultado, el programa Python no puede ser interrumpido por CTRL+C nunca más. Para detener el script de Python, se implementó una forma alternativa en el script de ejemplo: la señal SIGUSR1 termina el script de Python. Además, en lugar de un bucle sin fin usamos el método signal.pause(). Solo espera a que se reciba una señal.

    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 adecuado de las señales

    El manejador de señales que hemos usado hasta ahora es bastante simple y solo informa una señal recibida. Esto nos muestra que la interfaz de nuestro script Python está funcionando bien. Mejorémoslo.

    Captar 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 mediante una rutina específica por señal. Esto funciona mejor si entendemos qué 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, se espera que también escriba un volcado de memoria.

    Hasta ahora hemos implementado una única 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).

    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:

        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) por los comandos de UNIX kill -1 16640 y kill -15 16640 da como resultado la siguiente salida:

    $ 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 guión completo:

    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)
    

    Otras lecturas

    Utilizando el signal módulo y un controlador de eventos correspondiente, es relativamente fácil captar señales. Conocer el significado de las diferentes señales y reaccionar correctamente según lo definido 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.

     

    Etiquetas:

    Deja una respuesta

    Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *