Che cos'è un segnale? I segnali sono interrupt software.
Un programma robusto deve gestire i segnali. Questo perché i segnali sono un modo per fornire eventi asincroni all'applicazione.
Un utente che preme ctrl+c, un processo che invia un segnale per terminare un altro processo ecc. sono tutti casi in cui un processo deve eseguire la gestione del segnale.
Segnali Linux
In Linux, ogni segnale ha un nome che inizia con i caratteri SIG. Ad esempio :
- Un segnale SIGINT che viene generato quando un utente preme ctrl+c. Questo è il modo per terminare i programmi dal terminale.
- Un SIGALRM viene generato allo spegnimento del timer impostato dalla funzione sveglia.
- Un segnale SIGABRT viene generato quando un processo chiama la funzione di interruzione.
- ecc
Quando si verifica il segnale, il processo deve dire al kernel cosa farne. Ci possono essere tre opzioni attraverso le quali un segnale può essere smaltito:
- Il segnale può essere ignorato. Ignorando intendiamo che non verrà fatto nulla quando si verifica il segnale. La maggior parte dei segnali può essere ignorata, ma i segnali generati da eccezioni hardware come la divisione per zero, se ignorati possono avere strane conseguenze. Inoltre, un paio di segnali come SIGKILL e SIGSTOP non possono essere ignorati.
- Il segnale può essere catturato. Quando viene scelta questa opzione, il processo registra una funzione con il kernel. Questa funzione viene chiamata dal kernel quando si verifica quel segnale. Se il segnale non è fatale per il processo, allora in quella funzione il processo può gestire il segnale correttamente o altrimenti può scegliere di terminare con grazia.
- Lascia che l'azione predefinita venga applicata. Ogni segnale ha un'azione predefinita. Questo potrebbe essere terminato, ignorare ecc.
Come abbiamo già affermato, due segnali SIGKILL e SIGSTOP non possono essere ignorati. Questo perché questi due segnali forniscono un modo per l'utente root o il kernel di terminare o interrompere qualsiasi processo in qualsiasi situazione. L'azione predefinita di questi segnali è terminare il processo. Né questi segnali possono essere catturati né ignorati.
Cosa succede all'avvio del programma?
Tutto dipende dal processo che chiama exec. Quando il processo viene avviato, lo stato di tutti i segnali è ignorato o predefinito. È l'ultima opzione che ha maggiori probabilità di verificarsi a meno che il processo che chiama exec non ignori i segnali.
È proprietà delle funzioni exec cambiare l'azione su qualsiasi segnale in modo che sia l'azione predefinita. In termini più semplici, se il genitore ha una funzione di cattura del segnale che viene chiamata all'occorrenza del segnale, se quel genitore esegue un nuovo processo figlio, allora questa funzione non ha significato nel nuovo processo e quindi la disposizione dello stesso segnale è impostata sul valore predefinito nel nuovo processo.
Inoltre, poiché di solito abbiamo processi in esecuzione in background, quindi la shell imposta semplicemente la disposizione del segnale di uscita come ignorata poiché non vogliamo che i processi in background vengano terminati da un utente che preme un tasto ctrl+c perché ciò vanifica lo scopo di creare un processo eseguito in background.
Perché le funzioni di cattura del segnale dovrebbero essere rientranti?
Come abbiamo già discusso, una delle opzioni per la disposizione del segnale è catturare il segnale. Nel codice di processo questo viene fatto registrando una funzione nel kernel che il kernel chiama quando si verifica il segnale. Una cosa da tenere a mente è che la funzione che il processo registra deve essere rientrante.
Prima di spiegarne il motivo, capiamo prima cosa sono le funzioni rientranti? Una funzione rientrante è una funzione la cui esecuzione può essere interrotta nel frattempo per qualsiasi motivo (ad esempio a causa di un'interruzione o di un segnale) e quindi può essere nuovamente inserita in modo sicuro prima che le sue precedenti invocazioni completino l'esecuzione.
Tornando al problema, supponiamo che una funzione func() sia registrata per una chiamata di ritorno su un'occorrenza del segnale. Assumiamo ora che questo func() fosse già in esecuzione quando si è verificato il segnale. Poiché questa funzione è richiamata per questo segnale, l'esecuzione corrente su questo segnale verrà interrotta dallo scheduler e questa funzione verrà richiamata di nuovo (a causa del segnale).
Il problema può essere se func() funziona su alcuni valori globali o strutture di dati che vengono lasciati in uno stato incoerente quando l'esecuzione di questa funzione è stata interrotta a metà, la seconda chiamata alla stessa funzione (a causa del segnale) potrebbe causare alcuni risultati indesiderati.
Quindi diciamo che le funzioni di cattura del segnale dovrebbero essere rese rientranti.
Fare riferimento ai nostri articoli send-signal-to-process e Linux fuser command per vedere esempi pratici su come inviare segnali a un processo.
Fili e segnali
Abbiamo già visto in una delle sezioni precedenti che la gestione del segnale ha un proprio bit di complessità (come l'utilizzo di funzioni rientranti). Per aumentare la complessità, di solito abbiamo applicazioni multi thread in cui la gestione del segnale diventa davvero complicata.
Ogni thread ha la propria maschera di segnale privata (una maschera che definisce quali segnali possono essere consegnati) ma il modo in cui viene eseguita la disposizione del segnale è condiviso da tutti i thread nell'applicazione. Ciò significa che una disposizione per un particolare segnale impostato da un thread può essere facilmente annullata da qualche altro thread. In questo caso il meccanismo di disposizione cambia per tutti i fili.
Ad esempio, un thread A può scegliere di ignorare un particolare segnale, ma un thread B nello stesso processo può scegliere di catturare lo stesso segnale registrando una funzione di callback nel kernel. In questo caso la richiesta fatta dal thread A viene annullata dalla richiesta del thread B.
I segnali vengono consegnati solo a un singolo thread in qualsiasi processo. A parte le eccezioni hardware o la scadenza del timer (che vengono consegnate al thread che ha causato l'evento), tutti i segnali vengono passati al processo in modo arbitrario.
Per contrastare questa carenza ci sono alcune API posix come pthread_sigmask() ecc. che possono essere utilizzate.
Nel prossimo articolo (parte 2) di questa serie, discuteremo di come catturare i segnali in un processo e spiegheremo l'aspetto pratico della gestione dei segnali utilizzando frammenti di codice.