Nella parte 1 della serie Linux Signals, abbiamo appreso i concetti fondamentali alla base dei segnali Linux.
Basandosi sulla parte precedente, in questo articolo impareremo come catturare i segnali in un processo. Presenteremo l'aspetto pratico della gestione del segnale usando frammenti di codice di programma C.
Catturare un segnale
Come già discusso nell'articolo precedente, se un processo desidera gestire determinati segnali nel codice, il processo deve registrare una funzione di gestione dei segnali nel kernel.
Quello che segue è il prototipo di una funzione di gestione del segnale:
void <signal handler func name> (int sig)
La funzione di gestione del segnale ha un tipo di ritorno nullo e accetta un numero di segnale corrispondente al segnale che deve essere gestito.
Per ottenere la funzione del gestore del segnale registrata nel kernel, il puntatore della funzione del gestore del segnale viene passato come secondo argomento alla funzione 'segnale'. Il prototipo della funzione segnale è :
void (*signal(int signo, void (*func )(int)))(int);
Questa potrebbe sembrare una dichiarazione complicata. Se proviamo a decodificarlo :
- La funzione richiede due argomenti.
- Il primo argomento è un numero intero (signo) che rappresenta il numero del segnale o il valore del segnale.
- Il secondo argomento è un puntatore alla funzione di gestione del segnale che accetta un intero come argomento e non restituisce nulla (void).
- Mentre la funzione 'segnale' stessa restituisce un puntatore a funzione il cui tipo restituito è void.
Bene, per rendere le cose più facili, usiamo typedef :
typedef void sigfunc(int)
Quindi, qui abbiamo creato un nuovo tipo "sigfunc". Ora usando questo typedef, se riprogettiamo il prototipo del gestore del segnale :
sigfunc *signal(int, sigfunc*);
Ora vediamo che è più facile comprendere che la funzione di gestione del segnale accetta un intero e un puntatore a funzione di tipo sigfunc mentre restituisce un puntatore a funzione di tipo sigfunc.
Esempio di programma C per catturare un segnale
La maggior parte degli utenti Linux usa la combinazione di tasti Ctr+C per terminare i processi in Linux.
Hai mai pensato a cosa c'è dietro questo. Ebbene, ogni volta che si preme ctrl+c, al processo viene inviato un segnale SIGINT. L'azione predefinita di questo segnale è terminare il processo. Ma anche questo segnale può essere gestito. Il codice seguente lo dimostra:
#include<stdio.h> #include<signal.h> #include<unistd.h> void sig_handler(int signo) { if (signo == SIGINT) printf("received SIGINT\n"); } int main(void) { if (signal(SIGINT, sig_handler) == SIG_ERR) printf("\ncan't catch SIGINT\n"); // A long long wait so that we can easily issue a signal to this process while(1) sleep(1); return 0; }
Nel codice sopra, abbiamo simulato un processo di lunga durata utilizzando un ciclo while infinito.
Viene utilizzata una funzione sig_handler come un gestore di segnali. Questa funzione viene registrata nel kernel passandola come secondo argomento della chiamata di sistema 'signal' nella funzione main(). Il primo argomento della funzione 'segnale' è il segnale che intendiamo gestire dal gestore del segnale, che in questo caso è SIGINT.
Una nota a margine, l'uso della funzione sleep(1) ha una ragione dietro. Questa funzione è stata utilizzata nel ciclo while in modo che il ciclo while venga eseguito dopo un po' di tempo (cioè un secondo in questo caso). Questo diventa importante perché altrimenti un ciclo while infinito in esecuzione selvaggiamente potrebbe consumare la maggior parte della CPU rendendo il computer molto molto lento.
Comunque, tornando indietro, quando il processo viene eseguito e proviamo a terminare il processo usando Ctrl+C:
$ ./sigfunc ^Creceived SIGINT ^Creceived SIGINT ^Creceived SIGINT ^Creceived SIGINT ^Creceived SIGINT ^Creceived SIGINT ^Creceived SIGINT
Nell'output precedente vediamo che abbiamo provato più volte la combinazione di tasti ctrl+c ma ogni volta il processo non è terminato. Questo perché il segnale è stato gestito nel codice e questo è stato confermato dalla stampa che abbiamo ottenuto su ogni riga.
SIGKILL, SIGSTOP e Segnali definiti dall'utente
Oltre a gestire i segnali standard (come INT, TERM ecc.) che sono disponibili. Possiamo anche avere segnali definiti dall'utente che possono essere inviati e gestiti. Di seguito è riportato il codice che gestisce un segnale definito dall'utente USR1 :
#include<stdio.h> #include<signal.h> #include<unistd.h> void sig_handler(int signo) { if (signo == SIGUSR1) printf("received SIGUSR1\n"); else if (signo == SIGKILL) printf("received SIGKILL\n"); else if (signo == SIGSTOP) printf("received SIGSTOP\n"); } int main(void) { if (signal(SIGUSR1, sig_handler) == SIG_ERR) printf("\ncan't catch SIGUSR1\n"); if (signal(SIGKILL, sig_handler) == SIG_ERR) printf("\ncan't catch SIGKILL\n"); if (signal(SIGSTOP, sig_handler) == SIG_ERR) printf("\ncan't catch SIGSTOP\n"); // A long long wait so that we can easily issue a signal to this process while(1) sleep(1); return 0; }
Vediamo che nel codice sopra, abbiamo provato a gestire un segnale definito dall'utente USR1. Inoltre, poiché sappiamo che due segnali KILL e STOP non possono essere gestiti. Quindi abbiamo anche provato a gestire questi due segnali in modo da vedere come risponde la chiamata di sistema "segnale" in questo caso.
Quando eseguiamo il codice sopra:
$ ./sigfunc can't catch SIGKILL can't catch SIGSTOP
Quindi l'output di cui sopra chiarisce che non appena il "segnale" della chiamata di sistema tenta di registrare il gestore per i segnali KILL e STOP, la funzione del segnale fallisce indicando che questi due segnali non possono essere catturati.
Ora proviamo a passare il segnale USR1 a questo processo usando il comando kill:
$ kill -USR1 2678
e sul terminale in cui è in esecuzione il programma di cui sopra vediamo:
$ ./sigfunc can't catch SIGKILL can't catch SIGSTOP received SIGUSR1
Quindi vediamo che il segnale definito dall'utente USR1 è stato ricevuto nel processo ed è stato gestito correttamente.