Gestire Segnali con la funzione signal in Linux
In questa lezione vedremo come gestire i segnali in Linux attraverso la funzione signal
. Vedremo come registrare un handler di segnale personalizzato per un segnale specifico e come ripristinare l'handler precedente.
La funzione signal()
Originariamente, i sistemi operativi UNIX, così come anche Linux, permettevano di cambiare la gestione di un segnale attraverso la funzione signal
. Questa funzione rappresenta il modo più semplice per modificare il comportamento di un processo in seguito alla ricezione di un segnale.
Oggigiorno, si utilizza la funzione sigaction
, che vedremo nelle prossime lezioni, in quanto signal
è considerata obsoleta.
Il principale problema nell'utilizzo di signal
è che esistono delle sottili differenze nel suo comportamento tra i vari sistemi operativi UNIX, e quindi non è garantito che il codice scritto sia portabile.
La funzione sigaction
, invece, ha un comportamento standard. In questa lezione vedremo come usare signal
ma, in generale, è consigliabile utilizzare sigaction
. Inoltre, sotto Linux, in realtà, signal
è implementata come un wrapper di sigaction
.
In generale, la funzione signal
ha la seguente firma:
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
La firma di signal
è un po' complessa; esaminiamola punto per punto:
- Il primo argomento
signum
rappresenta il segnale che vogliamo gestire; -
Il secondo argomento
handler
rappresenta un puntatore a funzione. Si tratta di un puntatore all'handler di segnale che vogliamo associare al segnalesignum
.Un handler di segnale è una qualunque funzione con la seguente firma:
void handler(int signum);
Ossia, una funzione che accetta un intero come argomento e non restituisce alcun valore.
Dopo vedremo come creare un handler di segnale personalizzato e qual è l'utilizzo del parametro
signum
all'interno dell'handler. -
La funzione
signal
restituisce un puntatore a funzione. Questo puntatore rappresenta l'handler di segnale precedente associato al segnalesignum
. In questo modo, possiamo salvare l'handler precedente e ripristinarlo, eventualmente, in seguito.In caso di errore, la funzione restituisce il valore
SIG_ERR
e imposta la variabileerrno
con un valore appropriato.
Ad esempio, volendo gestire il segnale SIGINT
(che corrisponde alla combinazione di tasti Ctrl + C), con un handler personalizzato per poi ripristinare l'handler precedente, possiamo scrivere il seguente codice:
sighandler_t vecchio_handler = nullptr;
vecchio_handler = signal(SIGINT, handler_personalizzato);
if (vecchio_handler == SIG_ERR) {
perror("signal");
exit(EXIT_FAILURE);
}
/* Ora il segnale SIGINT è gestito da handler_personalizzato */
/* Ripristiniamo l'handler precedente */
if (signal(SIGINT, vecchio_handler) == SIG_ERR) {
perror("signal");
exit(EXIT_FAILURE);
}
Funzione signal()
La funzione signal
permette di cambiare la gestione di un segnale. La funzione ha la seguente firma:
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
signum
: rappresenta il segnale che vogliamo gestire;handler
: rappresenta un puntatore a funzione che rappresenta l'handler di segnale che vogliamo associare al segnalesignum
.
La funzione restituisce un puntatore a funzione che rappresenta l'handler di segnale precedente associato al segnale signum
. In caso di errore, la funzione restituisce SIG_ERR
e imposta la variabile errno
.
Identificatori SIG_DFL
e SIG_IGN
Attraverso la funzione signal
si possono anche specificare altre azioni da eseguire in seguito alla ricezione di un segnale. In particolare, possiamo utilizzare due identificatori speciali:
-
SIG_DFL
: che sta per Signal Default. Utilizzando questo identificatore, viene resettata la disposizione del segnale al valore di default come mostrato nella tabella vista in precedenza; -
SIG_IGN
: che sta per Signal Ignore. Utilizzando questo identificatore, il segnale viene ignorato.
Questi due identificatori possono essere restituiti dalla funzione signal
.
Esempio
Proviamo, con un esempio, a mostrare il funzionamento di signal
. Riprendiamo l'esempio di processo bersaglio che abbiamo creato per studiare la funzione kill
.
Modifichiamo, però, l'esempio in modo tale da ignorare il segnale SIGINT
:
/* bersaglio.c */
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
int main() {
/* Stampa il proprio PID */
printf("PID: %d\n", getpid());
/* Ignora il segnale SIGINT */
signal(SIGINT, SIG_IGN);
while (1) {
printf("Processo bersaglio\n");
sleep(1);
}
return 0;
}
Compiliamo il codice e avviamo il processo bersaglio:
$ ./bersaglio
PID: 1234
Processo bersaglio
Processo bersaglio
Processo bersaglio
...
Ovviamente il PID del processo sarà diverso da 1234
. Il processo bersaglio stamperà ripetutamente la stringa "Processo bersaglio" ogni secondo.
Ora, se proviamo a inviare il segnale SIGINT
al processo bersaglio, vedremo che il processo non verrà terminato:
$ kill -SIGINT 1234
Non solo, ma anche premendo Ctrl + C non avremo alcun effetto.
Viceversa, se proviamo ad inviare il segnale SIGTERM
, il processo verrà terminato:
$ kill -SIGTERM 1234
$ ./bersaglio
PID: 1234
Processo bersaglio
Processo bersaglio
Processo bersaglio
...
Terminated
Introduzione agli Handler di Segnali
Nella sezione precedente abbiamo visto come cambiare la disposizione di un segnale attraverso la funzione signal
. Adesso introduciamo il concetto di handler di segnale.
Nella sua forma più semplice, un handler di segnale è una funzione che viene eseguita in seguito alla ricezione di uno specifico segnale.
L'invocazione di un handler potrebbe interrompere il flusso principale di un programma in qualunque momento. Il kernel del sistema invoca l'handler per conto del processo e, quando l'esecuzione dell'handler termina, l'esecuzione del programma riprende esattamente dal punto in cui era stata interrotta.
Per comprendere meglio, osserviamo la figura che segue:
Come si osserva dalla figura, il processo bersaglio è in esecuzione e, ad un certo punto, dopo aver eseguito l'istruzione n
, riceve un segnale. A questo punto, il flusso di esecuzione passa all'handler di segnale che viene eseguito nella sua interezza e termina. Dopo che l'handler termina, il flusso di esecuzione del processo riprende esattamente dal punto in cui era stato interrotto, ossia dall'istruzione n+1
.
Sebbene un handler di segnale è una funzione come un'altra, è sempre buona prassi mantenere l'handler il più semplice possibile. I motivi li esamineremo nelle prossime lezioni.
Per ora, vediamo come creare un handler di segnale personalizzato. Modifichiamo il codice del programma bersaglio.c
in modo tale da creare un handler personalizzato per il segnale SIGINT
. Questo handler farà due cose:
- Stampa un messaggio a video;
- Imposta una variabile globale in modo tale da chiudere in maniera pulita l'esecuzione del programma.
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 |
|
Abbiamo modificato il codice in quattro punti:
- Abbiamo dichiarato una variabile globale
termina
che utilizzeremo per terminare il ciclowhile
nelmain
(riga 8); - Abbiamo definito una funzione
handler_sigint
che stamperà un messaggio a video e imposterà la variabiletermina
a1
(righe 11-14); - Abbiamo impostato l'handler personalizzato per il segnale
SIGINT
(riga 21); - Abbiamo modificato il ciclo
while
in modo tale che termini quando la variabiletermina
è diversa da0
(riga 23).
Compiliamo il codice e avviamo il programma:
$ ./bersaglio_handler
PID: 1234
Processo bersaglio
Processo bersaglio
Processo bersaglio
...
Ora, se inviamo il segnale SIGINT
al processo bersaglio, utilizzando la combinazione di tasti Ctrl + C, vedremo che il programma si chiuderà in maniera pulita stampando i messaggi che seguono:
$ ./bersaglio_handler
PID: 1234
Processo bersaglio
Processo bersaglio
Processo bersaglio
...
^CRicevuto segnale SIGINT
Uscita...
Questo codice, tuttavia, ha dei difetti nascosti che per ora non approfondiremo. In generale, non è una buona norma utilizzare funzioni di stdio.h
come la printf
all'interno di un handler. Inoltre, modificare il valore di variabili globali all'interno di un handler deve essere fatto con cautela. Analizzeremo nel dettaglio questi aspetti nelle prossime lezioni.
Una cosa importante da notare è che un handler riceve un parametro in ingresso: il parametro signum
che rappresenta il segnale che ha scatenato l'invocazione dell'handler. Nell'esempio di sopra, poiché l'handler è stato registrato solo per il segnale SIGINT
, questo parametro è di scarsa utilità. Tuttavia, in casi più complessi, potrebbe essere utile.
Ricapitolando:
Handler di Segnale
Un Handler di Segnale è una funzione che viene eseguita in seguito alla ricezione di un segnale. L'esecuzione di un handler comincia immediatamente dopo la ricezione del segnale interrompendo il flusso principale del programma. Quando l'handler termina, l'esecuzione del programma riprende esattamente dal punto in cui era stata interrotta.
La firma di un handler di segnale è la seguente:
void handler(int signum);
Il parametro signum
rappresenta il segnale che ha scatenato l'invocazione dell'handler.
Un handler di segnale può essere registrato per catturare uno o più segnali.
Esempio di Handler per più Segnali
Proviamo a scrivere un programma con un handler che gestisce più segnali.
Il programma che andiamo a realizzare è simile al programma bersaglio.c
visto sopra ma con alcune differenze:
- Per prima cosa, il programma, tra una stampa e l'altra del messaggio potrà aspettare una quantità di tempo differente;
- La quantità di secondi di attesa sarà memorizzata in una variabile globale
secondi_attesa
inizialmente valorizzata a1
; - Quando il processo riceve il segnale
SIGUSR1
, la variabilesecondi_attesa
verrà incrementata di1
; - Quando il processo riceve il segnale
SIGUSR2
, la variabilesecondi_attesa
verrà decrementata di1
, fino al valore minimo di1
; - Infine, quando il processo riceve il segnale
SIGINT
, il processo terminerà.
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 43 44 45 46 47 48 49 50 |
|
Provando a compilare il programma ed eseguirlo, possiamo inviare i segnali SIGUSR1
, SIGUSR2
e SIGINT
al processo bersaglio per vedere come il programma si comporta.
$ ./bersaglio_handler_multi
PID: 1234
Attendo 1 secondi...
Attendo 1 secondi...
...
Inviamo, prima, il segnale SIGUSR1
al processo bersaglio:
$ kill -SIGUSR1 1234
Il processo stamperà:
Attendo 2 secondi...
Attendo 2 secondi...
...
Il tempo di attesa è aumentato di un secondo.
Inviamo, ora, il segnale SIGUSR2
al processo bersaglio:
$ kill -SIGUSR2 1234
Il processo stamperà:
Attendo 1 secondi...
Attendo 1 secondi...
...
Il tempo di attesa è diminuito di un secondo.
Infine, inviamo il segnale SIGINT
al processo bersaglio:
$ kill -SIGINT 1234
Il processo stamperà:
Uscita...
In Sintesi
In questa lezione abbiamo visto come gestire i segnali attraverso la funzione signal
. Abbiamo visto come registrare un handler di segnale personalizzato per un segnale specifico e come ripristinare l'handler precedente.
Abbiamo anche introdotto il concetto di handler di segnale e come esso interrompa il flusso principale del programma in seguito alla ricezione di un segnale.
Nella prossima lezione analizzeremo due modi che permettono ad un processo di inviare segnali a se stesso: raise
e killpg
.