signal - Eventi di Sistema Asincroni

Scopo: Eventi di Sistema Asincroni

I segnali sono una caratteristica di un sistema operativo che fornisce mezzi per notificare un programma di un evento, e se è stato gestito in modo asincrono. Possono essere generati dal sistema stesso, oppure inviati da un processo a un altro. Visto che i segnali interrompono il flusso regolare di un programma, è possibile che alcune operazioni (in particolare di Input/Output) possano produrre errori se un segnale viene ricevuto durante il loro svolgimento.

I segnali sono identificati da interi e sono definiti nelle intestazioni C del sistema operativo. Python espone i segnali appropriati alla piattaforma come simboli nel modulo signal. Gli esempi in questa sezione usano SIGINT e SIGUSR1. Entrambi sono tipicamente definiti per tutti i sistemi Unix e similari.

Programmare con i gestori di segnale Unix non è triviale. Questa è una introduzione, e non include tutti i dettagli necessari per usare i segnali con successo su ogni piattaforma. Esiste un certo grado di uniformità attraverso le versioni di Unix, ma ci sono anche alcune variazioni, quindi si consulti la documentazione del sistema operativo se non si vogliono avere problemi.

Ricevere Segnali

Così come altre forme di programmazione basate sugli eventi, i segnali sono ricevuti impostando una funzione di callback, chiamata gestore di segnale, che viene invocata quando il segnale si manifesta. Gli argomenti per il gestore di segnale sono il numero di segnale e lo stack frame a partire dal punto nel programma che è stato interrotto dal segnale.

# signal_signal.py

import signal
import os
import time


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

# Registra i gestori di segnale
signal.signal(signal.SIGUSR1, receive_signal)
signal.signal(signal.SIGUSR2, receive_signal)

# Stampa l'ID  del processo in modo che possa essere usato con 'kill'
# per inviare segnali a questo programma
print('Il mio PID è:', os.getpid())

while True:
    print('In attesa...')
    time.sleep(3)

Questo script di esempio esegue un ciclo infinito, mettendosi in pausa per pochi secondi ogni volta. Quando entra un segnale, la chiamata a sleep() viene interrotta e il gestore di segnale receive_signal stampa il numero del segnale. Dopo che il gestore di segnale ritorna, il ciclo continua.

Si mandino segnali al programma in esecuzione tramite os.kill() oppure con il programma da riga di comando Unix kill.

$ python3 signal_signal.py

Il mio PID è: 7388
In attesa...
In attesa...
In attesa...
Ricevuto: 10
In attesa...
In attesa...
Ricevuto: 12
In attesa...
In attesa...
In attesa...
Traceback (most recent call last):
  File "signal_signal.py", line 21, in <module>
    time.sleep(3)
KeyboardInterrupt

L'output precedente è stato ottenuto eseguendo signal_signal.py in una finestra, mentre in un altra si eseguiva:

$ kill -USR1 <numero_pid>
$ kill -USR2 <numero_pid>
$ kill -INT <numero_pid>

Recuperare Gestori Registrati

Per vedere quali gestori di segnale sono registrati per un segnale si usa getsignal(). Si passa il numero di segnale come argomento. Il valore ritornato è il gestore registrato, oppure uno dei valori speciali SIG_IGN (se il segnale è stato ignorato), SIG_DFL (se viene utilizzato il comportamento predefinito), oppure None (se il gestore di segnale esistente era stato registrato da C, invece che da 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)

Ancora una volta, visto che ogni sistema operativo potrebbe avere definiti segnali diversi, l'output su altri sistemi potrebbe variare. Questo è da Unix:

$ 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
SIGBUS     ( 7): SIG_DFL
SIGFPE     ( 8): SIG_DFL
SIGKILL    ( 9): SIG_DFL
SIGUSR1    (10): SIG_DFL
SIGSEGV    (11): SIG_DFL
SIGUSR2    (12): SIG_DFL
SIGPIPE    (13): SIG_IGN
SIGALRM    (14): <function alarm_received at 0x7f08d2387048>
SIGTERM    (15): SIG_DFL
SIGCLD     (17): SIG_DFL
SIGCONT    (18): SIG_DFL
SIGSTOP    (19): SIG_DFL
SIGTSTP    (20): SIG_DFL
SIGTTIN    (21): SIG_DFL
SIGTTOU    (22): SIG_DFL
SIGURG     (23): SIG_DFL
SIGXCPU    (24): SIG_DFL
SIGXFSZ    (25): SIG_IGN
SIGVTALRM  (26): SIG_DFL
SIGPROF    (27): SIG_DFL
SIGWINCH   (28): SIG_DFL
SIGPOLL    (29): SIG_DFL
SIGPWR     (30): SIG_DFL
SIGSYS     (31): SIG_DFL
SIGRTMIN   (34): SIG_DFL
SIGRTMAX   (64): SIG_DFL

Inviare Segnali

La funzione per inviare segnali da Python è os.kill(). Il suo uso è trattato nella sezione del modulo os, Creare Processi con os.fork().

Allarmi

Gli allarmi sono un tipo speciale di segnale, dove il programma chiede al sistema operativo di notificarlo dopo che è trascorso un certo periodo di tempo. Come evidenziato dalla documentazione standard del modulo os, la cosa è utile per evitare di bloccare all'infinito una operazione di Input/Output o altra chiamata di sistema.

# signal_alarm.py

import signal
import time


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

# Chiam receive_alarm in 2 secondi
signal.signal(signal.SIGALRM, receive_alarm)
signal.alarm(2)

print('Prima:', time.ctime())
time.sleep(4)
print('Dopo :', time.ctime())

In questo esempio, la chiamata a sleep() viene interrotta, poi riprende dopo che il segnale è elaborato, quindi il messaggio stampato dopo il ritorno di sleep() mostra che il programma è stato messo in pausa per almeno tanto quanto la durata della pausa stessa.

$ python3 signal_alarm.py

Prima: Tue Jan 31 21:24:15 2017
Allarme : Tue Jan 31 21:24:17 2017
Dopo : Tue Jan 31 21:24:19 2017

Ignorare i Segnali

Per ignorare un segnale, si registra SIG_IGN come gestore. Questo script sostituisce il gestore predefinito per SIGINT con SIG_IGN e registra un gestore per SIGUSR1. Poi usa signal.pause() per attendere che il segnale sia ricevuto.

# signal_ignore.py

import signal
import os
import time


def do_exit(sig, stack):
    raise SystemExit('In uscita')

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

print('Il mio PID:', os.getpid())

signal.pause()

Normalmente SIGINT (il segnale inviato dalla shell a un programma quando l'utente preme Ctrl-C) solleva una eccezione KeyboardInterrupt. Questo esempio ignora SIGINT e solleva SystemExit quando vede SIGUSR1. Ogni ^C nell'output rappresenta il tentativo di usare Ctrl-C per terminare lo script dal terminale. Usando kill -USR1 26880 da un altro terminale si causerà l'uscita dello script.

$ python3 signal_ignore.py

Il mio PID: 26880
^C^C^CIn uscita

Segnali e Thread

Segnali e thread in genere non si combinano bene in quanto solo il thread principale riceverà segnali. L'esempio di seguito imposta un gestore di segnale, attende il segnale in un thread, e invia il segnale da un altro.

# signal_threads.py

import signal
import threading
import os
import time


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

signal.signal(signal.SIGUSR1, signal_handler)


def wait_for_signal():
    print('In attesa dell\'entrata del segnale',
          threading.currentThread().name)
    signal.pause()
    print('Attesa terminata')

# Inizia un thread che non riceverà il segnale
receiver = threading.Thread(
    target=wait_for_signal,
    name='ricevente',
)
receiver.start()
time.sleep(0.1)


def send_signal():
    print('Invio del segnale', threading.currentThread().name)
    os.kill(os.getpid(), signal.SIGUSR1)

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

# Attende il thread per vedere il segnale (non succederà!)
print('In attesa di', receiver.name)
signal.alarm(2)
receiver.join()

I gestori di segnale sono tutti registrati nel thread principale in quanto questo è un requisito dell'implementazione per Python del modulo signal, a prescindere dal supporto per l'integrazione di thread e segnali della piattaforma sottostante. Anche se il thread ricevente chiama signal.pause() non riceve il segnale. La chiamata di signal.alarm(2) verso la fine dell'esempio previene un ciclo infinito, visto che il thread ricevente non uscirà mai.

$ python3 signal_threads.py

In attesa dell'entrata del segnale ricevente
Invio del segnale mittente
Segnale 10 ricevuto in MainThread
In attesa di ricevente
Alarm clock

Sebbene si possano impostare allarmi in qualunque thread, essi saranno sempre ricevuti dal thread principale.

# signal_threads_alarm.py

import signal
import threading
import time


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

signal.signal(signal.SIGALRM, signal_handler)


def use_alarm():
    t_name = threading.currentThread().name
    print(time.ctime(), 'Impostazione allarme in', t_name)
    signal.alarm(1)
    print(time.ctime(), 'In pausa per', t_name)
    time.sleep(3)
    print(time.ctime(), 'Pausa terminata in', t_name)

# Inizia un thread che non riceverà il segnale
alarm_thread = threading.Thread(
    target=use_alarm,
    name='alarm_thread',
)
alarm_thread.start()
time.sleep(0.1)

# Attende il thread per vedere il signale (non succederà!)
print(time.ctime(), 'In attesa di', alarm_thread.name)
alarm_thread.join()

print(time.ctime(), 'Uscita normale')

L'allarme non interrompe la chiamata di sleep() in use_alarm.

$ python3 signal_threads_alarm.py

Fri Feb  3 20:39:25 2017 Impostazione allarme in alarm_thread
Fri Feb  3 20:39:25 2017 In pausa per alarm_thread
Fri Feb  3 20:39:26 2017 In attesa di alarm_thread
Fri Feb  3 20:39:26 2017 Allarme in MainThread
Fri Feb  3 20:39:28 2017 Pausa terminata in alarm_thread
Fri Feb  3 20:39:28 2017 Uscita normale

Vedere anche:

signal
La documentazione della libreria standard per questo modulo.
PEP 475
Falliti tentativi di ripetere chiamate di sistema con EINTR
subprocess
Genera processi addizionali
Creare Processi con os.fork()
La funzione kill() può essere usata per inviare segnali tra processi