In UNIX, i segnali sono un meccanismo di comunicazione tra processi utilizzato per notificare a un processo l’occorrenza di un evento specifico, come un errore, un’interruzione dell’utente o un’operazione speciale.

1 - Nomi simbolici dei segnali

Ogni segnale è associato univocamente a:

  • Un intero, a partire dall’1.
  • Un nome simbolico, della forma SIG***.

Poiché gli effettivi interi assegnati a ogni segnale variano a seconda delle implementazioni nei vari sistemi operativi, all’interno dei programmi è meglio utilizzare direttamente i nomi simbolici.

Consiglio

È possibile verificare la numerazione dei segnali nel proprio sistema operativo eseguendo il comando:

kill -l

2 - Tipi di segnali in UNIX

In UNIX, i segnali si dividono nelle seguenti categorie:

  • Trap: segnali generati da un processo e inviati al processo stesso.
  • Interrupt: segnali generati da un agente esterno (es. utente, altro processo) a un processo.

2.1 - Trap

In UNIX, le trap sono segnali generati da eventi prodotti da un processo e inviati al processo stesso. Alcune trap sono causate da comportamenti errati del processo stesso, e immediatamente inviate al processo che normalmente reagisce terminando.

Sono esempi di trap i tentativi di divisione per zero (SIGFPE), indirizzamento errato degli array (SIGSEGV), tentativo di eseguire istruzioni privilegiate (SIGILL), ecc.

2.2 - Interrupt

In UNIX, gli interrupt sono segnali inviati ad un processo da un agente esterno (come l’utente o un altro processo).

Esempi di interrupt inviati dall’utente sono la pressione delle combinazione di tasti Ctrl + C (SIGINT) o Ctrl + Z (SIGSTOP) durante l’esecuzione di un processo, mentre un interrupt inviato da un altro processo può essere la funzione kill().

3 - Ciclo di vita dei segnali in UNIX

In UNIX, i segnali hanno il seguente ciclo di vita:

graph LR
    A[Generazione] --> B[Blocco] --> C[Consegna] --> D[Gestione] --> E[Conclusione]
    B --> B

  1. Generazione del segnale: il segnale viene creato come risultato di un evento specifico e viene posto in una associata al processo destinatario.
  2. Blocco del segnale: dopo la sua generazione, si può impedire temporaneamente che un segnale venga consegnato a un processo. Invece di essere immediatamente gestito, il segnale rimane in uno stato di pendenza finché non viene sbloccato.
  3. Consegna del segnale: quando il processo destinatario è attivo, il kernel controlla se ci sono segnali pendenti per quel processo. Se il segnale non è bloccato, allora viene recapitato, altrimenti rimane in stato pendente fino a quando non può essere gestito. Alcuni segnali, come SIGKILL, vengono consegnati immediatamente e non possono essere bloccati o ignorati.
  4. Gestione del segnale: una volta consegnato, il segnale viene gestito dal processo a cui è stato recapitato.
  5. Conclusione del segnale: una volta gestito, il segnale esce dalla coda dei segnali pendenti. Se il segnale ha attivato un’azione (ad esempio, la terminazione del processo), il ciclo di vita del segnale termina con l’esecuzione dell’azione predefinita o personalizzata.

4 - Generazione di un segnale

Durante la generazione del segnale, il segnale viene creato come risultato di un evento specifico e viene posto in una coda dei segnali pendenti associata al processo destinatario.

4.1 - Fonti di generazione di un segnale

La generazione di un segnale può essere causato da diversi eventi o sorgenti:

  • Evento hardware: l’hardware ha verificato una condizione di errore che è stata notificata al kernel, il quale a propria volta ha inviato un segnale corrispondente al processo in questione. Per esempio, l’esecuzione di istruzioni di linguaggio macchina malformate (SIGILL), divisioni per (SIGFPE), o riferimenti a parti di memoria inaccessibili (SIGSEGV).
  • Evento software: eventi che non derivano direttamente dall’hardware ma sono causati da azioni compiute da processi, dal kernel o da altre operazioni software. Per esempio, l’input è divenuto disponibile su un descrittore di file (SIGIO), un timer è arrivato a (SIGALRM), il tempo di processore per il processo è stato superato (SIGXCPU) o un figlio del processo è terminato (SIGCHLD).
  • Azione dell’utente: l’utente ha digitato sul terminale combinazioni di tasti che generano i segnali, per esempio Ctrl + C (SIGINT) o Ctrl + Z (SIGTSTP).

4.2 - Generazione di un segnale tramite eventi software

La generazione di un segnale tramite eventi software avviene tramite azioni compiute da processi, dal kernel o da altre operazioni software. In particolare, si può ottenere usando le seguenti chiamate di sistema:

  • kill(): invia un segnale a un processo o a un gruppo di processi.
  • raise(): invia un segnale allo stesso processo chiamante.
  • alarm(): imposta un timer di sistema al termine del quale invia il segnale SIGALRM.

4.2.1 - Invio di un segnale tramite kill()

La funzione kill() è una chiamata di sistema utilizzata per inviare segnali a un processo o gruppo di processi.

Il suo prototipo è il seguente:

#include <signal.h>
 
int kill(pid_t pid, int sig);

dove:

  • pid: processo o gruppo di processi a cui inviare il segnale, in particolare:
    • pid > 0: invia il segnale al processo specifico con PID uguale a pid.
    • pid == 0: invia il segnale a tutti i processi nel gruppo di processi del chiamante.
    • pid == -1 (detto broadcast signal): invia il segnale a tutti i processi che l’utente può controllare (tranne i processi di sistema e il processo chiamante stesso).
    • pid < -1: invia il segnale a tutti i processi nel gruppo di processi specificato dal valore assoluto pid (cioè -pid).
  • sig: segnale da inviare.
  • valore di ritorno int: può assumere i seguenti valori:
    • 0: la funzione ha eseguito correttamente l’operazione.
    • -1: c’è stato un errore durante l’esecuzione della funzione ed errno viene impostato.

Attenzione!

La funzione kill() non sempre termina un processo, ma semplicemente invia un segnale che attiverà l’azione predefinita di terminazione o, se possibile e se specificato, il relativo signal handler che compirà un’azione diversa.

4.3.1 - Invio di un segnale allo stesso processo chiamante tramite raise()

La funzione raise() è una chiamata di sistema utilizzata per inviare un segnale allo stesso processo chiamante. È una forma semplificata per generare segnali all’interno di un programma, senza dover usare kill() o identificare esplicitamente il processo.

Il suo prototipo è il seguente:

#include <signal.h>
 
int raise(int sig);

dove:

  • sig: segnale da inviare.
  • valore di ritorno int:
    • 0: la funzione ha eseguito correttamente l’operazione.
    • -1: c’è stato un errore durante l’esecuzione della funzione ed errno viene impostato.

4.3.2 - Impostazione di un timer di sistema tramite alarm()

La funzione alarm() è una chiamata di sistema usata per impostare un timer di sistema al termine del quale viene inviato il segnale SIGALRM. È utile per gestire eventi basati sul tempo, come timeout per operazioni o esecuzioni periodiche.

Il suo prototipo è il seguente:

#include <unistd.h>
 
unsigned int alarm(unsigned int seconds);

dove:

  • seconds: numero di secondi dopo il quale il segnale SIGALRM sarà inviato. Se seconds viene impostato a 0, l’allarme già esistente (se presente) viene annullato.
  • unsigned int restituito: numero di secondi rimanenti del precedente timer impostato, se esisteva. Altrimenti, restituisce 0 se non era impostato alcun timer.

4.4 - Segnali SIGUSR1 e SIGUSR2

I segnali SIGUSR1 e SIGUSR2 sono segnali definiti dall’utente. Sono segnali generici che non hanno un’azione predefinita specifica (a parte la terminazione del processo, se non gestiti) e vengono solitamente utilizzati per notifiche o comunicazioni personalizzate tra processi.

I numeri assegnati a SIGUSR1 e SIGUSR2 possono variare tra i sistemi, ma tipicamente sono rispettivamente 10 e 12.

4.4.1 - Uso tipico

  • Notifiche personalizzate: segnalare eventi specifici tra processi.
  • Comunicazione tra processi (IPC): permettere ai processi di scambiarsi informazioni senza passare per meccanismi complessi come pipe o socket.
  • Debugging: utilizzati per notificare lo stato interno di un’applicazione durante il debug.

5 - Blocco del segnale

Il blocco di un segnale è una tecnica usata per impedire temporaneamente che un segnale venga consegnato a un processo, memorizzandolo all’interno della coda dei segnali pendenti.

5.1 - Uso tipico

  • Protezione delle sezioni critiche: durante l’esecuzione di una sezione di codice critico, un segnale può causare interruzioni indesiderate che portano a comportamenti errati, come la generazione di dati incoerenti, la corruzione di strutture dati condivise od operazioni incomplete. Bloccando i segnali, si garantisce che il codice critico venga eseguito senza interferenze.
  • Coordinamento in applicazioni multithreading: in programmi multithreading, è importante evitare che più thread gestiscano lo stesso segnale contemporaneamente. Bloccando i segnali, si può sincronizzare la gestione a livello di thread o di processo.
  • Posticipare la gestione di segnali non prioritari: un segnale potrebbe essere generato in un momento inopportuno, ad esempio mentre il processo sta elaborando un’altra richiesta importante. Bloccarlo consente di gestirlo successivamente, quando il processo è pronto.
  • Debug e testing: durante il debugging o il testing di applicazioni, è utile bloccare alcuni segnali per evitare che interferiscano con l’esecuzione o per analizzare il loro comportamento una volta sbloccati.

5.2 - Maschera dei segnali

La maschera dei segnali è un meccanismo che consente di specificare quali segnali un processo desidera bloccare temporaneamente. È una struttura cruciale per la gestione dei segnali, utilizzata per impedire che determinati segnali vengano consegnati al processo finché la maschera non viene modificata.

Viene rappresentata da un oggetto di tipo sigset_t, che è una struttura dati per definire un set di segnali. Quando un segnale è “mascherato” (cioè bloccato), non viene consegnato immediatamente al processo, ma viene messo nella coda dei segnali pendenti.

Le funzioni tipicamente usate per gestire una maschera dei segnali sono:

  • sigemptyset(): inizializza una nuova maschera vuota.
  • sigfillset(): inizializza una maschera contenente tutti i segnali dichiarati dal sistema.
  • sigismember(): verifica se un determinato segnale è presente nella maschera.
  • sigaddset(): aggiunge un determinato segnale alla maschera.
  • sigdelset(): rimuove un determinato segnale dalla maschera.
  • sigprocmask(): associa una maschera a un determinato processo.

5.2.1 - Inizializzazione di una maschera vuota tramite sigemptyset()

La funzione sigemptyset() permette di inizializzare una maschera vuota, rappresentata da un set di segnali. È spesso usata come primo passo per configurare una maschera di segnali o per costruire un set di segnali specifici.

Il suo prototipo è il seguente:

#include <signal.h>
 
int sigemptyset(sigset_t *set);

dove:

  • set: puntatore a una variabile di tipo sigset_t (struttura dati che rappresenta un insieme di segnali). Questa variabile viene inizializzata come vuota dalla funzione.
  • valore di ritorno int:
    • 0: la funzione ha eseguito correttamente l’operazione.
    • -1: c’è stato un errore durante l’esecuzione della funzione ed errno viene impostato (anche se nella pratica questa funzione non fallisce mai).

Esempio

#include <stdio.h>
#include <signal.h>
 
int main() {
    sigset_t set;
 
    // Inizializzazione del set vuoto
    if (sigemptyset(&set) == -1) {
        perror("Errore nell'inizializzazione del set");
        return 1;
    }
 
	// Uso della maschera
 
    return 0;
}

5.2.2 - Inizializzazione di una maschera piena tramite sigfillset()

La funzione sigfillset() permette di inizializzare una maschera contenente tutti i segnali definiti dal sistema. È l’opposto di sigemptyset(), che invece crea un set vuoto.

Il suo prototipo è il seguente:

#include <signal.h>
 
int sigfillset(sigset_t *set);

dove:

  • set: puntatore a una variabile di tipo sigset_t (struttura dati che rappresenta un insieme di segnali). La funzione riempie questa struttura con tutti i segnali supportati dal sistema.
  • valore di ritorno int:
    • 0: la funzione ha eseguito correttamente l’operazione.
    • -1: c’è stato un errore durante l’esecuzione della funzione ed errno viene impostato (anche se nella pratica questa funzione non fallisce mai).

Esempio

#include <stdio.h>
#include <signal.h>
 
int main() {
    sigset_t set;
 
    // Inizializzazione del set con tutti i segnali
    if (sigfillset(&set) == -1) {
        perror("Errore nell'inizializzazione del set");
        return 1;
    }
 
	// Uso della maschera
 
    return 0;
}

5.2.3 - Verifica di un segnale nella maschera tramite sigismember()

La funzione sigismember() verifica se un segnale specifico è presente in un set di segnali (di tipo sigset_t). È comunemente utilizzata per controllare la composizione di un set prima di applicare una maschera di segnali o eseguire altre operazioni.

Il suo prototipo è il seguente:

#include <signal.h>
 
int sigismember(const sigset_t *set, int signum);

dove:

  • set: puntatore a un oggetto di tipo sigset_t che rappresenta un set di segnali.
  • signum: intero che rappresenta il numero del segnale da aggiungere al set (es. SIGINT, SIGTERM).
  • valore di ritorno int:
    • 1: il segnale è presente nel set.
    • 0: il segnale non è presente nel set.
    • -1: c’è stato un errore durante l’esecuzione della funzione ed errno viene impostato con codice di errore EINVAL (che indica un argomento non valido).

Esempio

#include <stdio.h>
#include <signal.h>
 
int main() {
    sigset_t set;
 
    // Inizializzazione di un set vuoto
    if (sigemptyset(&set) == -1) {
        perror("Errore nell'inizializzazione del set");
        return 1;
    }
 
    // Aggiunta di SIGINT al set
    if (sigaddset(&set, SIGINT) == -1) {
        perror("Errore nell'aggiunta di SIGINT");
        return 1;
    }
 
    // Verifica di SIGINT nel set
    if (sigismember(&set, SIGINT)) {
        printf("SIGINT è presente nel set.\n");
    } else {
        printf("SIGINT non è presente nel set.\n");
    }
 
    // Verifica di SIGTERM nel set
    if (sigismember(&set, SIGTERM)) {
        printf("SIGTERM è presente nel set.\n");
    } else {
        printf("SIGTERM non è presente nel set.\n");
    }
 
    return 0;
}

5.2.4 - Aggiunta di un segnale tramite sigaddset()

La funzione sigaddset() permette di aggiungere un segnale specifico a un set di segnali (di tipo sigset_t). Il suo prototipo è il seguente:

#include <signal.h>
 
int sigaddset(sigset_t *set, int signum);

dove:

  • set: puntatore a un oggetto di tipo sigset_t che rappresenta un set di segnali.
  • signum: intero che rappresenta il numero del segnale da aggiungere al set (es. SIGINT, SIGTERM).
  • valore di ritorno int:
    • 0: la funzione ha eseguito correttamente l’operazione.
    • -1: c’è stato un errore durante l’esecuzione della funzione ed errno viene impostato con codice di errore EINVAL (che indica un argomento non valido).

Esempio

#include <stdio.h>
#include <signal.h>
 
int main() {
    sigset_t set;
 
    // Inizializzazione del set vuoto
    if (sigemptyset(&set) == -1) {
        perror("Errore nell'inizializzazione del set");
        return 1;
    }
 
    // Aggiunta di SIGINT al set
    if (sigaddset(&set, SIGINT) == -1) {
        perror("Errore nell'aggiunta di SIGINT");
        return 1;
    }
 
    // Verifica di SIGINT nel set
    if (sigismember(&set, SIGINT)) {
        printf("SIGINT è stato aggiunto al set.\n");
    } else {
        printf("SIGINT non è nel set.\n");
    }
 
    return 0;
}

5.2.5 - Rimozione di un segnale tramite sigdelset()

La funzione sigdelset() permette di rimuovere un segnale specifico da un set di segnali (di tipo sigset_t). Il suo prototipo è il seguente:

#include <signal.h>
 
int sigdelset(sigset_t *set, int signum);

dove:

  • set: puntatore a un oggetto di tipo sigset_t che rappresenta un set di segnali.
  • signum: intero che rappresenta il numero del segnale da rimuovere dal set.
  • valore di ritorno int:
    • 0: la funzione ha eseguito correttamente l’operazione.
    • -1: c’è stato un errore durante l’esecuzione della funzione ed errno viene impostato con codice di errore EINVAL (che indica un argomento non valido).

Esempio

#include <stdio.h>
#include <signal.h>
 
int main() {
    sigset_t set;
 
    // Inizializzazione del set con tutti i segnali
    if (sigfillset(&set) == -1) {
        perror("Errore nell'inizializzazione del set");
        return 1;
    }
 
    // Rimozione di SIGINT dal set
    if (sigdelset(&set, SIGINT) == -1) {
        perror("Errore nella rimozione di SIGINT");
        return 1;
    }
 
    // Verifica di SIGINT nel set
    if (sigismember(&set, SIGINT)) {
        printf("SIGINT è ancora nel set.\n");
    } else {
        printf("SIGINT è stato rimosso dal set.\n");
    }
 
    return 0;
}

5.2.6 - Applicazione di una maschera al processo tramite sigprocmask()

La funzione sigprocmask() viene utilizzata per modificare la maschera associata a un processo.

Il suo prototipo è il seguente:

#include <signal.h>
 
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

dove:

  • how: intero che specifica come modificare la maschera esistente (quella correntemente applicata al processo) e può assumere i seguenti valori:
    • SIG_BLOCK: aggiunge i segnali specificati alla maschera già esistente.
    • SIG_UNBLOCK: rimuove i segnali specificati dalla maschera esistente.
    • SIG_SETMASK: sostituisce la maschera esistente con quella specificata.
  • set: puntatore alla nuova maschera da applicare.
  • oldset (opzionale): puntatore alla maschera precedente (utile per poterla salvare prima di sostituirla con la nuova).
  • valore di ritorno int:
    • 0: la funzione ha eseguito correttamente l’operazione.
    • -1: c’è stato un errore durante l’esecuzione della funzione ed errno viene impostato.

Esempio

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
 
int main() {
    sigset_t set, oldset;
 
    // Inizializzazione di una maschera vuota
    sigemptyset(&set);
    sigaddset(&set, SIGINT); // Aggiunta di SIGINT alla maschera
 
    // Blocco di SIGINT
    if (sigprocmask(SIG_BLOCK, &set, &oldset) == -1) {
        perror("Errore nel blocco di SIGINT");
        return 1;
    }
 
    printf("SIGINT bloccato. Genera segnali con Ctrl + C.\n");
    sleep(10);
 
    // Ripristino della maschera originale
    if (sigprocmask(SIG_SETMASK, &oldset, NULL) == -1) {
        perror("Errore nel ripristino della maschera");
        return 1;
    }
 
    printf("SIGINT sbloccato.\n");
    sleep(10);
 
    return 0;
}

5.3 - Segnali non bloccabili (SIGKILL e SIGSTOP)

Nei sistemi UNIX/Linux, esistono segnali non bloccabili, ignorabili o gestibili da un processo tramite un signal handler. Questi segnali hanno comportamenti predefiniti che il sistema operativo applica sempre, indipendentemente dalle richieste del processo. Gli unici due segnali non bloccabili sono:

  • SIGKILL: termina immediatamente un processo, viene usato dal sistema operativo o dagli utenti per forzare la terminazione di un processo che non risponde a segnali ordinari.
  • SIGSTOP: sospende immediatamente l’esecuzione di un process, viene utilizzato per mettere in pausa un processo (ad esempio con Ctrl + Z o comandi come kill -STOP).

Rendere questi segnali non bloccabili assicura che il sistema operativo mantenga un controllo minimo indispensabile sui processi, evitando situazioni in cui un processo malintenzionato o malfunzionante possa eludere la gestione dei segnali.

6 - Coda dei segnali pendenti

La coda dei segnali pendenti è una struttura utilizzata dal kernel per mantenere traccia dei segnali che sono stati generati ma non ancora consegnati a un processo. Questo avviene quando:

  • Un segnale è bloccato, cioè il processo ha indicato di non volerlo gestire immediatamente).
  • Il processo è temporaneamente incapace di riceverlo (ad esempio, è in uno stato di pausa o attesa).

6.1 - Caratteristiche principali

  • Un segnale per tipo: la coda non accumula più istanze dello stesso segnale. Se un segnale è già pendente, ulteriori generazioni dello stesso segnale vengono ignorate.
  • Gestione dei segnali bloccati: i segnali bloccati vengono aggiunti alla coda invece di essere immediatamente consegnati. Quando un segnale viene sbloccato (es. tramite sigprocmask()), esso viene prelevato dalla coda e consegnato.
  • Priorità dei segnali: la priorità dei segnali non viene determinata in base al tempo di generazione, ma dipende dal tipo del segnale, assegnando ad alcuni segnali una priorità più alta se necessario.
  • Segnali non bloccabili: alcuni segnali non possono essere messi nella coda dei segnali pendenti, come SIGKILL e SIGSTOP, che vengono gestiti immediatamente dal kernel.
  • Compatibilità con i thread: nei programmi multithreading, ogni thread ha la propria coda dei segnali pendenti, ma alcuni segnali (es. segnali diretti al processo) possono essere condivisi e devono essere gestiti con coordinamento.
  • Non persistenza: i segnali pendenti non vengono conservati se il processo a cui si riferiscono termina prima di riceverli.
  • Sicurezza nelle sezioni critiche: permette di proteggere sezioni critiche di codice bloccando temporaneamente i segnali, senza rischiare di perdere notifiche importanti.

6.2 - Verifica dei segnali pendenti tramite sigpending()

La funzione sigpending() permette di controllare quali segnali sono attualmente pendenti per il processo.

Il suo prototipo è il seguente:

#include <signal.h>
 
int sigpending(sigset_t *set);

dove:

  • set: puntatore a un oggetto sigset_t che verrà popolato con l’elenco dei segnali pendenti.
  • valore di ritorno int:
    • 0: la funzione ha eseguito correttamente l’operazione.
    • -1: c’è stato un errore durante l’esecuzione della funzione ed errno viene impostato con codice di errore EINVAL (che indica un argomento non valido).

Esempio

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
 
int main() {
    sigset_t set;
 
    // Controlla i segnali pendenti
    if (sigpending(&set) == -1) {
        perror("Errore nel recupero dei segnali pendenti");
        exit(EXIT_FAILURE);
    }
 
    // Verifica se SIGINT è pendente
    if (sigismember(&set, SIGINT)) {
        printf("SIGINT è pendente.\n");
    } else {
        printf("SIGINT non è pendente.\n");
    }
 
    return 0;
}

7 - Gestione del segnale

Al momento della ricezione di un segnale, il processo può decidere come gestirlo in uno dei seguenti modi:

  • Eseguire l’azione predefinita: se il segnale non è stato configurato diversamente, il processo esegue l’azione predefinita associata al segnale.
  • Assegnare un signal handler: il processo può assegnare una funzione (detta signal handler) per gestire un segnale specifico tramite le funzioni signal() e sigaction() anziché eseguire l’azione predefinita.

Nota

I segnali non bloccabili (SIGKILL e SIGSTOP) possono solamente eseguire l’azione predefinita.

7.1 - Esecuzione dell’azione predefinita

Alla ricezione del segnale, se non è stato configurato diversamente, il processo esegue l’azione predefinita associata al segnale. Ogni segnale ha un proprio comportamento preimpostato, che varia in base alla sua natura e scopo. Le principali azioni predefinite sono:

  • Terminazione del processo: il processo termina immediatamente e il sistema libera le risorse allocate. Esempi di segnali con questa azione includono SIGTERM (terminazione ordinata) e SIGINT (interruzione da tastiera, azionabile con la combinazione di tasti Ctrl + C).
  • Terminazione con generazione di un core dump: il processo termina e il sistema crea un file di core dump, utile per analizzare lo stato del programma al momento del crash. Esempi di segnali con questa azione sono SIGSEGV (violazione di segmentazione) e SIGABRT (interruzione forzata).
  • Ignorazione del segnale: il processo non intraprende alcuna azione e continua la sua esecuzione. Esempi di segnali ignorati per impostazione predefinita includono SIGCHLD, che notifica la terminazione di un processo figlio.
  • Sospensione del processo: il processo viene messo in stato di stop e rimane sospeso fino a quando non riceve un segnale per riprendere. Un esempio di segnali con questa azione è SIGSTOP (sospensione forzata, azionabile con la combinazione di tasti Ctrl + Z), che sospende il processo senza possibilità di blocco o gestione finché non riceve il segnale di ripresa SIGCONT.
  • Ripresa del processo: il processo sospeso riprende la sua esecuzione. Un esempio è SIGCONT, utilizzato per continuare un processo precedentemente sospeso con SIGSTOP.

7.2 - Il signal handler

Il signal handler è una funzione specificata dall’utente che dice a un programma come gestire la ricezione di un segnale e come agire di conseguenza. Si può impostare tramite le chiamate di sistema signal() e sigaction().

7.2.1 - Assegnazione di un signal handler tramite signal()

La funzione signal() è una chiamata di sistema e rappresenta la maniera più semplice per associare un signal handler a un segnale.

Il suo prototipo è il seguente:

#include <signal.h>
 
void (*signal(int sig, void (*handler)(int)))(int);

dove:

  • sig: segnale che si vuole gestire.
  • handler: puntatore alla funzione che gestirà il segnale, la quale prende un parametro di tipo int (tipicamente il numero del segnale).

Esempio

#include <stdio.h>
#include <signal.h>
 
void handler(int sig) {
    printf("Ricevuto segnale %d\n", sig);
}
 
int main() {
    signal(SIGINT, handler); // Associa il gestore per SIGINT
    return 0;
}

Attenzione!

Vi sono differenze nel comportamento della funzione signal() fra le varie implementazioni di UNIX; se possibile, quindi, va evitato il suo uso in programmi complessi.

7.2.2 - Assegnazione di un signal handler tramite sigaction()

La funzione sigaction() è una chiamata di sistema e rappresenta una versione più potente e sicura per gestire i segnali. Permette un controllo più fine, come il blocco temporaneo dei segnali durante l’esecuzione del gestore.

Il suo prototipo è il seguente:

#include <signal.h>
 
int sigaction(int sig, const struct sigaction *act, struct sigaction *oldact);

dove:

  • sig: segnale che si vuole gestire.
  • act: puntatore a una struttura sigaction che descrive come deve essere gestito il segnale.
  • oldact: puntatore a una struttura sigaction in cui verrà memorizzato il gestore precedente (può essere NULL).
  • valore di ritorno int:
    • 0: la funzione ha eseguito correttamente l’operazione.
    • -1: c’è stato un errore durante l’esecuzione della funzione ed errno viene impostato con codice di errore EINVAL (che indica un argomento non valido).

7.2.2.1 - Struttura sigaction

La struttura sigaction, usata nella funzione sigaction(), è così definita:

struct sigaction {
    void (*sa_handler)(int);
    sigset_t sa_mask;
    int sa_flags;
    void (*sa_restorer)(void);
};

dove:

  • sa_handler: funzione che definisce il comportamento da adottare all’attivazione del segnale (signal handler).
  • sa_mask: maschera dei segnali temporanea applicata durante l’esecuzione dell’handler che sostituisce quella legata al processo in esecuzione. Insieme ai segnali specificati in questa maschera, viene automaticamente bloccato anche il segnale che ha attivato l’handler.
  • sa_flags: flag per personalizzare il comportamento dell’handler.
  • sa_restorer (obsoleto): puntatore a una funzione che il kernel potrebbe utilizzare per ripristinare il contesto del programma dopo l’esecuzione di un signal handler. Era usato in implementazioni storiche per specificare una routine di “pulizia” che riportava il processo al suo stato normale dopo che il signal handler terminava.

Esempio

#include <stdio.h>
#include <signal.h>
 
void handler(int sig) {
    printf("Ricevuto segnale %d\n", sig);
}
 
int main() {
    struct sigaction sa;
    sa.sa_handler = handler;  // Imposta il gestore
    sigemptyset(&sa.sa_mask);  // Nessun segnale è bloccato durante l'esecuzione
    sa.sa_flags = 0;  // Nessun flag particolare
    sigaction(SIGINT, &sa, NULL);  // Associa il gestore a SIGINT
    return 0;
}

7.2.3 - Meglio usare signal() o sigaction()?

Solitamente, sigaction() è preferita rispetto a signal() per i seguenti motivi:

  • Flessibilità: sigaction() permette di bloccare temporaneamente altri segnali durante l’esecuzione del gestore e offre maggiore controllo sui segnali.
  • Compatibilità: signal() può avere un comportamento imprevisto su alcune piattaforme, mentre sigaction() è più robusta e portabile.
  • Controllo avanzato: con sigaction() è possibile specificare una maschera di segnali da bloccare durante l’esecuzione del gestore e altre opzioni, mentre signal() è più semplice ma meno potente.

7.2.4 - Interruzione del signal handler

Il signal handler, durante la sua esecuzione, può comunque essere interrotto da altri segnali (o dallo stesso segnale, nel caso la flag SA_NODEFER sia settata nella struttura sigaction in modo da evitare il blocco automatico del segnale che attiva l’handler).

Quando l’handler termina, la maschera dei segnali viene reimpostata al valore precedente la sua esecuzione, indipendentemente dalle possibili manipolazioni dei segnali bloccati eventualmente presenti nell’handler.

7.2.5 - Ereditarietà del signal handler e funzioni AS-Safe

In seguito a un’operazione di fork(), il signal handler viene ereditato dal processo figlio e le variabili globali definite nel programma sono visibili sia dalle funzioni handler che dal resto del programma. Questo può essere utile per modificare il valore di una variabile globale simulando un cambio di stato del programma che verrà poi utilizzato durante l’esecuzione “normale” del codice.

Ma cosa succede quando un handler modifica il valore di una variabile globale il cui valore non dovrebbe essere modificato? Alcune chiamate di sistema utilizzano strutture dati globali e quindi il loro utilizzo all’interno di un handler può generare problemi. Per esempio, cosa succede se un segnale interrompe l’esecuzione di una printf(), che poi viene anche utilizzata all’interno dell’handler?

8 - Lista dei segnali più comuni

SegnaleNumeroQuando viene generatoAzione predefinita
SIGHUP
(hang up)
1Disconnessione del terminale o chiusura della shell.Termina o ricarica il processo, spesso usato per configurazioni.
SIGINT
(interrupt)
2Interruzione da tastiera (Ctrl + C).Termina il processo.
SIGQUIT
(quit)
3Interruzione da tastiera con dump (Ctrl+\).Termina il processo e genera un core dump.
SIGILL
(illegal instruction)
4Istruzione illegale eseguita dal processo.Termina il processo segnalando un errore critico.
SIGTRAP
(trap)
5Breakpoint impostato da un debugger (es. GDB) o sollevata un’eccezione.Termina il processo con generazione di un core dump.
SIGABRT
(abort)
6Invocazione della funzione abort().Termina il processo e genera un core dump.
SIGBUS
(bus error)
7Accesso a memoria non allineato o errore su memoria mappata.Termina il processo con generazione di un core dump.
SIGFPE
(floating point exception)
8Errore aritmetico (es. divisione per zero).Termina il processo e segnala un errore matematico.
SIGKILL
(kill)
9Inviato dal kernel o da un comando kill -9.Termina immediatamente il processo (non bloccabile).
SIGUSR1
(user defined 1)
10Generato da altri processi per scopi personalizzati.Usato per notifiche o segnali definiti dall’utente.
SIGSEGV
(segmentation violation)
11Accesso a memoria non valida (es. dereferenziamento nullo).Termina il processo e genera un core dump.
SIGUSR2
(user defined 2)
12Generato da altri processi per scopi personalizzati.Usato per notifiche o segnali definiti dall’utente.
SIGPIPE
(pipe)
13Scrittura su una pipe chiusa o lettura non valida.Termina il processo o gestisce l’errore.
SIGALRM
(alarm)
14Timer scaduto generato da una funzione alarm() o settimer().Utilizzato per gestire timeout nei processi.
SIGTERM
(terminate)
15Richiesta di terminazione ordinata tramite comando kill.Termina il processo in modo ordinato.
SIGSTKFLT
(stack fault)
16Errore relativo alla gestione dello stack (segnale obsoleto).Termina il processo.
SIGCHLD
(child)
17Processo figlio terminato o sospeso.Notifica al processo padre della terminazione di un figlio.
SIGCONT
(continue)
18Ripresa di un processo sospeso.Riprende l’esecuzione di un processo sospeso precedentemente con SIGSTOP.
SIGSTOP
(stop)
19Sospensione forzata di un processo.Sospende il processo (non bloccabile).
SIGTSTP
(terminal stop)
20Sospensione da tastiera (Ctrl + Z).Sospende il processo, modificabile dal processo.
SIGTTIN
(terminal input)
21Processo in background tenta di leggere dall’input.Sospende il processo fino a che non è in primo piano.
SIGTTOU
(terminal output)
22Processo in background tenta di scrivere sull’output.Sospende il processo fino a che non è in primo piano.
SIGPOLL
(pollable event)
o SIGIO
(input/output)
29Evento su un descrittore di file monitorato.Notifica un input/output disponibile (POSIX).

Fonti

  • 🏫 Lezioni e slide del Prof. Schifanella Claudio del corso di Laboratorio di Sistemi Operativi (canale B, turno T4), Corso di Laurea in Informatica presso l’Università di Torino, A.A. 2024-25: