Il Concetto di Segnale in Linux

Iniziamo, con questa lezione, la trattazione di uno degli argomenti più importanti e complessi della programmazione di sistema in Linux: i segnali.

I segnali sono uno dei meccanismi principali attraverso i quali il kernel di Linux comunica con i processi. Essi sono utilizzati per segnalare eventi hardware, software o generati dall'utente ad un processo. Inoltre, i segnali possono essere inviati da un processo ad un altro processo, permettendo così una forma primitiva di comunicazione inter-processo.

Questa lezione introduce il concetto di segnale e descrive, in generale, come i segnali vengono consegnati ad un processo e come un processo può decidere di gestire un segnale. Vedremo anche come è possibile controllare lo stato dei segnali di un processo in Linux.

Il Concetto di Segnale

Un segnale è una notifica asincrona di un evento verso un processo del sistema operativo. I segnali vengono anche chiamati interrupt software in quanto svolgono, più o meno, lo stesso ruolo degli interrupt hardware. Del resto, essi rappresentano il meccanismo principale attraverso il quale il kernel di Linux, e in generale i kernel Unix like, segnalano eventi ai processi.

Sono simili agli interrupt, in quanto, essi interrompono il normale flusso di esecuzione di un processo e, tipicamente, non c'è un modo di prevedere quando un segnale verrà inviato. Per tal motivo si parla di Notifica Asincrona.

Un segnale può anche essere inviato da un processo ad un altro, ammesso che il processo che invia il segnale abbia i permessi necessari per farlo. Possono essere utilizzati come meccanismo di sincronizzazione tra processi oppure come una forma semplice e primitiva di meccanismo di comunicazione inter-processo (IPC). Addirittura, un processo può inviare un segnale a se stesso: in tal caso si parla di raising o sollevamento del segnale.

In generale, tuttavia, i segnali nascono per permettere al kernel di poter segnalare eventi ai processi. Gli eventi che possono scatenare l'invio di un segnale possono essere raggruppati in tre categorie:

  • Eventi hardware: il kernel ha riscontrato una condizione hardware che necessita di attenzione da parte del processo. Tale condizione potrebbe essere un'eccezione o addirittura un guasto. Esempi di segnali hardware potrebbero essere una divisione per zero (quindi un'eccezione aritmetica), un'accesso a memoria non valido, un'accesso al bus di I/O non valido, ecc.
  • Eventi software. In questa categoria rientrano tutti quegli eventi asincroni non legati strettamente all'hardware. Esempi sono lo scattare di un timer, la ricezione di un segnale da parte di un altro processo, la terminazione di un processo figlio, ecc.
  • Eventi generati dall'utente. Con questa categoria si intende la possibilità di inviare segnali da parte dell'utente. Questo tipo di segnali sono utili per interagire con un processo in esecuzione, per esempio per terminarlo (premendo CTRL+C) o per metterlo in pausa (premendo CTRL+Z).

Ciascun segnale è identificato da un numero intero, tipicamente compreso tra 1 e 31. Questi valori sono definiti nel file di intestazione <signal.h> e sono associati a dei nomi simbolici che hanno la forma SIGxxxx. Ad esempio, il segnale che viene inviato quando si preme CTRL+C è SIGINT (che sta per SIGnal INTerrupt). Il valore numerico, in teoria, può cambiare tra le implementazioni di UNIX, anche se su Linux e su molti UNIX like i valori sono standardizzati. In ogni caso conviene sempre usare questi nomi simbolici.

Anche dal punto di vista numerico, i segnali possono essere raggruppati in due grandi categorie:

  • I segnali da 1 a 31 chiamati anche Segnali Standard. Questi segnali sono utilizzati dal kernel o dall'utente per segnalare eventi ad un processo.
  • I segnali da 32 in poi chiamati anche Segnali Real Time. Questi segnali sono utilizzati per implementare meccanismi di comunicazione inter-processo in tempo reale e seguono regole leggermente diverse rispetto ai segnali standard. Vedremo i segnali Real time nelle prossime lezioni.

Ricapitolando:

Definizione

Segnale in Linux

Un Segnale è una notifica asincrona verso un processo di un evento che è accaduto. Possono essere inviati dal kernel o da un altro processo. I segnali vengono utilizzati per segnalare eventi hardware, software o generati dall'utente.

Ogni segnale è identificato da una costante numerica, tipicamente compresa tra 1 e 31, definita nel file di intestazione <signal.h>. Ogni costante è identificata da un nome simbolico che inizia per SIG. I segnali possono essere divisi in due categorie: i segnali standard (da 1 a 31) e i segnali real time (da 32 in poi).

Abbiamo detto che un segnale è una notifica asincrona di un evento. In generale, per comprenderne il funzionamento, bisogna suddividere il loro funzionamento in due passaggi:

  • La Consegna: ossia il meccanismo che, a partire da un evento, genera un segnale e lo consegna al processo destinatario.
  • La Gestione: ossia il meccanismo che permette al processo destinatario di gestire il segnale una volta che quest'ultimo è stato consegnato.

Consegna dei Segnali

Il meccanismo di consegna dei segnali riguarda tutto ciò che accade dal momento in cui si verifica un evento che richiede l'invio di un segnale fino al momento in cui il segnale viene consegnato al processo destinatario. Questo meccanismo è gestito dal kernel e, in generale, non è possibile influenzarlo direttamente da parte del processo.

Per prima cosa, si dice che un segnale è generato da un evento. Quando un segnale viene generato, il kernel provvede a consegnarlo al processo destinatario. Quest'ultimo prende in carico il segnale e decide come gestirlo. Non necessariamente il processo destinatario è in grado di gestire un segnale appena consegnato. In tal caso si dice che il segnale è in attesa o pending.

Normalmente, un segnale pending è consegnato ad un processo non appena quest'ultimo viene schedulato per l'esecuzione dallo scheduler del kernel, oppure è consegnato immediatamente se il processo è già in esecuzione.

Il meccanismo di consegna è riassunto nella figura che segue:

Meccanismo di consegna dei segnali in Linux
Figura 1: Meccanismo di consegna dei segnali in Linux

Nella figura, lato kernel, un evento genera un segnale che viene consegnato al processo. La linea tratteggiata indica che il processo, quando riceve il segnale, è in stato di wait, ossia non è in esecuzione. Pertanto il segnale rimane pending o in attesa. Ad un certo punto, il kernel decide, attraverso il suo scheduler, di far riprendere l'esecuzione al processo. Quando ciò accade, il processo prende in carico il segnale.

In altri casi, potrebbe sorgere l'esigenza di non interrompere l'esecuzione del flusso normale del processo, ma di posticipare la consegna del segnale. Questo può essere necessario quando vi sono porzioni di codice che il processo non vuole interrompere.

In tal caso, il segnale viene messo in attesa e consegnato al processo solo quando quest'ultimo si trova in una condizione di ricevere il segnale. Questo meccanismo è chiamato masking o mascheramento. Per far questo, il segnale viene aggiunto ad una maschera di segnali, ossia ad un insieme di segnali che il processo ha deciso di bloccare.

Un segnale bloccato rimane in attesa fin quando il processo non decide di sbloccarlo. A quel punto il segnale viene consegnato al processo e il processo stesso può decidere come gestirlo. Bisogna tenere presente, però, che non tutti i segnali possono essere mascherati o bloccati.

Gestione dei Segnali

Una volta che il segnale è stato ricevuto dal processo destinatario, quest'ultimo deve decidere come gestirlo. Questo meccanismo è chiamato gestione dei segnali.

Di default, quando un processo riceve un segnale, possono accadere le seguenti cose:

  • Il segnale viene ignorato e non viene fatta nessuna azione. Il kernel scarta automaticamente il segnale e il processo non ha informazione alcuna che il segnale è stato inviato.
  • Il processo viene terminato. Il processo viene terminato, in gergo tecnico ucciso o killed, dal kernel. Si parla, in questo caso, di terminazione anormale di un processo, per distinguerla dalla terminazione normale che avviene quando un processo chiama la funzione exit() oppure quando il processo termina la sua esecuzione.
  • Il processo viene messo in pausa. Il processo viene messo in pausa, in gergo tecnico si dice che il processo è in stop, e rimane in attesa di un segnale di ripristino.
  • Il processo viene riesumato. Il processo viene riesumato e riprende la sua esecuzione dal punto in cui era stato interrotto.
  • Viene generato un Core Dump. Il processo viene terminato e il kernel genera un file di dump della memoria del processo. Questo file è utile per analizzare le cause della terminazione del processo. Un file di Core Dump, infatti, può essere caricato in un debugger per analizzare lo stato del processo al momento della terminazione.

Un processo può, tuttavia, decidere di gestire un segnale in modo diverso dalla modalità di default. Il processo può impostare una diversa gestione del segnale o, in gergo tecnico, signal disposition. Questa gestione può essere una delle seguenti:

  • Default: il segnale viene gestito secondo la modalità di default.
  • Ignora: il segnale viene ignorato.
  • Invocazione di un handler: il segnale viene gestito da una funzione chiamata signal handler o gestore del segnale. Questa funzione è definita dal programmatore e viene invocata dal kernel quando il segnale viene ricevuto dal processo.

L'ultima gestione è quella più importante. Utilizzare un signal handler permette al programmatore di gestire il segnale in modo personalizzato. Quando si vuole gestire un segnale con un handler si dice che si installa un handler per quel segnale e quando il segnale viene gestito si dice che il segnale è stato catturato.

In generale, quando si vuole cambiare la gestione di un segnale, non si può esplicitamente scegliere la terminazione di un processo, a meno che questa non sia la gestione di default. In altre parole, non si può decidere di terminare un processo quando si riceve un segnale che di default causerebbe un altro comportamento.

Ricapitolando:

Definizione

Gestione di un Segnale

La gestione di un segnale è la modalità con cui un processo prende in carico un segnale. La gestione può essere una delle seguenti:

  • Default: il segnale viene gestito secondo la modalità di default.
  • Ignora: il segnale viene ignorato.
  • Invocazione di un handler: il segnale viene gestito da una funzione chiamata signal handler o gestore del segnale.
Definizione

Signal Handler o Gestore del Segnale

Un Signal Handler o Gestore del Segnale è una funzione definita dal programmatore che viene invocata dal kernel quando un segnale viene ricevuto. Il signal handler permette di gestire il segnale in modo personalizzato.

Segnali e stato di un processo in Linux

In Linux, per esaminare come un processo ha deciso di gestire i segnali in ingresso, è possibile controllare il file associato al processo in /proc/<pid>/status.

Se, infatti, proviamo ad eseguire il comando:

cat /proc/$$/status

dove $$ rappresenta il PID del processo corrente, otteniamo una serie di informazioni sul processo. Tra queste informazioni, troviamo la sezione dedicata ai segnali:

SigQ:   0/31294
SigPnd: 0000000000000000
ShdPnd: 0000000000000000
SigBlk: 0000000000000000
SigIgn: 0000000000001000
SigCgt: 0000000180000000
  • SigQ (Signals Queued) rappresenta il numero di segnali in coda per il processo.

    Nell'esempio di sopra vi sono zero segnali in coda e la coda può contenere fino a 31294 segnali massimo.

  • SigPnd (Signals Pending) è una maschera di bit che rappresenta i segnali pendenti. Essendo una maschera in esadecimale, ogni cifra rappresenta quattro bit ed ogni bit rappresenta un segnale. Se il bit è a 1, allora il segnale corrispondente alla posizione del bit è pendente.

    Nell'esempio di sopra i bit sono tutti a zero, quindi non ci sono segnali pendenti.

    Se però il valore fosse 0000000000000001, allora il segnale di valore 1 sarebbe pendente, ossia il segnale SIGHUP. Analogamente se il valore fosse 0000000000000002, allora il segnale di valore 2 sarebbe pendente, ossia il segnale SIGINT. E così via.

  • ShdPnd (Shared Signals Pending) è una maschera di bit che rappresenta i segnali pendenti a livello di processo. La differenza rispetto a SigPnd è che quest'ultimo rappresenta i segnali pendenti a livello di thread.

  • SigBlk (Signals Blocked) è una maschera di bit che rappresenta i segnali bloccati. Se un bit è a 1, allora il segnale corrispondente è bloccato.
  • SigIgn (Signals Ignored) è una maschera di bit che rappresenta i segnali ignorati. Se un bit è a 1, allora il segnale corrispondente è ignorato.
  • SigCgt (Signals Caught) è una maschera di bit che rappresenta i segnali per i quali è stato installato un signal handler. Se un bit è a 1, allora il segnale corrispondente è gestito da un signal handler.

Storia dei Segnali in Unix

I segnali sono stati introdotti per la prima volta nel sistema operativo Unix nelle sue primissime versioni. La storia dei segnali in Unix è piuttosto travagliata e, in generale, i segnali sono considerati uno dei punti deboli del suo design.

Essi furono introdotti per la prima volta nel sistema operativo Unix V4 (1972) ma, all'epoca, non era garantito che un segnale venisse consegnato ad un processo: poteva accadere che un segnale venisse perduto. Inoltre, il meccanismo di blocco o mascheramento non era affidabile, per cui poteva addirittura accadere, in certe circostanze, che un segnale venisse consegnato ad un processo che lo aveva bloccato.

Tutti questi problemi furono risolti con l'introduzione dei BSD Signals o Reliable Signals nel sistema operativo 4.2BSD nel 1983. Questi segnali sono quelli che vengono utilizzati ancora oggi nei sistemi Unix like, Linux compreso, e che sono descritti nello standard POSIX.1-1990. Essi sono i segnali standard di cui abbiamo parlato in precedenza.

I segnali Real-Time furono introdotti in un secondo momento, con l'introduzione dello standard POSIX.1b-1993. La motivazione della loro introduzione è data dal fatto che per i segnali standard BSD, come vedremo, non è garantito l'ordine di consegna. Questo significa che se due segnali vengono inviati ad un processo, non è garantito che il primo segnale inviato venga consegnato prima del secondo. Questo comportamento può essere problematico in certi contesti, per esempio quando si vuole implementare un meccanismo di sincronizzazione tra processi. Inoltre, i segnali BSD non sono accodati, ossia se un segnale viene inviato più volte ad un processo, il processo riceverà il segnale una sola volta. Studieremo i segnali Real-Time nelle prossime lezioni in modo separato rispetto ai segnali standard.

In Sintesi

In questa lezione introduttiva abbiamo visto cosa sono i segnali in Linux e in generale nei sistemi Unix like. Abbiamo visto che i segnali sono una forma di notifica asincrona di eventi verso un processo e che possono essere generati da tre tipi di eventi: hardware, software e generati dall'utente.

Abbiamo visto che i segnali sono identificati da un numero intero e che possono essere divisi in due categorie: i segnali standard e i segnali Real-Time. Abbiamo visto che i segnali vengono consegnati ad un processo e che un processo può decidere di gestire un segnale in modo personalizzato attraverso un signal handler.

Infine, abbiamo visto come un processo può decidere di gestire un segnale e come è possibile controllare lo stato dei segnali di un processo attraverso il file /proc/<pid>/status.

Nella prossima lezione vedremo in dettaglio quali sono i segnali standard di Linux (e dei sistemi Unix Like) e quali sono le loro disposizioni di default.