Nei titoli e nei testi troverete qualche rimando cinematografico (ebbene si, sono un cinefilo). Se non vi interessano fate finta di non vederli, già che non sono fondamentali per la comprensione dei post...

Di questo blog ho mandato avanti, fino a Settembre 2018, anche una versione in Spagnolo. Potete trovarla su El arte de la programación en C. Buona lettura.

mercoledì 9 luglio 2025

Signal Handler
come scrivere un signal handler in C - pt.3

Michele Apicella: [Sfogandosi con una delle sue piante che sta seccando e morendo] Hai troppo sole, poco sole, cos'è che vuoi? Più acqua, meno acqua? Perché non parli!? Rispondi! [rovescia il vaso].

Dove eravamo rimasti? Ah si! Avevo chiuso la seconda parte dell'articolo sui signal handler (e la prima parte la trovate qui) elencando due possibili problemi residui da risolvere in qualche maniera. Avevo scritto:

  1. 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(8) il signal handler appena visto non riesce più a scrivere sul socket del syslog. Invece sui sistemi Linux standard (Mint, Ubuntu, ecc.) il problema non si presenta.
  2. In alcuni casi potrebbe essere necessario aggiungere dettagli “dinamici”  nelle stringhe destinate al syslog, ma come farlo senza usare, ad esempio, la snprintf(3)?

E quindi, come promesso, oggi ci dedicheremo a questa versione "plus" del nostro signal handler, sempre accompagnati dal nostro Michele Apicella (Nanni Moretti) che, nel bellissimo film Bianca, se la prendeva anche con le piante, un po' come succede a me (a noi) quando un programma non risponde bene ai segnali... (ah ah ah).

...ti avevo detto di uscire quando arrivava il segnale 15!...
E proseguiamo con il nostro nuovo signal handler che farà un po' più rispetto al già buon esempio dello scorso articolo. Ma, visto che questo "un po' più" mi preoccupa comincio con una avvertenza:
...All functions not in the above lists are considered to be unsafe with respect
to signals. That is to say, the behaviour of such functions when called from a
signal handler is undefined. In general though, signal handlers should do little
more than set a flag; most other actions are not safe...
[SIGACTION(2) FreeBSD Manual Pages]

Ecco, questa raccomandazione cade a fagiolo, e ci indica che un signal handler DEVE essere semplice, perché svolge un ruolo molto delicato e dobbiamo essere sicuri che faccia almeno il suo compito principale; ogni prestazione aggiuntiva potrebbe rovinarne il funzionamento. Quindi, io oggi vi darò alcune dritte (spero interessanti) ma sono da applicare "cum grano salis"...

(...ah, come dite? Ma quello sopra è tratto da un manuale di FreeBSD, non di Linux... Embè? FreeBSD è praticamente uno UNIX, quindi famiglia POSIX, quindi parente strettissimo di Linux (più padre che fratello, direi...), per cui (quasi) tutto ciò che è valido per FreeBSD lo è anche per Linux, non vi preoccupate...)

Ok, e come si possono affrontare i due punti descritti all'inizio dell'articolo? Cominciamo col punto 1:

Punto 1: la connessione

Risolvere il problema della perdita della connessione col socket collegato a /dev/log è abbastanza semplice: invece di effettuare questa connessione una volta per tutte nel main del programma (salvando il socket nella apposita variabile volatile sig_atomic_t) possiamo effettuare la connessione ogni volta che il socket handler intercetta un segnale, così saremo a prova di disconnessione. Certo, dovremo rivedere un po' la funzione connectSyslog() dell'ultimo articolo, pulendola di tutto ciò che non è async-signal-safe. Si può fare! Ma attenzione: questo appesantisce un po' il signal handler ! (vedi la raccomandazione fatta sopra...). E vediamola 'sta funzione! vai col codice!

// connectSyslog - collego il socket del syslog (/dev/log) in modo async-signal-safe
static int connectSyslog()
{
// preparo la struct sockaddr_un per il collegamento via Unix Domain Socket
struct sockaddr_un reader;
memset(&reader, 0, sizeof(reader));
reader.sun_family = AF_UNIX;
strcpy(reader.sun_path, "/dev/log");

// provo a creare il socket in modo IPC Stream
int log_sock;
if ((log_sock = socket(AF_UNIX, SOCK_STREAM, 0)) != -1) {
// connect to the syslog socket
if (connect(log_sock, (struct sockaddr *)&reader, sizeof(reader)) != -1) {
// successo della connect(2)
return log_sock;
} else {
// errore connect(2)
close(log_sock);
}
}

// errore IPC Stream: provo a creare il socket in modo IPC Datagram
if ((log_sock = socket(AF_UNIX, SOCK_DGRAM, 0)) != -1) {
// connect to the syslog socket
if (connect(log_sock, (struct sockaddr *)&reader, sizeof(reader)) != -1) {
// successo della connect(2)
return log_sock;
} else {
// errore connect(2)
close(log_sock);
}
}

// connessione non effettuata
return -1;
}

Beh, questa era semplice: come avrete visto è la connectSyslog() dell'ultimo articolo con qualche piccola modifica: niente scrittura degli eventuali errori e una snprintf(3) sostituita da una strcpy(3) che è async-signal-safe. Fatto! Possiamo passare al punto 2.

Punto 2: La stringa dinamica

Certo, senza usare la snprintf(3) è un po' complicato creare in maniera molto flessibile delle stringhe di log  con dati dinamici, ma se è sufficiente costruire una stringa con dati fissi e aggiungere solo qualche numero, anche in questo caso Si può fare! Bisogna aggiungere una funzione di costruzione (che ho chiamato builLogStr()) e una per trasformare numeri in stringhe (che ho chiamato myItoa(), visto che è, effettivamente, una itoa()). Entrambe queste funzioni devono essere il più semplici possibili e non usare nulla che non sia async-signal-safe. E vediamo La buildLogStr()! Vai col codice!

// buildLogStr - costruisco la log string in modo async-signal-safe
static char *buildLogStr(char *dest, int signum)
{
// prendo il numero PID e lo converto in una stringa
pid_t pid = getpid(); // getpid(2) è async-signal-safe
char str_pid[16];
myItoa(pid, str_pid);

/* costruisco la stringa
(e.g.: "[1428] testsighandler: ricevuto un SIGSEGV. Esco con errore.") */
char *last = stpcpy(dest, "<11>[");
last = stpcpy(last, str_pid); // stpcpy(3) è async-signal-safe
last = stpcpy(last, "] testsighandler: ");

switch (signum) {
case SIGSEGV:
stpcpy(last, "ricevuto un SIGSEGV. Esco con errore.");
break;

case SIGINT:
stpcpy(last, "ricevuto un SIGINT. Esco con errore.");
break;

case SIGBUS:
stpcpy(last, "ricevuto un SIGBUS. Esco con errore.");
break;

case SIGABRT:
stpcpy(last, "ricevuto un SIGABRT. Esco con errore.");
break;

case SIGTERM:
stpcpy(last, "ricevuto un SIGTERM. Esco correttamente.");
break;

default:
break;
}

return dest;
}

Che ne dite? La buildLogStr(), che è, come al solito, stra-commentata, costruisce per passi una stringa usando solo funzioni async-signal-safe. Il punto di riferimento per la costruzione è il numero del segnale, che guida il risultato finale. L'obiettivo è creare una stringa di log del tipo indicato nei commenti, e cioè qualcosa come questo:

[numero PID] <nome applicazione>: <testo>

ossia, per esempio, questo:

[1428] testsighandler: ricevuto un SIGSEGV. Esco con errore.

E, come avrete sicuramente notato, giocando opportunamente con uno switch è un compito relativamente semplice.

E, a questo punto, l'unica cosa che manca è il numero PID  come stringa, ma questo ce lo fornisce la myItoa(): per questa funzione mi sono rifatto al bellissimo esempio contenuto nella Bibbia del C, il K&R "The C Programming Language (2nd ed.)". È una itoa() che usa esclusivamente istruzioni di basso livello più una sola funzione async-signal-safe, la strlen(3). E vediamo anche questa! vai col codice!

/* myItoa - una itoa() async-signal-safe
(ispirata dal K&R "The C Programming Language (2nd ed.)") */
static void myItoa(int n, char *s)
{
// check del segno
int sign;
if ((sign = n) < 0) // registro il segno
n = -n; // lo rendo positivo

// genero i digit in ordine inverso
int i = 0;
do {
*(s + i++) = n % 10 + '0'; // prendo il prossimo digit
} while ((n /= 10) > 0); // lo cancello

// re-check del segno e termino la stringa
if (sign < 0)
*(s + i++) = '-';

*(s + i) = '\0';

// inverto il risultato
int j;
for (i = 0, j = strlen(s) - 1; i < j; i++, j--) { // strlen(3) è async-signal-safe
int c = *(s + i);
*(s + i) = *(s + j);
*(s + j) = c;
}
}

Per completezza, e per concludere, vi mostro anche il nuovo main del nuovo testsighandler:

#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <signal.h>
#include <sys/socket.h>
#include <sys/un.h>

// prototipi locali
static void sigHandler(int signum);
static void sigAction();
static int connectSyslog();
static char *buildLogStr(char *dest, int signum);
static void myItoa(int n, char *s);

/* 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[])
{
// prepara il signal handler
sigAction();

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

// il loop pseudo-infinito è stato interrotto da un SIGTERM
printf("programma terminato correttamente\n");
return 0;
}

Insomma, il signal handler plus è fatto! Certo che abbiamo dovuto aggiungere alla versione originale ben tre funzioni: connectSyslog(), buildLogStr() e myItoa() il che appesantisce notevolmente il disegno iniziale e non rispetta molto la raccomandazione fatta sopra (quella del manuale FreeBSD). E, tanto per confondervi ulteriormente le idee, vi aggiungo: anche una funzione che usa internamente solo funzioni async-signal-safe non è garantito al 100% che sia veramente safe, perché se "fa troppe cose"  potrebbe non essere veramente interrompibile a piacere (vedi di nuovo la raccomandazione iniziale). Comunque, se è proprio indispensabile fare un signal handler  "sofisticato" il metodo che vi ho proposto direi che è un ottimo compromesso. Usatelo, se vi serve, ma occhio alle controindicazioni!

Credo che possiamo considerare concluso (almeno per un po' di tempo...) l'argomento signal handler: dopo ben tre articoli non ho molto altro da aggiungere. E ora cosa ci riserva il futuro? Boh... ci sto già pensando: non so esattamente cosa sarà ma vi assicuro che sarà interessante!

Ciao, e al prossimo post!

venerdì 27 giugno 2025

Signal Handler
come scrivere un signal handler in C - pt.2

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 within
a 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:

  1. Ho scritto una funzione sigHandler() che scrive sul socket  di /dev/log tramite write(2) che è una system-call async-signal-safe.
  2. Ho scritto una funzione sigAction() che prepara le attività del signal handler in maniera un po' più sofisticata rispetto al vecchio esempio.
  3. 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."

// prototipi locali
static 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/implementazione
in 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 main
int main(int argc, char* argv[])
{
// prepara il signal handler
log_sock = connectSyslog();
sigAction();

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

// il loop pseudo-infinito è stato interrotto da un SIGTERM
printf("programma terminato correttamente\n");
return 0;
}

// sigHandler - la funzione del signal handler
static 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 forzata
write(log_sock, SIGINT_MSG1, sizeof(SIGINT_MSG1));
_exit(signum);

case SIGTERM:
// set del flag "resta_attivo" per forzare l'uscita controllata del main loop
write(log_sock, SIGINT_MSG2, sizeof(SIGINT_MSG2));
resta_attivo = 0;
break;

default:
break;
}

// ripristino l'errno
errno = my_errno;
}

// sigAction - set delle azioni sui segnali da catturare
static 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 handler
sa.sa_flags = SA_RESTART;

// maschera tutti i segnali
if (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 SIGABRT
if (sigaction(SIGSEGV, &sa, NULL) == -1)
fprintf(stderr, "%s: sigaction error for SIGSEGV: %s\n", __func__, strerror(errno));

if (sigaction(SIGINT, &sa, NULL) == -1)
fprintf(stderr, "%s: sigaction error for SIGINT: %s\n", __func__, strerror(errno));

if (sigaction(SIGBUS, &sa, NULL) == -1)
fprintf(stderr, "%s: sigaction error for SIGBUS: %s\n", __func__, strerror(errno));

if (sigaction(SIGABRT, &sa, NULL) == -1)
fprintf(stderr, "%s: sigaction error for SIGABRT: %s\n", __func__, strerror(errno));

// set azione per il segnale controllato SIGTERM
if (sigaction(SIGTERM, &sa, NULL) == -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 Socket
struct 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 Stream
int log_sock;
if ((log_sock = socket(AF_UNIX, SOCK_STREAM, 0)) != -1) {
// connect to the syslog socket
if (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 Datagram
if ((log_sock = socket(AF_UNIX, SOCK_DGRAM, 0)) != -1) {
// connect to the syslog socket
if (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 effettuata
return -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 64872
aldo@Linux $

la applicazione scriverà sul terminale :

aldo@Linux $ ./testsighandler
programma terminato correttamente
aldo@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 65071
aldo@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:

  1. 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.
  2. 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!

lunedì 26 maggio 2025

Il buono, il brutto, il VLA
come usare i Variable Length Array in C - pt.3

Tuco (il brutto): Il mondo è diviso in due, amico mio: quelli che hanno la corda al collo e quelli che la tagliano. Solo che il collo dentro la corda è il mio, sono io che rischio, perciò la prossima volta voglio più della metà.
Biondo (il buono): Sì, è vero che tu rischi. Ma io taglio e se tu mi abbassi la percentuale... sigaro? ... potrei sbagliare la mira.

(...una premessa: questo post è un remake di un mio vecchio post (parte 3 di 3). Ma, anche se tratta lo stesso argomento, amplia e perfeziona un po' il discorso è mi è sembrato il caso di riproporlo. Leggete e mi direte...)

Dunque, dove eravamo rimasti? Ah si, nel primo articolo della serie (che avete appena riletto, vero?), avevamo approvato, con riserva, i Variable Length Array (VLA per gli amici), che sono facili da usare, utili e con ottime prestazioni. Poi, nel secondo articolo (anche quello è da rileggere, eh!), avevamo, ahimè, confermato la riserva assegnandogli il ruolo del cattivo del mitico Il buono, il brutto, il cattivo del grande Sergio Leone, e questo perché i contro dei VLA superavano ampliamene i pro. Ed ora eccoci di nuovo sul pezzo e, come promesso, oggi parleremo di un parente stretto dei VLA, ovvero della funzione alloca(3)... sarà un buono, un brutto o un cattivo?

...ciao, sono lo spoiler di questo post!...

E allora: ho aggiunto del codice al programma di test (visto nei precedenti articoli) per provare la alloca(3). E, per non farci mancare niente, ho aggiunto anche del codice per provare la versione C++ della malloc(3), ovvero la new (ebbene si: dopo il problematico test di std::vector dello scorso post era doveroso parlare anche di qualcosa più prestante, mica si dica che ce l'ho con il C++...). Quindi useremo il programma C++ già proposto (tanto era praticamente identico alla versione C) di cui vi riporterò solamente il main() e le due funzioni di test aggiunte (e, per ricostruire il programma completo, basta consultare i due post precedenti e fare un po' di copia-e-incolla). Vai col codice!

#include <stdio.h>
#include <time.h>
#include <stdlib.h>
#include <vector>

#define MYSIZE 1000000

// variabile dummy per evitare lo svuotamento totale delle funzioni usando g++ -O2
int avoid_optimization;

// prototipi locali
void runTest(int iterations, void (*funcptr)(int), int size, const char *name);
void testVLA(int size);
void testMallocVLA(int size);
void testStackFLA(int dum);
void testHeapFLA(int dum);
void testAllocaVLA(int size);
void testVectorVLA(int size);
void testNewVLA(int size);

// funzione main()
int main(int argc, char* argv[])
{
// test argomenti
if (argc != 2) {
// errore: conteggio argomenti errato
printf("%s: wrong arguments counts\n", argv[0]);
printf("usage: %s vla iterations [e.g.: %s 10000]\n", argv[0], argv[0]);
return EXIT_FAILURE;
}

// estrae iterazioni
int iterations = atoi(argv[1]);

// esegue test
runTest(iterations, &testVLA, MYSIZE, "testVLA");
runTest(iterations, &testMallocVLA, MYSIZE, "testMallocVLA");
runTest(iterations, &testStackFLA, 0, "testStackFLA");
runTest(iterations, &testHeapFLA, 0, "testHeapFLA");
runTest(iterations, &testAllocaVLA, MYSIZE, "testAllocaVLA");
runTest(iterations, &testVectorVLA, MYSIZE, "testVectorVLA");
runTest(iterations, &testNewVLA, MYSIZE, "testNewVLA");

// esce
return EXIT_SUCCESS;
}

// testAllocaVLA() - funzione per eseguire il test della alloca VLA
void testAllocaVLA(
int size) // size per alloca()
{
int *allocavla = (int*)alloca(size * sizeof(int));

// loop di test
for (int i = 0; i < size; i++)
allocavla[i] = i;

// istruzione per evitare lo svuotamento totale della funzione usando g++ -O2
avoid_optimization = allocavla[size / 2];
}

// testNewVLA() - funzione per eseguire il test della new VLA
void testNewVLA(
int size) // size per new
{
int *newvla = new int[size];

// loop di test
for (int i = 0; i < size; i++)
newvla[i] = i;

// istruzione per evitare lo svuotamento totale della funzione usando g++ -O2
avoid_optimization = newvla[size / 2];

delete[] newvla;
}

Come potete vedere, le due funzioni aggiunte sono perfettamente allineate stilisticamente con le altre che avevo già proposto e sono, come sempre, iper-commentate, così non c'è neanche bisogno di dilungarmi in spiegazioni. E i risultati del test? Vediamoli!

aldo@Linux $ g++ -O0 vlacpp.cpp -o vlacpp
aldo@Linux $ ./vlacpp 2000
testVLA - Tempo trascorso: 3.908325 secondi
testMallocVLA - Tempo trascorso: 2.724537 secondi
testStackFLA - Tempo trascorso: 3.631790 secondi
testHeapFLA - Tempo trascorso: 3.623486 secondi
testAllocaVLA - Tempo trascorso: 2.751826 secondi
testVectorVLA - Tempo trascorso: 8.930886 secondi
testNewVLA - Tempo trascorso: 2.752857 secondi

aldo@Linux $ g++ -O2 vlacpp.cpp -o vlacpp
aldo@Linux $ ./vlacpp 2000
testVLA - Tempo trascorso: 0.633613 secondi
testMallocVLA - Tempo trascorso: 0.627741 secondi
testStackFLA - Tempo trascorso: 0.267571 secondi
testHeapFLA - Tempo trascorso: 0.263820 secondi
testAllocaVLA - Tempo trascorso: 0.623920 secondi
testVectorVLA - Tempo trascorso: 0.773795 secondi
testNewVLA - Tempo trascorso: 0.613130 secondi

Allora, cosa si può dire? I risultati dei test dei post precedenti li abbiamo già ampiamente commentati, quindi ora possiamo solo aggiungere questo: alloca(3) è molto veloce, visto che è, in pratica, una malloc(3) nello stack (e, usandola in maniera appropriata, potrebbe/dovrebbe essere la più veloce del gruppo). E la new? Beh, si comporta (come previsto) benissimo, anche perché, spesso, la new usa internamente la malloc(3).

E va bene: la alloca(3) è veloce, ma lo è (solo un po' meno) anche un VLA, e questo non lo ha salvato dal essere eletto come cattivo nello scorso articolo Quindi ci toccherà fare di nuovo una lista di pro e contro, e vedere quale parte pesa di più. Vediamo prima i pro:

  1. La alloca(3) è molto veloce, già che usa lo stack invece del heap.
  2. È anche facile da usare, visto che è, praticamente, una malloc(3) senza free(3). La variabile allocata ha uno scope a livello di funzione, quindi rimane valida fino a quando la funzione ritorna al chiamante, esattamente come una qualsiasi variabile automatica locale (anche un VLA funziona più o meno così, ma il suo scope è a livello di blocco, non di funzione, e questo è, probabilmente, un punto a favore dei VLA).
  3. Per il motivo visto al punto 2 la alloca(3) non lascia in giro residui di memoria in caso di errori gravi nella attività di una funzione (e con malloc + free non è altrettanto facile realizzare questo). Se poi siete soliti a usare cosucce come longjmp(3) i vantaggi in questo senso sono grandissimi.
  4. A causa della sua implementazione interna (senza entrare in dettagli profondi) la alloca(3) non causa frammentazione della memoria.

Uh, che bello! E i contro?

  1. La gestione degli errori è problematica, perché non c'è maniera di sapere se alloca(3) ha allocato bene o ha provocato un stack overflow (in questo caso provoca effetti simili a quelli di un errore per ricorsione infinita)... uh, questo è esattamente lo stesso problema dei VLA.
  2. Non è molto portabile, visto che non è una funzione standard e il suo funzionamento e la sua presenza dipendono molto dal compilatore in uso.
  3. È error prone per almeno tre motivi:
    • Bisogna usarla con attenzione, visto che induce, tipicamente, a errori come usare la variabile allocata quando oramai non è più valida (ritornarla o inserirla dentro una struttura dati esterna alla funzione, per esempio)... ma noi siamo ottimi programmatori e questo punto non ci spaventa, no?
    • Ci sono problemi ancora più sottili da considerare nell'uso, ad esempio può risultare MOLTO pericoloso mettere una alloca(3) dentro un loop o in una funzione ricorsiva  (povero stack!) oppure in una funzione inline (visto che una inline usa lo stack in una maniera che si scontra un po' con la maniera di usare lo stack della alloca(3))... ma noi siamo ottimi programmatori e questo punto non ci spaventa, no?
    • Usa lo stack, che è normalmente limitato rispetto allo heap (specialmente nella programmazione embedded che è molto frequentata dai programmatori C...). Quindi esaurire lo stack e provocare uno stack overflow è facile (e difficile da controllare, vedi il punto 1)... ma noi siamo ottimi programmatori e questo punto non ci spaventa, no?

E vabbè, conclusioni? Ci sarebbero gli estremi per dichiarare la alloca(3) come un altro cattivo (la stessa sorte dei VLA) ma, dati i notevoli pro e, soprattutto, dato che oggi sono di buon umore, la dichiareremo solo come brutto (visto lo spoiler nella figura iniziale?). Comunque, mi raccomando: usate la alloca(3) con molta cautelauomo avvisato mezzo salvato!

Ciao e al prossimo post!