24 Jun 2023, 8:27 PM
24 Jun 2023, 8:27 PM

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

Segnali.png|700

Implementazione

Per ogni processo, all’interno della process table, vengono mantenute due liste:

Ad ogni schedulazione del processo le due liste vengono controllate per consentire al processo di reagire nella maniera più adeguata.

Visione concettuale dei segnali

Implementazione segnali.png|600

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):

Nota

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ò:

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:

#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:

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:

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).

Job number vs PID

  • 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
Attenzione!

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

Segnali-2.png|600

La signal mask

SignalMask.png|600

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
Attenzione!

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:

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:

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:

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.