signal — Eventos asíncronos del sistema

Propósito:Eventos asíncronos del sistema

Las señales son una característica del sistema operativo que proporciona un medio de notificar a un programa de un evento, y manejarlo de forma asíncrona. Pueden ser generados por el propio sistema o enviados desde un proceso a otro. Dado que las señales interrumpen el flujo regular del programa, es posible que algunas operaciones (especialmente I/O) produzcan errores si se recibe una señal en el medio.

Las señales se identifican mediante números enteros y se definen en las cabeceras C del sistema operativo. Python expone las señales apropiadas para el plataforma como símbolos en el módulo signal. Los ejemplos en este sección utilizan SIGINT y SIGUSR1. Ambas son típicamente definidas para todos los sistemas Unix y Unix.

Nota

La programación con manejadores de señales Unix no es un esfuerzo trivial. Esta es una introducción, y no incluye todas los detalles necesarios para utilizar señales con éxito en cada plataforma. Hay cierto grado de estandarización en las versiones de Unix, pero también hay algunas variaciones, así que consulta el documentación del sistema operativo si tienes problemas.

Recibir señales

Al igual que con otras formas de programación basada en eventos, se reciben señales estableciendo una función de devolución de llamada, llamada un manejador de señal, que se invoca cuando se produce la señal. Los argumentos al manejador de señal son el número de señal y la pila desde el punto en el programa que fue interrumpido por la señal.

signal_signal.py
import signal
import os
import time


def receive_signal(signum, stack):
    print('Received:', signum)


# Register signal handlers
signal.signal(signal.SIGUSR1, receive_signal)
signal.signal(signal.SIGUSR2, receive_signal)

# Print the process ID so it can be used with 'kill'
# to send this program signals.
print('My PID is:', os.getpid())

while True:
    print('Waiting...')
    time.sleep(3)

Esta secuencia de comandos de ejemplo se repite indefinidamente, deteniéndose unos segundos cada vez. Cuando llega una señal, la llamada sleep() se interrumpe y el manejador de señal receive_signal imprime el número de la señal. Una vez que el manejador de señal regresa, el bucle continúa.

Envía señales al programa en ejecución usando os.kill() o el programa de linea de comando Unix kill.

$ python3 signal_signal.py

My PID is: 71387
Waiting...
Waiting...
Waiting...
Received: 30
Waiting...
Waiting...
Received: 31
Waiting...
Waiting...
Traceback (most recent call last):
  File "signal_signal.py", line 28, in <module>
    time.sleep(3)
KeyboardInterrupt

La salida anterior se produjo ejecutando signal_signal.py en una ventana, luego corriendo en otra ventana:

$ kill -USR1 $pid
$ kill -USR2 $pid
$ kill -INT $pid

Recuperar manejadores registrados

Para ver qué manejadores de señales están registrados para una señal, usa getsignal(). Pasa el número de señal como argumento. El valor de regreso es el controlador registrado, o uno de los valores especiales SIG_IGN (si la señal está siendo ignorada), SIG_DFL (si se está utilizando el comportamiento predeterminado), o None (si el controlado de señales el controlador de señales existente se registró desde C, en lugar de Python).

signal_getsignal.py
import signal


def alarm_received(n, stack):
    return


signal.signal(signal.SIGALRM, alarm_received)

signals_to_names = {
    getattr(signal, n): n
    for n in dir(signal)
    if n.startswith('SIG') and '_' not in n
}

for s, name in sorted(signals_to_names.items()):
    handler = signal.getsignal(s)
    if handler is signal.SIG_DFL:
        handler = 'SIG_DFL'
    elif handler is signal.SIG_IGN:
        handler = 'SIG_IGN'
    print('{:<10} ({:2d}):'.format(name, s), handler)

Nuevamente, dado que cada sistema operativo puede tener diferentes señales definidas, la salida en otros sistemas pueden variar. Esto es de macOS X:

$ python3 signal_getsignal.py

SIGHUP     ( 1): SIG_DFL
SIGINT     ( 2): <built-in function default_int_handler>
SIGQUIT    ( 3): SIG_DFL
SIGILL     ( 4): SIG_DFL
SIGTRAP    ( 5): SIG_DFL
SIGIOT     ( 6): SIG_DFL
SIGEMT     ( 7): SIG_DFL
SIGFPE     ( 8): SIG_DFL
SIGKILL    ( 9): None
SIGBUS     (10): SIG_DFL
SIGSEGV    (11): SIG_DFL
SIGSYS     (12): SIG_DFL
SIGPIPE    (13): SIG_IGN
SIGALRM    (14): <function alarm_received at 0x1019a6a60>
SIGTERM    (15): SIG_DFL
SIGURG     (16): SIG_DFL
SIGSTOP    (17): None
SIGTSTP    (18): SIG_DFL
SIGCONT    (19): SIG_DFL
SIGCHLD    (20): SIG_DFL
SIGTTIN    (21): SIG_DFL
SIGTTOU    (22): SIG_DFL
SIGIO      (23): SIG_DFL
SIGXCPU    (24): SIG_DFL
SIGXFSZ    (25): SIG_IGN
SIGVTALRM  (26): SIG_DFL
SIGPROF    (27): SIG_DFL
SIGWINCH   (28): SIG_DFL
SIGINFO    (29): SIG_DFL
SIGUSR1    (30): SIG_DFL
SIGUSR2    (31): SIG_DFL

Enviar señales

La función para enviar señales desde Python es os.kill(). Su uso está cubierto en la sección sobre el módulo os creating-processes-with-os-fork.

Alarmas

Las alarmas son un tipo especial de señal, donde el programa le pide al sistema operativo que lo notifique después de que haya transcurrido algún tiempo. Como la documentación estándar del módulo os señala, esto es útil para evitar el bloqueo indefinido en una operación de E/S u otra llamada del sistema.

signal_alarm.py
import signal
import time


def receive_alarm(signum, stack):
    print('Alarm :', time.ctime())


# Call receive_alarm in 2 seconds
signal.signal(signal.SIGALRM, receive_alarm)
signal.alarm(2)

print('Before:', time.ctime())
time.sleep(4)
print('After :', time.ctime())

En este ejemplo, la llamada a sleep() se interrumpe, pero continúa después de que la señal se procesa, por lo que el mensaje se imprime después que sleep() muestra que el programa se pausó al menos durante la duración de sleep.

$ python3 signal_alarm.py

Before: Sat Apr 22 14:48:57 2017
Alarm : Sat Apr 22 14:48:59 2017
After : Sat Apr 22 14:49:01 2017

Ignorar Señales

Para ignorar una señal, registra SIG_IGN como el controlador. Esta secuencia de comandos reemplaza el controlador predeterminado para SIGINT con SIG_IGN, y registra un manejador para SIGUSR1. Entonces utiliza signal.pause() para esperar a que se reciba una señal.

signal_ignore.py
import signal
import os
import time


def do_exit(sig, stack):
    raise SystemExit('Exiting')


signal.signal(signal.SIGINT, signal.SIG_IGN)
signal.signal(signal.SIGUSR1, do_exit)

print('My PID:', os.getpid())

signal.pause()

Normalmente SIGINT (la señal enviada por el shell a un programa cuando el usuario presiona Ctrl-C) levanta un KeyboardInterrupt. Este ejemplo ignora SIGINT y genera SystemExit cuando ve SIGUSR1. Cada ^C en la salida representa un intento de usar Ctrl-C para matar la secuencia de comandos desde la terminal. El uso de kill -USR1 72598 desde otro terminal eventualmente causa que la secuencia de comandos termine.

$ python3 signal_ignore.py

My PID: 72598
^C^C^C^CExiting

Señales e hilos

Las señales y los hilos generalmente no se mezclan bien porque solo el hilo principal de un proceso recibirá señales. El siguiente ejemplo establece un manejador de señal, espera la señal en un hilo y envía la señal de otro.

signal_threads.py
import signal
import threading
import os
import time


def signal_handler(num, stack):
    print('Received signal {} in {}'.format(
        num, threading.currentThread().name))


signal.signal(signal.SIGUSR1, signal_handler)


def wait_for_signal():
    print('Waiting for signal in',
          threading.currentThread().name)
    signal.pause()
    print('Done waiting')


# Start a thread that will not receive the signal
receiver = threading.Thread(
    target=wait_for_signal,
    name='receiver',
)
receiver.start()
time.sleep(0.1)


def send_signal():
    print('Sending signal in', threading.currentThread().name)
    os.kill(os.getpid(), signal.SIGUSR1)


sender = threading.Thread(target=send_signal, name='sender')
sender.start()
sender.join()

# Wait for the thread to see the signal (not going to happen!)
print('Waiting for', receiver.name)
signal.alarm(2)
receiver.join()

Los manejadores de señales fueron todos registrados en el hilo principal porque este es un requisito de la implementación del módulo signal para Python, independientemente del soporte de la plataforma subyacente para mezclar hilos y señales. Aunque el subproceso del receptor llama a signal.pause(), no recibe la señal. La llamada signal.alarm(2) cerca de el final del ejemplo evita un bloque infinito, ya que el receptor el hilo nunca saldrá.

$ python3 signal_threads.py

Waiting for signal in receiver
Sending signal in sender
Received signal 30 in MainThread
Waiting for receiver
Alarm clock

Aunque las alarmas se pueden configurar en cualquier hilo, siempre son recibidas por el hilo principal.

signal_threads_alarm.py
import signal
import time
import threading


def signal_handler(num, stack):
    print(time.ctime(), 'Alarm in',
          threading.currentThread().name)


signal.signal(signal.SIGALRM, signal_handler)


def use_alarm():
    t_name = threading.currentThread().name
    print(time.ctime(), 'Setting alarm in', t_name)
    signal.alarm(1)
    print(time.ctime(), 'Sleeping in', t_name)
    time.sleep(3)
    print(time.ctime(), 'Done with sleep in', t_name)


# Start a thread that will not receive the signal
alarm_thread = threading.Thread(
    target=use_alarm,
    name='alarm_thread',
)
alarm_thread.start()
time.sleep(0.1)

# Wait for the thread to see the signal (not going to happen!)
print(time.ctime(), 'Waiting for', alarm_thread.name)
alarm_thread.join()

print(time.ctime(), 'Exiting normally')

La alarma no cancela la llamada sleep() en use_alarm().

$ python3 signal_threads_alarm.py

Sat Apr 22 14:49:01 2017 Setting alarm in alarm_thread
Sat Apr 22 14:49:01 2017 Sleeping in alarm_thread
Sat Apr 22 14:49:01 2017 Waiting for alarm_thread
Sat Apr 22 14:49:02 2017 Alarm in MainThread
Sat Apr 22 14:49:04 2017 Done with sleep in alarm_thread
Sat Apr 22 14:49:04 2017 Exiting normally

Ver también