6. Segnali
Ci sono vari eventi che possono avvenire in maniera asincrona al normale flusso di un programma, alcuni dei quali in maniera inaspettata e non predicibile. Per esempio, durante l’esecuzione di un programma ci può essere una richiesta di terminazione o di sospensione da parte di un utente, la terminazione di un processo figlio o un errore generico.
Unix prevede la gestione di questi eventi attraverso i segnali: quando il sistema operativo si accorge di un certo evento, genera un segnale da mandare al processo interessato il quale potrà decidere (nella maggior parte dei casi) come comportarsi.
Il numero dei segnali disponibili cambia a seconda del sistema operativo, con Linux che ne definisce 32. Ad ogni segnale corrisponde sia un valore numerico che un’etichetta mnemonica (definita nella libreria “signal.h”) nel formato SIGXXX. Alcuni esempi:
| SIGALRM (alarm clock) | SIGQUIT (terminal quit) |
| SIGCHLD (child terminated) | SIGSTOP (stop) |
| SIGCONT (continue, if stopped) | SIGTERM (termination) |
| SIGINT (terminal interrupt, CTRL + C) | SIGUSR1 (user signal) |
| SIGKILL (kill process) | SIGUSR2 (user signal) |
Visione concettuale
/%F0%9F%92%BE%20Sistemi%20Operativi/Laboratorio/_images/Segnali.png)
Implementazione
Per ogni processo, all’interno della process table, vengono mantenute due liste:
- Pending signals: segnali emessi dal kernel e che il processo deve ancora gestire. Un segnale viene definito in sospeso dal momento in cui viene generato al momento in cui viene consegnato.
- Blocked signals: segnali che non devono essere comunicati al processo. Chiamata anche con il termine signal mask, maschera dei segnali.
Ad ogni schedulazione del processo le due liste vengono controllate per consentire al processo di reagire nella maniera più adeguata.
Visione concettuale dei segnali
/%F0%9F%92%BE%20Sistemi%20Operativi/Laboratorio/_images/Implementazione%20segnali.png)
Gestione dei segnali
I segnali sono anche detti "software interrupts" perché sono, a tutti gli effetti, delle interruzioni del normale flusso del processo generate dal sistema operativo (invece che dall’hardware, come per gli hardware interrupts).
Come per gli interrupts, il programma può decidere come gestire l’arrivo di un segnale (presente nella lista pending):
- Eseguendo l’azione default.
- Ignorandolo (non sempre possibile) → programma prosegue normalmente.
- Eseguendo un handler personalizzato → programma si interrompe.
Nella pratica, il programma comunica al kernel come vuole che il segnale venga gestito, ed è poi il kernel che richiamerà la funzione adeguata del programma.
Default handler
Ogni segnale ha un suo handler di default che tipicamente può:
- Ignorare il segnale
- Terminare il processo
- Continuare l’esecuzione (se il processo era in stop)
- Stoppare il processo
Ogni processo può sostituire il gestore di default con una funzione “custom” (a parte per SIGKILL e SIGSTOP) e comportarsi di conseguenza. La sostituzione avviene
tramite la system call signal() (definita in "signal.h").
signal()
sighandler_t signal(int signum, sighandler_t handler);
Imposta un nuovo signal handler handler per il segnale signum. Restituisce il signal handler precedente. Quello nuovo può essere:
- SIG_DFL: handler di default
- SIG_IGN: ignora il segnale
- typedef void (*sighandler_t)(int): custom handler.
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
signal(SIGINT,SIG_IGN); //Ignore signal
signal(SIGCHLD,SIG_DFL); //Use default handler
return 0;
}
La funzione signal ha un valore di ritorno di tipo sighandler_t, che è un puntatore a funzione. Restituisce il gestore di segnali precedente per il segnale specificato come primo argomento.
sighandler_t prev_handler = signal(SIGINT, new_handler); // Imposta il nuovo gestore e salva il precedente
signal(SIGINT, prev_handler); // Ripristina il gestore precedente
Custom handler
Un custom handler deve essere una funzione di tipo void che accetta come argomento un int rappresentante il segnale catturato. Questo consente allo stesso handlers di gestire segnali diversi.
Definiamo quindi una funzione custom di tipo void:
#include <signal.h>
#include <stdio.h>
void myHandler(int sigNum)
{
if(sigNum == SIGINT)
{
printf("CTRL+C\n");
}
else if(sigNum == SIGTSTP)
{
printf("CTRL+Z\n");
}
}
A questo punto, chiamiamo la funzione signal e ridefiniamo, per esempio, SIGINT e SIGTSTP:
signal(SIGINT,myHandler);
signal(SIGTSTP,myHandler);
Esempio 1.
Dato il seguente script in C chiamato "sigCST.c"
//sigCST.c
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
void myHandler(int sigNum)
{
printf("CTRL+Z\n");
exit(2);
}
int main(void)
{
signal(SIGTSTP,myHandler);
while(1);
}
compiliamolo ed eseguiamolo con i seguenti comandi
$ gcc sigCST.c -o sig.out
$ ./sig.out
$ <CTRL+Z>
Questo handler personalizzato rimane in attesa, mediande while(1), di ricevere il segnale SIGTSTP. Quando il segnale viene ricevuto, la funzione stampa una stringa "CTRL+Z" e termina il programma con un valore di uscita di 2. Ovviamente il segnale SIGTSTP viene ridefinito con myHandler solo all'interno dell'esecuzione del programma, per questo abbiamo inserito while(1).
Esempio 2.
Dato il seguente script in C chiamato "sigDFL.c"
#include <signal.h> //sigDFL.c
int main()
{
signal(SIGTSTP,SIG_DFL);
while(1);
}
compiliamolo ed eseguiamolo con i seguenti comandi
$ gcc sigDFL.c -o sig.out
$ ./sig.out
$ <CTRL+Z>
Questo codice utilizza il gestore dei segnali predefinito (SIG_DFL) per il segnale SIGTSTP utilizzando la funzione di libreria "signal()" di C. Quindi premendo CTRL+Z non si verificherà la terminazione del programma, ma verrà ripristinato il comportamento predefinito del segnale SIGTSTP, che nel caso specifico interrompe l'esecuzione del programma e lo mette in pausa.
L'output generato, per esempio
[2]+ Stopped ./sigIGN.out
Indica che il processo è stato messo in pausa ed è stato assegnato un job number 2. Il messaggio "Stopped" indica che il processo è stato fermato a causa del segnale SIGTSTP.
Per riprendere l'esecuzione del programma è possibile utilizzare il comando "fg" (foreground) o "bg" (background) seguito dal job number del processo.
Esempio 3.
Dato il seguente script in C chiamato "sigIGN.c"
#include <signal.h> //sigIGN.c
int main()
{
signal(SIGTSTP,SIG_IGN);
while(1);
}
compiliamolo ed eseguiamolo con i seguenti comandi
$ gcc sigIGN.c -o sig.out
$ ./sig.out
$ <CTRL+Z>
Il programma imposta il gestore dei segnali a SIG-IGN, il che significa che il programma ignorerà il segnale SIGTSTP, che è il segnale che viene inviato dal sistema operativo quando si preme CTRL+Z durante l'esecuzione di un programma. Questo porterà il programma ad eseguire continuamente il ciclo while senza interrompersi.
signal() return
signal() restituisce un riferimento all’handler che era precedentemente assegnato al segnale:
- NULL: handler precedente era l’handler di default
- 1: l’handler precedente era SIG_IGN
- <address>: l’handler precedente era *(address)
Per esempio, dato il seguente script in C
#include <signal.h>
#include <stdio.h> //return.c
void myHandler(int sigNum){}
int main()
{
printf("DFL: %p\n",signal(SIGINT,SIG_IGN));
printf("IGN: %p\n",signal(SIGINT,myHandler));
printf("Custom: %p == %p\n",signal(SIGINT,SIG_DFL),myHandler);
}
L'output è il seguente:
DFL: 0x0
IGN: 0x1
Custom: 0x10b138ee0 == 0x10b138ee0
Alcuni segnali
| SIGXXX | description | default |
| SIGALRM | (alarm clock) | quit |
| SIGCHLD | (child terminated) | ignore |
| SIGCONT | (continue, if stopped) | ignore |
| SIGINT | (terminal interrupt, CTRL + C) | quit |
| SIGKILL | (kill process) | quit |
| SIGSYS | (bad argument to syscall) | quit with dump |
| SIGTERM | (software termination) | quit |
| SIGUSR1/2 | (user signal 1/2) | quit |
| SIGSTOP | (stopped) | quit |
| SIGTSTP | (terminal stop, CTRL + Z) | quit |
Esempio
Dato il seguente script in C
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h> //child.c
void myHandler(int sigNum) // Il gestore viene chiamato quando il processo padre riceve il segnale SIGCHLD
{
printf("Child terminated! Received %d\n",sigNum); // Stampa un messaggio che indica che il processo figlio è stato terminato e il segnale ricevuto. Quindi il processo padre esce dalla funzione wait() e termina la sua esecuzione
}
int main()
{
signal(SIGCHLD,myHandler); // Configura un gestore di segnali per gestire SIGCHLD
int child = fork(); // Crea un processo figlio
if(!child)
{
return 0; // Il figlio termina e quindi il segnale SIGCHLD viene inviato al processo padre
}
while(wait(NULL)>0); // La funzione "wait()" è utilizzata per bloccare il processo padre fino a quando il processo figlio non termina. La funzione "wait()" ritorna -1 quando non ci sono più processi figli da attendere
}
L'output è il seguente:
Child terminated! Received 20
kill()
La funzione kill() viene utilizzata per inviare un segnale specifico a un processo identificato dal suo PID (Process ID). La sua firma è la seguente:
int kill(pid_t pid, int sig);
$ kill -<signo> <pid_t>
Questa funzione invia un segnale ad uno o più processi a seconda dell’argomento pid:
- pid > 0: segnale al processo con PID=pid
- pid = 0: segnale ad ogni processo dello stesso gruppo
- pid = -1: segnale ad ogni processo possibile (stesso UID/RUID) per i quali il processo che invia il segnale ha il permesso di inviare il segnale. Ciò significa che i processi di altri utenti non riceveranno il segnale inviato con "pid" uguale a -1
- pid < -1: segnale ad ogni processo del gruppo il cui ID è uguale al valore assoluto di pid. In questo caso, il processo che invia il segnale deve avere i privilegi di root
Restituisce 0 se il segnale viene inviato, -1 in caso di errore.
Ci sono molti tipi di segnali disponibili e ognuno di essi ha un significato specifico, come SIGINT che viene utilizzato per interrompere un processo in esecuzione. Tuttavia, non tutti i segnali sono necessariamente generati da un evento specifico e quindi è possibile inviare un segnale a un processo in qualsiasi momento, indipendentemente dal fatto che sia o meno correlato ad un evento effettivamente accaduto.
Esempio
Dato il seguente script in C
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
//kill.c
void myHandler(int sigNum) // Appena il gestore del figlio riceve il segnale stampa un messaggio di allarme
{
printf("[%d]ALARM!\n",getpid());
}
int main(void)
{
signal(SIGALRM,myHandler); // Configura un gestore di segnali per gestire SIGALARM
int child = fork(); // Il valore di ritorno è 0 per il processo figlio
if (!child) // Nel caso del figlio avremo !0 e quindi 1, per cui è vero ed entra nell'if. Nel caso del padre, che avrà qualsiasi altro valore diverso da 0, l'if verrà sempre saltato
{
while(1); // Blocca il processo figlio in un ciclo infinito
}
printf("[%d]sending alarm to %d in 3 s\n",getpid(),child);
sleep(3); // Il processo padre attende per 3 secondi e poi invia un segnale SIGALRM al processo figlio utilizzando la funzione "kill"
kill(child,SIGALRM); // Invia il segnale SIGALRM, il quale ha un gestore di segnale (impostato in precedenza)
printf("[%d]sending SIGTERM to %d in 3 s\n",getpid(),child);
sleep(3); // Dopo ulteriori 3 secondi, il processo padre invia un segnale SIGTERM al processo figlio utilizzando la funzione "kill"
kill(child,SIGTERM); // Invia il segnale TERM, che di default fa terminare il processo figlio
while(wait(NULL)>0); // Il processo padre attende il termine del processo figlio utilizzando la funzione wait()
}
L'output è simile al seguente:
[1234]sending alarm to 1235 in 3 s
[1235]ALARM!
[1234]sending SIGTERM to 1235 in 3 s
Ogni sistema operativo decide come eseguire il gestore di segnale, quindi le ultime due righe potrebbero anche essere invertite.
Kill da bash
kill è anche un programma in bash che accetta come primo argomento il tipo di segnale (kill -l per la lista) e come secondo argomento il PID del processo.
Esempio
Dato il seguente script in C chiamato "bash.c"
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h> //bash.c
void myHandler(int sigNum)
{
printf("[%d]ALARM!\n",getpid()); // Da linea di comando viene usato kill -14 <PID> (14 corrisponde all'invio di un segnale SIGALRM)
exit(0); // Una volta stampato il segnale, termina il processo
}
int main()
{
signal(SIGALRM,myHandler);
printf("I am %d\n",getpid());
while(1); // Rimane nel loop infinito finché non viene inviato il segnale SIGALRM
}
compiliamolo ed eseguiamolo con i seguenti comandi
$ gcc bash.c -o bash.out
$ ./bash.out
Ora eseguiamo il seguente comando in un nuovo terminale
$ kill -14 <PID> #14 corrisponde all'invio di un segnale SIGALRM
L'output è simile al seguente:
I am 5971
5971 ALARM!
SIGKILL
Per terminare un processo in esecuzione in background si può utilizzare il comando:
kill -9 %n
Il 9 è utilizzato per inviare al processo il segnale SIGKILL, mentre al posto di n si utilizza il job number del processo. Il carattere "%" indica che si sta facendo riferimento a un job number invece che a un PID (Process IDentifier).
- Il "job number" viene comunemente utilizzato nei sistemi Unix e Unix-like, come ad esempio Linux, per identificare i processi all'interno di una shell interattiva. Quando si avviano processi nella shell, viene assegnato loro un numero di job progressivo, iniziando da 1, per tener traccia di essi. Il job number viene utilizzato principalmente per riferirsi ai processi all'interno della shell stessa, ad esempio per inviare segnali a un processo specifico o per gestire i processi in background o in foreground.
- Il "PID" (Process ID), invece, è un identificatore univoco assegnato a ogni processo attivo nel sistema operativo. Il PID viene utilizzato dal sistema operativo per gestire i processi e tenere traccia di essi. Ogni volta che un nuovo processo viene avviato, gli viene assegnato un PID univoco che lo identifica. Il PID viene utilizzato per riferirsi ai processi da parte del sistema operativo, ad esempio per inviare segnali, gestire le priorità o terminare i processi.
Set up an alarm: alarm()
La funzione alarm() genera un segnale SIGALRM per il processo corrente dopo un lasso di tempo specificato in secondi. Restituisce i secondi rimanenti all'alarm precedente. La sua firma è la seguente:
unsigned int alarm(unsigned int seconds);
Esempio
Dato il seguente script in C chiamato "alarm.c"
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h> //alarm.c
short cnt = 0;
void myHandler(int sigNum)
{
printf("ALARM!\n"); cnt++;
}
int main()
{
signal(SIGALRM,myHandler);
alarm(0); // Cancella eventuali alarm che erano stati impostati e che non sono ancora scaduti, quindi il segnale SIGALRM potrebbe ancora essere in attesa di essere inviato
alarm(5); // Set alarm in 5 seconds
// In questo caso è inutile impostare alarm(0) perché se dopo si inizializza un alarm(n) quelli precedenti vengono cancellati. Usare alarm(0) è solo una buona pratica di programmazione
printf("Seconds remaining to previous alarm %d\n",alarm(2)); // Avvisa qual'era il valore dell'alarm precedente, ma sovrascrive quello precedente
while(cnt<1);
}
L'output è il seguente:
Seconds remaining to previous alarm 5
ALARM! #dopo 2 secondi dal printf
Alarm può interferire con sleep quindi è meglio usare o uno o l'altro. Questo accade perché entrambi usano il segnale SIGALRM.
Mettere in pausa: pause()
La funzione pause() è utilizzata per sospendere l'esecuzione di un programma fino a quando non viene ricevuto un segnale. La sua firma è la seguente:
int pause();
Esempio
Dato il seguente script in C chiamato "pause.c"
//pause.c
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
void myHandler(int sigNum)
{
printf("Continue!\n"); // Quando viene ricevuto il segnale SIGCONT (kill -18 <PID>) oppure SIGUSR1 (kill -10 <PID>) il programma riprende l'esecuzione ed esegue l'handler personalizzato. Se invece si invia SIGUSR2 per esempio (kill -12 <PID>) verrà usato l'handler di default
}
int main()
{
// Imposta due gestori di segnali che vengono eseguiti quando ricevono il segnale corrispondente
signal(SIGCONT,myHandler);
signal(SIGUSR1,myHandler);
pause(); // Il programma viene sospeso
}
compiliamolo ed eseguiamolo con i seguenti comandi
$ gcc pause.c -o pause.out
$ ./pause.out
Ora eseguiamo il seguente comando in un nuovo terminale
$ kill -18/-10 <PID>
L'output è simile al seguente:
I am 5971
Continue!
Bloccare i segnali
I processi nel sistema operativo utilizzano due liste per gestire i segnali: la lista dei "pending signals" e la lista dei "blocked signals". Quando un segnale viene inviato a un processo, il kernel decide se consegnarlo al processo o meno. Se il segnale viene consegnato, il segnale viene inserito nella lista dei "pending signals" per quel processo e viene gestito dal gestore di segnale registrato per quel segnale.
Tuttavia, un processo ha la possibilità di bloccare alcuni segnali utilizzando la "signal mask". In questo caso, quando un segnale viene bloccato, non può essere consegnato al gestore di segnale registrato per quel segnale. Il segnale viene invece aggiunto alla lista dei "blocked signals", rimanendo temporaneamente non gestito. Ciò significa che i segnali bloccati rimangono nello stato di attesa fino a quando non vengono sbloccati.
Al contrario, se un segnale viene ignorato, il segnale non viene inserito nella lista dei "pending signals" e quindi non viene mai gestito dal gestore di segnale registrato per quel segnale. Il segnale viene ignorato completamente senza alcuna notifica al processo.
È importante notare che se un segnale viene bloccato, il kernel non lo consegnerà al gestore di segnale registrato per quel segnale finché non viene sbloccato. Tuttavia, il segnale rimane nella lista dei "pending signals" per quel processo, in attesa di essere consegnato e gestito una volta che viene sbloccato. Al contrario dei segnali ignorati, un segnale bloccato rimane nello stato "pending" fino a quando non viene gestito o il suo handler viene trasformato in ignore.
Per modificare la propria signal mask, ovvero l'elenco dei segnali che un processo ha deciso di bloccare, è possibile utilizzare la funzione sigprocmask(), che modifica la signal mask del processo nel kernel.
In sintesi, la lista dei "pending signals" rappresenta i segnali che sono stati consegnati al processo ma che non sono ancora stati gestiti, mentre la lista dei "blocked signals" rappresenta i segnali che il processo ha deciso di bloccare temporaneamente. L'utilizzo della signal mask consente al processo di controllare quali segnali possono essere consegnati e quali non possono essere consegnati al gestore di segnale.
Visione concettuale
/%F0%9F%92%BE%20Sistemi%20Operativi/Laboratorio/_images/Segnali-2.png)
La signal mask
/%F0%9F%92%BE%20Sistemi%20Operativi/Laboratorio/_images/SignalMask.png)
sigset_t
La signal mask viene memorizzata come struttura dati ottimizzata che non può essere modificata direttamente. Invece, può essere gestita attraverso un sigset_t, cioè una struttura dati locale contenente un elenco di segnali. Questo insieme può essere modificato con funzioni dedicate e poi può essere utilizzato per modificare la maschera di segnale stessa.
int sigemptyset(sigset_t *set); Svuota
int sigfillset(sigset_t *set); Riempie
int sigaddset(sigset_t *set, int signo); Aggiunge singolo
int sigdelset(sigset_t *set, int signo); Rimuove singolo
int sigismember(const sigset_t *set, int signo); Interpella
La modifica di questa struttura non modifica implicitamente la maschera dei segnali! Le modifiche devono essere salvate con sigprocmask().
sigprocmask()
La funzione sigprocmask() viene utilizzata per modificare il set di maschere dei segnali del processo corrente. Il set di maschere dei segnali indica quali segnali sono temporaneamente bloccati o consentiti per il processo. La sua firma è la seguente:
int sigprocmask(int how, const sigset_t *restrict set, sigset_t *restrict oldset);
A seconda del valore di how e di set, la maschera dei segnali del processo viene cambiata. Nello specifico:
- how = SIG_BLOCK: i segnali in set sono aggiunti alla maschera;
- how = SIG_UNBLOCK: i segnali in set sono rimossi dalla maschera;
- how = SIG_SETMASK: set diventa la maschera.
Set è un puntatore ad una struttura dati di tipo sigset_t che rappresenta l'insieme di segnali da modificare nella maschera dei segnali corrente. Se questo parametro è NULL, la maschera dei segnali corrente non viene modificata.
Oldset è un puntatore ad una struttura dati di tipo sigset_t dove verrà copiata la vecchia maschera dei segnali corrente (anche se set è nullo). Se questo parametro è NULL, la vecchia maschera dei segnali non viene copiata.
La funzione sigprocmask() restituisce 0 in caso di successo e -1 in caso di errore, impostando errno di conseguenza (variabile globale utilizzata per segnalare gli errori avvenuti durante l'esecuzione di una funzione di sistema o di libreria in C).
Esempio 1.
#include <signal.h>
int main()
{
sigset_t mod,old;
sigfillset(&mod); // Add all signals to the blocked list
sigemptyset(&mod); // Remove all signals from blocked list
sigaddset(&mod,SIGALRM); // Add SIGALRM to blocked list
sigismember(&mod,SIGALRM); // is SIGALRM in blocked list?
sigdelset(&mod,SIGALRM); // Remove SIGALRM from blocked list
sigprocmask(SIG_BLOCK,&mod,&old); // Aggiornamento della maschera dei segnali correnti con i segnali presenti in mod
}
Esempio 2.
Dato il seguente script in C chiamato "sigprocmask.c"
#include <signal.h>
#include <unistd.h>
#include <stdio.h> //sigprocmask.c
sigset_t mod, old;
int i = 0;
void myHandler(int signo)
{
printf("signal received\n"); // Quando viene ricevuto il segnale SIGUSR1 (tramite kill -10 <PID>) per la prima volta, viene incrementato il valore del contatore ed il segnale SIGUSR1 viene bloccato. Verrà consegnato al processo solo quando verra sbloccato tramite una chiamata a sigprocmask() ed impostando il primo argomento a SIG_UNBLOCK (il resto dei parametri è lo stesso)
i++;
}
int main()
{
printf("my id = %d\n",getpid()); // Stampa l'id del processo corrente
signal(SIGUSR1,myHandler); // Registra il segnale SIGUSR1 per essere gestito dalla funzione myHandler() tramite la funzione signal()
sigemptyset(&mod); // Inizializza una maschera di segnali e setta tutti i segnali come non appartenenti alla maschera stessa (svuota la maschera). I segnali che non appartengono alla maschera non sono bloccati e possono essere ricevuti e gestiti dal processo, mentre quelli al suo interno sono i segnali bloccati
sigaddset(&mod,SIGUSR1); // Aggiunge il segnale SIGUSR1 alla maschera
while(1) if(i==1) sigprocmask(SIG_BLOCK,&mod,&old); // quando il valore del contatore raggiunge 1 viene chiamata la funzione sigprocmask() e quindi i segnali specificati in set (in questo caso mod) vengono aggiunti alla maschera dei segnali bloccati (perché how è impostato a SIG_BLOCK) per bloccare il segnale SIGUSR1, impostando l'insieme mod come maschera dei segnali bloccati e memorizzando l'insieme precedente in old
}
compiliamolo ed eseguiamolo con i seguenti comandi
$ gcc sigprocmask.c -o sigprocmask.out
$ ./sigprocmask.out
Ora eseguiamo il seguente comando in un nuovo terminale
$ kill -10 <PID> # ok
$ kill -10 <PID> # blocked
L'output è il seguente:
my id = 788
signal received #dopo il primo kill -10
#da qui in poi tutti i kill -10 788 verranno ignorati
Verificare pending signals: sigpending()
La funzione sigpending() restituisce l'insieme dei segnali bloccati che sono stati ricevuti ma non ancora gestiti dal processo corrente. La firma della funzione è la seguente:
int sigpending(sigset_t *set);
Esempio
Dato il seguente script in C chiamato "sigprocmask.c"
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
sigset_t mod,pen;
void handler(int signo)
{
printf("SIGUSR1 received\n");
sigpending(&pen); // Recupera l'insieme dei segnali pendenti e li memorizza in pen (viene sovrascritto pen)
if(!sigismember(&pen,SIGUSR1)) // Dato che SIGUSR1 non è più contenuto in pen viene eseguito il printf
{
printf("SIGUSR1 not pending\n");
}
exit(0);
}
int main()
{
/* Fa gestire il segnale SIGUSR1 a handler */
signal(SIGUSR1,handler);
/* Svuota la maschera dei segnali mod */
sigemptyset(&mod);
/* Aggiunge il segnale SIGUSR1 alla maschera */
sigaddset(&mod,SIGUSR1);
/* I segnali presenti in mod vengono aggiunti alla lista dei segnali bloccati
*/
sigprocmask(SIG_BLOCK,&mod,NULL);
/*
Invia un segnale SIGUSR1 al processo corrente identificato tramite il suo
PID (restituito da getpid), ma potrebbe anche essere un gruppo di
processi. Il segnale è inviato ma viene bloccato.
*/
kill(getpid(),SIGUSR1);
/*
Controlla se ci sono segnali in attesa di essere gestiti nel processo
corrente e li salva nella struttura dati pen
*/
sigpending(&pen);
/*
Controlla se un segnale specifico (in questo caso SIGUSR1) appartiene
all'insieme di segnali in pen.
Se appartiene all'insieme la funzione restituisce un valore diverso da 0,
altrimenti restituisce 0.
*/
if(sigismember(&pen,SIGUSR1))
printf("SIGUSR1 pending\n"); // Dato che non è 0 esegue la stampa
/*
L'insieme dei segnali in mod verrà rimosso dall’insieme corrente dei
segnali bloccati (in questo caso viene sbloccato solo SIGUSR1).
Dopodiché il segnale SIGUSR1, precedentemente inviato ma bloccato
verrà consegnato al processo e la funzione handler verrà eseguita.
*/
sigprocmask(SIG_UNBLOCK,&mod,NULL);
while(1);
}
L'output sarà il seguente:
SIGUSR1 pending
SIGUSR1 received
SIGUSR1 not pending
sigaction() system call
La funzione sigaction consente di esaminare e/o modificare l’azione associata a un segnale specifico. La firma della funzione è la seguente:
int sigaction(int signum, const struct sigaction *restrict act, struct sigaction *restrict oldact);
La funzione accetta tre argomenti:
- signum: il numero del segnale per il quale si desidera esaminare o modificare l’azione
- act: un puntatore a una struttura sigaction che specifica la nuova azione per il segnale. Se questo argomento è NULL, l’azione del segnale non viene modificata
- oldact: un puntatore a una struttura sigaction in cui verrà memorizzata l’azione corrente del segnale. Se questo argomento è NULL, l’azione corrente del segnale non viene restituita
La struttura sigaction contiene diversi campi che consentono di specificare l’azione da intraprendere quando viene ricevuto un segnale.
struct sigaction
{
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask; //Signals blocked during handler
int sa_flags; //modify behaviour of signal
void (*sa_restorer)(void); //Deprecated, not POSIX
};
Vediamoli ora in dettaglio:
- sa_handler: un puntatore a una funzione che verrà chiamata quando viene ricevuto il segnale. Questo campo può anche essere impostato su SIG_IGN per ignorare il segnale o su SIG_DFL per ripristinare l’azione predefinita del segnale
- sa_sigaction: un puntatore a una funzione che verrà chiamata quando viene ricevuto il segnale e che accetta tre argomenti: il numero del segnale, un puntatore a una struttura siginfo_t contenente informazioni aggiuntive sul segnale e un puntatore a un contesto di esecuzione. Questo campo viene utilizzato solo se il campo sa_flags contiene il flag SA_SIGINFO
- sa_mask: un insieme di segnali che verranno bloccati durante l’esecuzione della funzione specificata nei campi sa_handler o sa_sigaction
- sa_flags: un insieme di flag che modificano il comportamento del segnale. Ad esempio, il flag SA_RESTART fa in modo che le chiamate di sistema interrotte dal segnale vengano riprese automaticamente, mentre il flag SA_SIGINFO indica che la funzione specificata nel campo sa_sigaction deve essere utilizzata invece della funzione specificata nel campo sa_handler
- sa_restorer: questo campo è deprecato e non fa parte dello standard POSIX
In sintesi, la funzione sigaction consente di esaminare e/o modificare l’azione associata a un segnale specifico utilizzando la struttura sigaction
Esempio 1.
Dato il seguente script in C chiamato "sigaction.c"
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h> //sigaction.c
void handler(int signo)
{
printf("signal received\n");
}
int main()
{
struct sigaction sa; // Define sigaction struct
sa.sa_handler = handler; // Assign handler to struct field
sigemptyset(&sa.sa_mask); // Define an empty mask (la svuota)
sigaction(SIGUSR1,&sa,NULL); // Cambia l'azione intrapresa dal processo quando riceve il segnale SIGUSR1, &sa è un puntatore a una struct sigaction che specifica la nuova azione da intraprendere (in questo caso la struct sa è stata impostata in modo che punti alla funzione handler) e NULL indica che non si desidera ottenere informazioni sull'azione precedente associata al segnale. Quando il processo riceve il segnale SIGUSR1 verrà chiamata la funzione handler
kill(getpid(),SIGUSR1); // Invia il segnale SIGUSR1 a se stesso
}
L'output è il seguente:
signal received
Esempio 2 - blocking signal.
Dato il seguente script in C chiamato "sigaction2.c"
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h> //sigaction2.c
void handler(int signo)
{
printf("signal %d received\n",signo);
sleep(2);
printf("Signal done\n");
}
int main()
{
printf("Process id: %d\n",getpid()); // Stampa l'ID del processo
struct sigaction sa; // Crea una struct sigaction
sa.sa_handler = handler; // Imposta la struttura sa per specificare che la funzione handler dovrebbe essere chiamata quando viene ricevuto il segnale SIGUSR1
sigemptyset(&sa.sa_mask); // Svuota la mask e quindi nessun segnale verrà bloccato mentre il gestore del segnale è in esecuzione
sigaction(SIGUSR1,&sa,NULL); // Imposta il gestore del segnale per SIGUSR1
while(1); // Attende che i segnali vengano ricevuti
}
compiliamolo ed eseguiamolo con i seguenti comandi
$ gcc sigaction2.c -o sigaction2.out
$ ./sigaction2.out
Ora eseguiamo il seguente comando in un nuovo terminale
$ kill -10 <PID> ; sleep 1 && kill -12 <PID>
L'output è simile al seguente:
Process id: 21444
signal 10 received
Segnale 2 definito dall'utente
L'ultima riga dell'output viene mostrata perchè viene eseguito l'handler di default di SIGUSR2. In questo caso il segnale SIGUSR2 non viene bloccato durante l’esecuzione del gestore del segnale per SIGUSR1 e quindi, quando il segnale SIGUSR2 viene inviato al processo mentre il gestore del segnale per SIGUSR1 è in esecuzione, l’esecuzione del gestore del segnale per SIGUSR1 viene interrotta e il processo viene terminato dal gestore predefinito per SIGUSR2 (non mostra Signal done)
Esempio 3 - blocking signal.
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h> //sigaction3.c
void handler(int signo)
{
printf("signal %d received\n",signo);
sleep(2);
printf("Signal done\n");
}
int main()
{
printf("Process id: %d\n",getpid());
struct sigaction sa;
sa.sa_handler = handler;
sigemptyset(&sa.sa_mask);
sigaddset(&sa.sa_mask,SIGUSR2); // Aggiunge il segnale SIGUSR2 all'insieme sa.sa_mask e quindi, quando il gestore del segnale per SIGUSR1 è in esecuzione, la consegna del segnale SIGUSR2 verrà bloccata
sigaction(SIGUSR1,&sa,NULL);
while(1);
}
$ kill -10 <PID> ; sleep 1 && kill -12 <PID>
L'output è simile al seguente
Process id: 22404
signal 10 received
Signal done
Segnale 2 definito dall'utente #In questo caso il segnale SIGUSR2 è stato inviato durante l'esecuzione del gestore del segnale per SIGUSR1, ma è stato consegnato solo dopo che il gestore ha completato l’esecuzione
Esempio 4 - sa_sigaction.
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h> //sigaction4.c
void handler(int signo, siginfo_t * info, void * empty) // Viene chiamata quando il processo riceve il segnale SIGUSR1
{
printf("Signal received: %i\n", signo);
printf("Sender pid: %i\n", si->si_pid); // Stampa l'ID del processo che ha inviato il segnale utilizzando il campo si_pid della struttura siginfo_t
}
int main()
{
struct sigaction sa;
sigemptyset(&sa.sa_mask);
sa.sa_flags |= SA_SIGINFO; // Il flag SA_SIGINFO viene impostato nella struttura sa per indicare che il gestore del segnale deve essere chiamato con tre argomenti invece di uno solo
sa.sa_flags |= SA_RESETHAND; // Viene impostato anche il flag SA_RESETHAND nella struttura sa per indicare che dopo la prima ricezione del segnale SIGUSR1 il gestore del segnale deve essere ripristinato al gestore predefinito
sa.sa_sigaction = handler;
sigaction(SIGUSR1,&sa,NULL);
while(1);
}
$ echo $ ; kill -10 <PID> # custom
$ kill -10 <PID> # default
Conclusioni
I segnali sono uno strumento di comunicazione tra processi molto semplice ma efficace: essendo un metodo molto “antico” non è particolarmente flessibile (essendo nato quando le risorse erano più limitate dei tempi attuali), ma nella sua forma base è comodo e multipiattaforma. L’uso di “signal” è standard, ma alcuni comportamenti possono essere indefiniti, mentre “sigaction” è più affidabile ma meno portabile tra sistemi operativi.