Michele Apicella: Io lo so che tipo è lei: ha il suo macellaio di fiducia, che le tiene i pezzi migliori...
Bianca: Perché, c'è qualcosa di male?
Michele Apicella: E certo che c'è. Se ci vado io, poi mi prendo i pezzi peggiori!
Non so se ricordate, ma tempo fa scrissi un articolo sui signal handler, un argomento interessante, importante e, sfortunatamente, misconosciuto. Visto che ultimamente ho scritto alcuni signal handler un po' sofisticati ho pensato che era il caso di scrivere una seconda parte, visto che nella prima avevo fornito informazioni (spero) complete sulle basi, descrivendo i segnali e i tipi di funzione usabili (e anche un esempio reale!), restando, però, sempre a "livello base", senza complicare troppo il discorso. E, quindi, ci vuole una seconda parte, no?
Io direi che adesso vi date un ripassino del vecchio articolo e poi tornate qua, pronti a vedere come si fa un signal handler veramente professionale. Ah, dimenticavo; l'altra volta il film citato era Bianca del grande Nanni Moretti, quindi per non confondere le acque, ho scelto un bel dialogo (quello qui sopra) brillante e caustico come il film stesso (e come l'argomento del post, spero).
![]() |
...E se il macellaio ha finito i migliori signal handler che faccio?... |
Ok, dopo aver riletto il vecchio articolo dovreste avete ben chiaro cos'è e come si scrive un signal handler (che poi se eravate già esperti dell'argomento non cera bisogno di leggere quell'articolo e magari neanche questo, ah ah ah). Il vecchio esempio prevedeva un signal handler di due (2!) righe che maneggiava un flag (del tipo opportuno) e scriveva (nella maniera opportuna) un messaggio su stderr. E la preparazione del signal handler, anche questa super-semplificata, era scritta direttamente nel main del programma.
Ma un signal handler per una applicazione reale di produzione è possibile e probabile che debba fare più cose: ad esempio scrivere usando (correttamente) write(2) su stderr potrebbe essere insufficiente, dato che le applicazioni professionali di produzione a volte non hanno neanche una console, ma lavorano in background. Infatti un caso reale è scrivere sul syslog di sistema, ma bisogna farlo nella maniera opportuna, perché bisogna ricordare questo:
An async-signal-safe function is one that can be safely called from withina signal handler. Many functions are not async-signal-safe. In particular,nonreentrant functions are generally unsafe to call from a signal handler.da: signal-safety(7) Linux Programmer's Manual
una descrizione che, unita all'altra citazione che ho riportato nel vecchio articolo (quella da C Rationale, 7.14.1.1 [C99 Rationale 2003]), ci fa capire che dobbiamo andare coi piedi di piombo per scrivere nel syslog, ovvero: dobbiamo scriverci senza usare le funzioni della famiglia syslog(3)! (che, infatti sono funzioni NON async-signal-safe).
E come si può fare, allora? Beh, il trucco è il seguente: si può scrivere nel syslog usando lo stesso meccanismo che il demone Linux rsyslogd(8) usa internamente, e cioè scrivendo (con l'accortezza di usare solo funzioni async-signal-safe) sul un socket aperto su /dev/log (è un socket creato dallo stesso syslog di Linux alla partenza del sistema).
Ma qui ci vuole, allora, un nuovo esempio! Ok, ho preso il codice del vecchio articolo e l'ho modificato nella maniera seguente:
- Ho scritto una funzione sigHandler() che scrive sul socket di /dev/log tramite write(2) che è una system-call async-signal-safe.
- Ho scritto una funzione sigAction() che prepara le attività del signal handler in maniera un po' più sofisticata rispetto al vecchio esempio.
- Ho scritto una funzione connectSyslog() che si connette al socket di /dev/log.
Notare che il socket deve essere aperto usando una variabile locale di tipo volatile sig_atomic_t che è l'unica compatibile con questo tipo di meccanismo (leggere la nota nel codice!). Notare anche che sigAction() e connectSyslog() possono anche contenere funzioni NON async-signal-safe (come fprintf(3)) perché si chiamano direttamente nel main, fuori dal signal handler.
Vai col codice!
#include <stdio.h>#include <unistd.h>#include <errno.h>#include <signal.h>#include <sys/socket.h>#include <sys/un.h>#define SIGINT_MSG1 "<11>ricevuto un SIGSEGV/SIGINT/SIGBUS/SIGABRT. Esco con errore."#define SIGINT_MSG2 "<11>ricevuto un SIGTERM. Esco correttamente."// local prototypesstatic void sigHandler(int signum);static void sigAction();static int connectSyslog();/* un flag per controllare il loop pseudo-infinito + una variabile per il socket.volatile potrebbe essere necessario a seconda del sistema/implementazionein uso. (vedere "C11 draft standard n1570: 5.1.2.3") */static volatile sig_atomic_t resta_attivo = 1;static volatile sig_atomic_t log_sock;// main - funzione mainint main(int argc, char* argv[]){// prepara il signal handlerlog_sock = connectSyslog();sigAction();// un loop pseudo-infinito per testare la applicazionewhile (resta_attivo)sleep(1);// il loop pseudo-infinito è stato interrotto da un SIGTERMprintf("programma terminato correttamente\n");return 0;}// sigHandler - la funzione del signal handlerstatic void sigHandler(int signum){// salvo errno (un buon sighandler lo deve fare!)int my_errno = errno;// eseguo la azione necessaria (che normalmente è una exit)switch (signum) {case SIGSEGV:case SIGINT:case SIGBUS:case SIGABRT:// la applicazione esce perché il segnale è considerato causa di interruzione forzatawrite(log_sock, SIGINT_MSG1, sizeof(SIGINT_MSG1));_exit(signum);case SIGTERM:// set del flag "resta_attivo" per forzare l'uscita controllata del main loopwrite(log_sock, SIGINT_MSG2, sizeof(SIGINT_MSG2));resta_attivo = 0;break;default:break;}// ripristino l'errnoerrno = my_errno;}// sigAction - set delle azioni sui segnali da catturarestatic void sigAction(){// init dei dati di sigaction(2)struct sigaction sa;memset(&sa, 0, sizeof(sa));sa.sa_handler = sigHandler; // usa sa_handler per il signal handlersa.sa_flags = SA_RESTART;// maschera tutti i segnaliif (sigfillset(&sa.sa_mask) == -1)fprintf(stderr, "%s: sigfillset error: %s\n", __func__, strerror(errno));// set azioni per i segnali critici SIGSEGV, SIGINT, SIGBUS e SIGABRTif (sigaction(SIGSEGV, &sa, nullptr) == -1)fprintf(stderr, "%s: sigaction error for SIGSEGV: %s\n", __func__, strerror(errno));if (sigaction(SIGINT, &sa, nullptr) == -1)fprintf(stderr, "%s: sigaction error for SIGINT: %s\n", __func__, strerror(errno));if (sigaction(SIGBUS, &sa, nullptr) == -1)fprintf(stderr, "%s: sigaction error for SIGBUS: %s\n", __func__, strerror(errno));if (sigaction(SIGABRT, &sa, nullptr) == -1)fprintf(stderr, "%s: sigaction error for SIGABRT: %s\n", __func__, strerror(errno));// set azione per il segnale controllato SIGTERMif (sigaction(SIGTERM, &sa, nullptr) == -1)fprintf(stderr, "%s: sigaction error for SIGTERM: %s\n", __func__, strerror(errno));}// connectSyslog - collego il socket del syslog (/dev/log)static int connectSyslog(){// preparo la struct sockaddr_un per il collegamento via Unix Domain Socketstruct sockaddr_un reader;memset(&reader, 0, sizeof(reader));reader.sun_family = AF_UNIX;snprintf(reader.sun_path, sizeof(reader.sun_path), "%s", "/dev/log");// provo a creare il socket in modo IPC Streamint log_sock;if ((log_sock = socket(AF_UNIX, SOCK_STREAM, 0)) != -1) {// connect to the syslog socketif (connect(log_sock, (struct sockaddr *)&reader, sizeof(reader)) != -1) {// successo della connect(2)return log_sock;} else {// errore connect(2)fprintf(stderr, "%s: STREAM connect error: %s\n", __func__, strerror(errno));close(log_sock);}}// errore IPC Stream: provo a creare il socket in modo IPC Datagramif ((log_sock = socket(AF_UNIX, SOCK_DGRAM, 0)) != -1) {// connect to the syslog socketif (connect(log_sock, (struct sockaddr *)&reader, sizeof(reader)) != -1) {// successo della connect(2)return log_sock;} else {// errore connect(2)fprintf(stderr, "%s: DGRAM connect error: %s\n", __func__, strerror(errno));close(log_sock);}}// connessione non effettuatareturn -1;}
come avrete sicuramente notato, il codice è molto simile a quello del vecchio articolo ma con l'aggiunta dei punti descritti sopra. Ed è stra-commentato, quindi non è necessario aggiungere molte spiegazioni. Questa nuova versione scrive correttamente nel syslog, in base al tipo di segnale ricevuto, e quindi, se inviamo un SIGTERM (segnale 15) al PID dell'applicazione testsighandler:
aldo@Linux $ kill -15 64872aldo@Linux $
la applicazione scriverà sul terminale :
aldo@Linux $ ./testsighandlerprogramma terminato correttamentealdo@Linux $
e il syslog registrerà questa linea in /var/log/syslog:
2025-06-25T16:22:39.383661+02:00 Linux ricevuto un SIGTERM. Esco correttamente.
Mentre se inviamo all'applicazione uno dei segnali "trappati" come errori irrecuperabili (SIGSEGV, SIGINT, ecc.) succederà questo (usando, ad esempio un SIGBUS (segnale 7)):
aldo@Linux $ kill -7 65071aldo@Linux $
La applicazione uscirà senza scrivere nulla sul terminale e il syslog registrerà questa linea in /var/log/syslog:
2025-06-25T16:22:57.535779+02:00 Linux ricevuto un SIGSEGV/SIGINT/SIGBUS/SIGABRT. Esco con errore.
Io direi che funziona bene, no? E, teoricamente, con questo avremmo finito, ma... In realtà potrebbe essere necessario complicare ulteriormente il codice per alcune esigenze particolari. Ad esempio:
- Si può verificare sperimentalmente che, su alcuni sistemi Linux non completamente "canonici" (come alcune implementazioni embedded oppure il terribile WSL), se viene fatto un restart del processo rsyslogd il signal handler appena visto non riesce più a scriver sul socket del syslog. Invece sui sistemi Linux standard (Mint, Ubuntu, ecc.) il problema non si presenta.
- In alcuni casi potrebbe essere necessario aggiungere dettagli "dinamici" nelle stringhe destinate al syslog, ma come farlo senza usare, ad esempio, la snprintf(3)?
Ecco, nella terza parte dell'articolo (ebbene si! Ci sarà una terza parte!) vi mostrerò come risolvere i due problemini appena elencati. Ma mi raccomando: non trattenete il respiro nell'attesa! (può nuocere gravemente alla salute, ah ah ah).
Ciao, e al prossimo post!