Signal Handler – come si scrive un signal handler in C

S

Signal Handler come si scrive un signal handler in C

Michele Apicella: ...il Mont Blanc si regge su un equilibrio delicato, non è come la Sacher Torte...
Mario: Cosa?
Michele Apicella: La Sacher Torte...
Mario: Cos'è?
Michele Apicella: Cioè, lei praticamente non ha mai assaggiato la Sacher Torte?
Mario: No.
Michele Apicella: Vabbè, continuiamo così, facciamoci del male...

Signal Handler, un titolo secco e breve che mi fa venire in mente Bianca del grande Nanni Moretti (ogni scusa è buona per ricordarsi di un gran film). Anche qui un titolo secco per un film bellissimo, che ci presenta, con magistrali toni da commedia, argomenti tragici come solitudine, nevrosi e ossessioni… e il momento mitico citato in apertura ci ricorda che alcune cose date per scontate (la Sacher Torte) possono essere meno scontate di quello che sembra, come i Segnali di POSIX (e quindi di UNIX/Linux).

Signal Handler
…Cioè, lei praticamente non ha mai scritto un Signal Handler?…

Allora, in questo articolo parleremo di segnali, che sono la forma più semplice e antica di IPC nei sistemi POSIX. Anzi, dovrei aggiungere che nel ciclo di articoli sulla POSIX IPC (qui, qui e qui le tre parti) non ne ho parlato volutamente: e perché? Beh, in quel ciclo si parlava di comunicazione tra processi a livello di trasferimento di dati (e quindi con messaggi, memoria condivisa, ecc.), con invio e ricezione delle più svariate informazioni. I segnali, invece, sono una forma di comunicazione sicura e potente, ma limitata a un set di informazioni ben precise e codificate. Ossia sono del tipo: “Ti invio il segnale n.9 e tu sai già cosa devi fare”.

Come detto, i segnali sono la forma più antica di comunicazione tra processi: sono anteriori a POSIX (che si avviò nel 1988) e sono, addirittura, anteriori anche alla prima edizione del nome IPC, che venne usato per la prima volta nel 1983 per denominare il System V IPC di Unix SVR1 (Unix System V Release 1). Ebbene si, i segnali sono veramente antichi: sono apparsi per la prima volta in Unix V4 (Unix 4th Edition), nel 1973! Quanto tempo è passato…

Allora, come detto sopra, i segnali si limitano a “un set di informazioni ben precise e codificate”, che si possono listare usando il comando “kill -l”  e sulla mia macchina Linux sono queste:

aldo@Linux $ kill -l
 1) SIGHUP      2) SIGINT        3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT     7) SIGBUS        8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

Sono un po’ più di quelli standard (che sono 28) descritti nello standard POSIX, infatti almeno gli ultimi 30 della lista mostrata hanno un uso molto limitato e speciale. Il comando kill(1), è disponibile nella shell di Linux, e usa internamente le system call che ci interessano in questa sede, quelle relative ai segnali.

Il meccanismo d’uso dei segnali che può essere implementato nelle applicazioni che scriviamo è relativamente semplice:

  1. Un processo può inviare a un altro processo uno dei segnali listati qua sopra usando la system call kill(2).
  2. Il processo ricevente eseguirà l’azione di default collegata al segnale ricevuto, a meno che non abbia previamente definito un signal handler.
  3. Un processo può definire un signal handler (ad esempio usando la system call signal(2)) per trattare opportunamente i segnali ricevuti

Punto. Cioè, in realtà l’argomento è un po’ più complicato, ma sommariamente si può sintetizzare con i tre punti appena descritti.

1) Si Comincia con signal(2)

E quindi, cosa è un signal handler? È, come dice il nome, una funzione che “maneggia” opportunamente i segnali. Come dite? Non è chiaro? Volete un semplice esempio? Eccolo!

#include<stdio.h>
#include<signal.h>
#include<unistd.h>

// prototipi locali
static void sigHandler(int signum);

// main() - funzione main
int main(void)
{
    // WARNING! usiamo signal() solo per test!

    // creo un signal handler con signal() per SIGKILL (sig n.9)
    if (signal(SIGKILL, sigHandler) == SIG_ERR)
        printf("non posso intercettare SIGKILL\n");

    // creo un signal handler con signal() per SIGINT (sig n.2)
    if (signal(SIGINT, sigHandler) == SIG_ERR)
        printf("non posso intercettare SIGINT\n");

    // un loop infinito per testare la applicazione
    for (;;)
        sleep(1);

    return 0;
}

// sigHandler() - un semplice signal handler
static void sigHandler(int signum)
{
    // WARNING! usiamo printf() solo per test!

    // mostro l'arrivo di uno dei segnali intercettati
    if (signum == SIGKILL)
        printf("ricevuto un SIGKILL\n");
    else if (signum == SIGINT)
        printf("ricevuto un SIGINT\n");
}

Che ne dite? È un codice semplicissimo, di poche righe (e anche ben commentate).

Sorvolando sui due Warning inseriti nei commenti (ci torneremo dopo) è evidente che è relativamente semplice scrivere un signal handler: basta chiamare signal(2) indicando quale segnale vogliamo intercettare e con quale funzione vogliamo farlo. Poi, ovviamente, dobbiamo scrivere la funzione di handler, che può fare cose più o meno sofisticate, e in questo programma di test si limita a dire che ha intercettato il segnale. E vediamo cosa ci mostra il programma durante l’esecuzione:

aldo@Linux $ ./signal
non posso intercettare SIGKILL
^Cricevuto un SIGINT
ricevuto un SIGINT
^Cricevuto un SIGINT
Ucciso

Allora: ho eseguito il programma e subito ho ricevuto il messaggio di errore “non posso intercettare SIGKILL”  perché, sfortunatamente, alcuni segnali non si possono intercettare, e SIGKILL è proprio uno di questi (tenetelo presente quando scrivete un signal handler!). Dopodiché il programma non fa nulla (ha un loop infinito di sleep(3)) e si risveglia quando arriva un segnale intercettato, nel nostro caso SIGINT che è il “interrupt signal” e si può inviare semplicemente scrivendo “Ctrl + C” (righe 3 e 5 del test) oppure usando il comando shell kill(1) da un altro terminale (riga 4 del test). Quando il nostro programma riceve un segnale non intercettato esegue l’azione standard per quel tipo di segnale e, nel nostro caso, ho inviato un SIGKILL con “kill -9 pid-del-processo” , e il processo è stato ucciso (riga 6 del test. E questa è, tristemente, l’azione standard di SIGKILL).

La struttura del programma di test ci insegna che è possibile installare un signal handler che tratta più segnali alla volta, basta aggiungere altre chiamate a signal(2) nel main() e altri “else if”  nella funzione sigHandler(). E ogni segnale intercettato può eseguire diverse attività quando viene ricevuto, così si può personalizzare notevolmente il comportamento in varie situazioni di emergenza e non. E quale è un esempio molto tipico di uso un signal handler? Io direi la gestione dell’uscita controllata: ossia si può decidere che alla ricezione di un determinato segnale (tipicamente SIGTERM) il nostro programma esegua una serie di attività (tipo fermare dei thread, scrivere dei buffer su disco, liberare la memoria, ecc.) e poi uscire. Et voilà!

E ora veniamo alle dolenti note, quelle indicate nei due Warning del programma di test:

– WARNING! usiamo signal() solo per test!

Abbiamo visto che usare signal(2) è relativamente semplice, però, sfortunatamente, il suo uso è scoraggiato, e il motivo è ben spiegato nel manuale:

...The behavior of signal() varies across UNIX versions, and has also var‐
ied historically across different versions of Linux.   Avoid  its  use:
use sigaction(2) instead.  See Portability below.

...The effects of signal() in a multithreaded process are unspecified.

...POSIX.1  solved  the portability mess by specifying sigaction(2), which
provides explicit control of the semantics when a signal handler is in‐
voked; use that interface instead of signal().

                                   da: SIGNAL(2) Linux Programmer's Manual

ossia, signal(2) è standard (è addirittura parte del ISO C/ANSI C/Standard C) però fornisce comportamenti imprevedibili nel multithreading e, soprattutto, ha problemi di portabilità (il che è un po’ assurdo per una funzione standard del C!). POSIX ha risolto il problema deprecando signal(2) e inserendo la sigaction(2) che è, definitivamente, l’interfaccia raccomandata per tutte le nuove applicazioni (anche se, paradossalmente, questa nuova funzione è meno portabile in assoluto, perché non fa parte del Standard C ma solo di POSIX).

Comunque, è importante evidenziare che è assolutamente sconsigliato mischiare signal(2) e sigaction(2) nello stesso programma: lo standard POSIX stesso, per evitare problemi di questo tipo, raccomanda di implementare (a livello libreria) signal(2) usando la sigaction(2) stessa (e mi risulta che su Linux sia così). La sigaction(2) la vedremo tra poco.

– WARNING! usiamo printf() solo per test!

Questo è un problema più semplice da descrivere: la printf(3) è una funzione che non fa parte di una lista di funzioni che sono, per così dire, “signal-handler-safe” (in realtà il nome esatto è When a signal occurs, the normal flow of control of a program is interrupted. If a signal occurs that is being trapped by a signal handler, that handler is invoked. When it is finished, execution continues at the point at which the signal occurred. This arrangement can cause problems if the signal handler invokes a library function that was being executed at the time of the signal. da: C Rationale, 7.14.1.1 [C99 Rationale 2003]

2) Si prosegue con sigaction(2)

E ora credo che siamo pronti per esaminare una versione più accettabile del programma di test. Vai col codice!

#include <signal.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>

#define SIGINT_MSG "ricevuto un SIGINT\n"

// prototipi locali
static void sigHandler(int signum);

/* un flag per mantenere attivo il loop pseudo-infinito.
   volatile potrebbe essere necessario a seconda del sistema/implementazione
   in uso. (vedere "C11 draft standard n1570: 5.1.2.3") */
volatile sig_atomic_t resta_attivo = 1;

// main() - funzione main
int main(int argc, char *argv[])
{
    // preparo i dati per il signal handler
    struct sigaction sa;
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = sigHandler;
    sa.sa_flags = SA_RESTART;

    // creo un signal handler con signal() per SIGINT (sig n.2)
    if (sigaction(SIGINT, &sa, NULL) == -1)
        printf("non posso intercettare SIGINT\n");

    // un loop pseudo-infinito per testare la applicazione
    while (resta_attivo)
        sleep(1);

    // il loop pseudo-infinito è stato interrotto da un SIGINT
    printf("loop teminato\n");
    return 0;
}

// sigHandler() - un semplice signal handler
static void sigHandler(int signum)
{
    // mostro l'arrivo di un SIGINT e resetta il flag resta_attivo
    write(STDERR_FILENO, SIGINT_MSG, sizeof(SIGINT_MSG));
    resta_attivo = 0;
}

Il codice è molto simile a quello presentato prima, ma con alcune interessanti varianti:

Ed ora possiamo vedere cosa ci mostra il programma durante l’esecuzione:

aldo@Linux $ ./sigaction
^Cricevuto un SIGINT
loop teminato
aldo@Linux $  ./sigaction
ricevuto un SIGINT
loop teminato
aldo@Linux $  ./sigaction
Ucciso

Allora: l’esecuzione del programma non mostra nessun messaggio di errore visto che intercetta solo SIGINT. Il programma non fa nulla (entra nel loop pseudo-infinito di sleep(3)) e si risveglia quando arriva un segnale SIGINT: la prima esecuzione ho scritto “Ctrl + C” (riga 2 del test) e il programma è uscito fermando il loop e scrivendo il messaggio di ricezione. Nella successiva esecuzione ho usato, da un altro terminale (riga 5 del test), il comando kill(1) per inviare il segnale SIGINT: anche in questa esecuzione il programma mi ha mostrato il messaggio di ricezione ed è uscito fermando il loop. Infine ho eseguito di nuovo il programma e ho inviato un SIGKILL dal secondo terminale: il processo è stato ucciso e non ha scritto nessun messaggio (SIGKILL non si può intercettare).

Che ne dite? Per oggi può bastare, no? Come dite? L’articolo non vi è piaciuto?  Vabbè , continuiamo così, facciamoci del male….

Ciao, e al prossimo post!

A proposito di me

Aldo Abate

È un Cinefilo prestato alla Programmazione in C. Per mancanza di tempo ha rinunciato ad aprire un CineBlog (che richiede una attenzione quasi quotidiana), quindi ha deciso di dedicarsi a quello che gli riesce meglio con il poco tempo a disposizione: scrivere articoli sul suo amato C infarcendoli di citazioni Cinematografiche. Il risultato finale è ambiguo e spiazzante, ma lui conta sul fatto che il (si spera) buon contenuto tecnico induca gli appassionati di C a leggere ugualmente gli articoli ignorando la parte Cinefila.
Ma in realtà il suo obiettivo principale è che almeno un lettore su cento scopra il Cinema grazie al C...

Di Aldo Abate

Gli articoli più letti

Articoli recenti

Commenti recenti