Segnali

I segnali costituiscono una forma (molto semplice) di comunicazione tra processi: Tecnicamente sono interruzioni software causate da svariati eventi:

  • Generati da terminale. Ad esempio ctrl-c (SIGINT);
  • Eccezioni dovute ad errori in esecuzione: es. divisione per 0, riferimento “sbagliato” in memoria, ecc…
  • Segnali esplicitamente inviati da un processo all’altro;
  • Eventi asincroni che vengono notificati ai processi: esempio SIGALARM.

Cosa possiamo fare quando arriva un segnale?

  • Ignorarlo
  • Gestirlo
  • Lasciare il compito al gestore di sistema

Vediamo alcuni segnali POSIX (Portable Operating System Interface) supportati in Linux. La lista qui sotto è contenuta in 'man 7 signal'

Azioni possibili:

Un esempio semplice: alarm

alarm manda un SIGALRM dopo n secondi. Il default handler termina il programma (serve per dare un timeout). La shell analizza lo stato di uscita del programma e stampa il motivo della terminazione (“alarm clock”).

Vediamo un semplice esempio di programma che setta l’allarme dopo 3 secondi e poi va in loop infinito:

Il programma “punta una sveglia” dopo 3 secondi e attende in un ciclo infinito

Dopo 3 secondi il processo termina (viene anche scritto a terminale il motivo della terminazione in quanto la shell legge e interpreta il valore di ritorno del programma).

Impostare il gestore dei segnali tramite ‘signal’

Tramite la system call signal è possibile cambiare il gestore dei segnali. La system call prende come parametri un segnale e una funzione che da quel momento diventerà il nuovo gestore del segnale. Vediamo un semplice esempio:

Dopo tre secondi arriva il segnale SIGALRM. Viene invocato alarmHandler che stampa a video una frase e reimposta l’allarme dopo 3 secondi. Il controllo ritorna al punto in cui il programma è stato interrotto (nel ciclo while quindi) e il programma attende altri 3 secondi che arrivi il successivo segnale:

Parametri particolari e valore di ritorno

È possibile passare alla signal le costanti SIG_IGN o SIG_DFL al posto della funzione handler per indicare, rispettivamente:

  • che il segnale va ignorato
  • che l’handler è quello di default di sistema

Il valore di ritorno di signal è

  • SIG_ERR in caso di errore
  • l’handler precedente, in caso di successo

NOTA: sigaction rimpiazza signal con un’implementazione più stabile nelle varie versioni UNIX. Viene raccomandata se si vuole portabilità. Ad esempio, la signal originale UNIX, e la sua versione System V, fa il reset del gestore a SIG_DFL ogni volta che viene ricevuto il segnale. In Linux, si ottiene questo comportamento particolare compilando con l’opzione --ansi. Per l’uso di sigaction si faccia riferimento al manuale.

Esempio: Proteggersi da ctrl-c

Vediamo ora un altro esempio di gestione dei segnali. Se modifichiamo il gestore del segnale SIGINT possiamo evitare che un programma venga interrotto tramite ctrl-c da terminale.

Notare l’uso del valore di ritorno della signal per reimpostare il gestore originale. La signal, quando va a buon fine, ritorna il gestore precedente del segnale, che salviamo nella variabile old. Quando vogliamo reimpostare tale gestore è sufficiente passare old come secondo parametro a signal.

Se eseguiamo il programma possiamo osservare che per 3 secondi ctrl-c non ha alcun effetto. Appena viene reimpostato il vecchio gestore, invece, ctrl-c interrompe il programma.

Sospensione e ripristino di processi tramite kill

La chiamata a sistema kill manda un segnale a un processo.

In questo esempio mostriamo come la chiamata a sistema kill possa essere utilizzata per sospendere, ripristinare e terminare un processo.

Quando eseguiamo il programma notiamo che il primo figlio viene sospeso per 5 secondi e che alla fine entrambi i processi figli sono terminati:

Mascherare i segnali

A volte risulta utile bloccare temporaneamente la ricezione dei segnali per poi riattivarli. Tali segnali non sono ignorati ma solamente ‘posticipati’.

NOTA POSIX non specifica se più occorrenze dello stesso segnale debbano essere memorizzate (accodate) oppure no. Tipicamente se più segnali uguali vengono generati, solamente uno verrà “recapitato” quando il blocco viene tolto.

La chiamata a sistema sigprocmask(int action, sigset_t *newmask, sigset_t *oldmask) compie azioni differenti a seconda del valore del primo parametro action:

  • SIG_BLOCK: l’insieme dei segnali newmask viene unito all’insieme dei segnali attualmente bloccati, che sono restituiti in oldmask;
  • SIG_UNBLOCK: l’insieme dei segnali newmask viene sottratto dai segnali attualmente bloccati, sempre restituiti in oldmask;
  • SIG_SETMASK: l’insieme dei segnali newmask sostituisce quello dei segnali attualmente bloccati (oldmask).

Per gestire gli insiemi di segnali (di tipo sigset_t) si utilizzano:

  • sigemptyset(sigset_t *set) che inizializza l’insieme set all’insieme vuoto
  • sigaddset(sigset_t *set, int signum) che aggiunge il segnale signum all’insieme set

L’esempio mostra come bloccare SIGINT e poi ripristinarlo.

Se digitiamo ctrl-c mentre il segnale è mascherato esso viene sospeso. Appena la maschera viene rimossa, il segnale è ricevuto dal processo che viene immediatamente interrotto. Non stiamo quindi modificando il gestore del segnale ma solo sospendendone la ricezione per un periodo di tempo.

Tramite sigpending è possibile ottenere l’insieme dei segnali “pendenti” (vedere il manuale per maggiori informazioni)

NOTA: sigprocmask non aderisce allo standard ANSI (ISO C99) ma solamente allo standard POSIX. La gestione dei segnali da parte di ANSI-C è molto povera e si riduce solamente a signal, raise(sig), che equivale a kill(getpid(),sig), e abort(), cioè “abnormal termination”. ANSI-C non prevede multi-processing e quindi non prevede l’invio di segnali ad altri processi (sezione 7.14.2.1).

Attendere un segnale tramite ‘pause’

Negli esempi abbiamo sempre usato while(1){} per attendere un segnale (busy-waiting). La system call pause() attende un segnale senza consumare tempo di CPU. Vediamo un esempio:

Interferenze e funzioni ‘safe’ POSIX

L’uso della printf nell’handler è rischioso perchè usa dati globali: Se anche il programma interrotto stava facendo I/O i due potrebbero interferire! printf non è “safe”.

Facendo man 7 signal (in sistemi Linux recenti), troviamo la lista di funzioni safe che sicuramente non creano problemi di interferenza:

Il seguente esempio cerca di far interferire le printf aggiungendo nel ciclo while la stampa di una stringa. Se il programma viene interrotto proprio durante la stampa, la printf del gestore potrebbe interferire con quella del programma.

Tipicamente le stampe sono troncate o mischiate. In alcuni casi si possono addirittura perdere alcune printf (e/o alcuni a-capo) perché eseguite a metà di un’altra printf. In casi estremi può accadere che il programma vada in errore e si blocchi.

NOTA è necessario eseguire il comando con in coda “ | grep io” per evitare di osservare tutte le stampe “prova”. Il comando 'grep' stampa solo le linee contenenti la stringa “io” (che compare in “questo me lo gestisco io”). la pipe ‘|’ fa si che l’output del comando venga reindirizzato al comando successivo come input (è un modo per creare ‘al volo’ un canale di comunicazione message passing tra due processi, che approfondiremo la prossima volta).

Leave a Reply

Your email address will not be published. Required fields are marked *