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.

lunedì 29 agosto 2022

Thread Cancel? No, grazie!
considerazioni sul perché non usare la pthread_cancel(3) in C (e C++) - pt.1

Dutch: Non toccare quell'arma, non ti ha ucciso perché non eri armata...è uno sportivo.

Ok, ci risiamo: ho ottenuto uno spunto da fatti reali (cose di lavoro) e ho ridetto "Ma qui c'è materiale per un articolo!", per cui oggi parleremo di pthread_cancel(3). Uhm, e questa volta c'è un film collegato? No, anche questo articolo appartiene alla serie dei serie dei “No Grazie!” (ah: ne ho scritti altri e vi invito a leggerli o rileggerli, qui, quiqui, qui e qui) e non farò giochi di parole con il titolo, e ne sceglierò uno a caso tra quelli che ho visto/rivisto ultimamente... si, Predator, va benissimo: è un gran film action di John McTiernan, un autore che ha smesso prematuramente di regalarci grandi opere per colpa di alcune "birichinate" commesse nella sua vita privata... peccato, era un grande. E se proprio vogliamo forzare una attinenza film/articolo possiamo dire: pthread_cancel(3) è una di quelle cose che funzionano ma è meglio starne alla larga, è pericolosa come cercare di cancellare un Predator...

...giuro che non userò mai più la pthread_cancel(3). Te lo giuro...

Non so se ricordate, ma nell'articolo System? No, grazie! ho evidenziato i mille problemi dell'uso della system(3), ma, a parziale scusante (per gli impavidi che la usano) ho anche detto: "system(3), è una funzione della libc usata e abusata: è molto comoda e semplice da usare...". Ecco, l'argomento di questo articolo, la pthread_cancel(3), non ha neanche questa scusante: se proprio la si vuole usare bisogna usarla bene, e usarla bene è complicatissimo!

Ok, è ora di entrare nel dettaglio. Cosa è la pthread_cancel(3) e cosa permette di fare? Il manuale della glibc titola semplicemente questo:

pthread_cancel - send a cancellation request to a thread

Ecco, a quanto pare permette inviare una richiesta di cancellazione a un thread (un po' come inviare un sigkill o un sigterm a un processo): sembra facile ma non lo è! E perché non è facile? Perché quando inviamo la richiesta non sappiamo cosa sta facendo il thread: potrebbe essere, ad esempio, in una zona critica eseguendo una attività che, se interrotta bruscamente, potrebbe corrompere la memoria o lasciare il multitreading in uno stato indefinito, ecc. ecc. Insomma: un giochetto da niente. E uno potrebbe dire "Ma sicuramente se ne occupa la libpthread di chiudere bene... si, si, sicuro: la libreria è dotata di una AI che decide cosa è meglio fare per qualunque codice scritto, nel passato, nel presente e nel futuro, non ti preoccupare..."

Ok, scendiamo con i piedi per terra: la povera libpthread non può occuparsi di chiudere bene un thread che può contenere qualsiasi cosa: per farlo dovrebbe fornire degli strumenti specifici, mentre, essendo una (gran) libreria generica, può solo fornire degli strumenti generici per farlo, e il programmatore deve usarli adeguatamente in base al codice che sta scrivendo. E quindi consiglio caldamente di leggere accuratamente il manuale della pthread_cancel(3) prima di usarla (e, magari, la lettura del manuale vi toglierà ogni residua voglia di usarla). E, già che ci sono, vi evidenzio i 2 punti chiave, estratti direttamente dal manuale:

1. tipo di cancellazione (supponendo che il "cancelability state" del thread sia "cancellabile"):

A thread s cancellation type, determined by
pthread_setcanceltype(3), may be either asynchronous or deferred
(the default for new threads). Asynchronous cancelability means
that the thread can be canceled at any time (usually immediately,
but the system does not guarantee this). Deferred cancelability
means that cancellation will be delayed until the thread next
calls a function that is a cancellation point. A list of
functions that are or may be cancellation points is provided in
pthreads(7).

2. esecuzione della cancellation request:

1. Cancellation clean-up handlers are popped (in the reverse of
the order in which they were pushed) and called. (See
pthread_cleanup_push(3).)
2. Thread-specific data destructors are called, in an unspecified
order. (See pthread_key_create(3).)
3. The thread is terminated. (See pthread_exit(3).)

È evidente che bisogna avere ben chiare quali sono le zone critiche del thread, dove sono i cancellation-point, e altre cosucce... E vabbè, ma se proprio la voglio usare che faccio? Ok, è il momento dei consigli:

  1. In creazione del thread si dovrebbe usare: pthread_setcancelstate(3), pthread_setcanceltype(3) e pthread_create(3) (e magari anche pthread_key_create(3)).
  2. Durante la vita utile del thread usare: pthread_cleanup_push(3) e pthread_cleanup_pop(3).
  3. Se in creazione abbiamo usato pthread_key_create(3), durante la vita utile del thread si dovrebbe anche usare: pthread_getspecific(3) e pthread_setspecific(3).
  4. In cancellazione del thread usare: pthread_cancel(3) e pthread_join(3).
  5. E se dopo aver letto i punti da 1 a 4 volete ancora usare la pthread_cancel(3) prendetevi un momento di riflessione, fatevi una buona dormita, e al risveglio, magari, avrete le idee più chiare...

il quadro esposto sopra è quello minimale, e si può complicare a piacere, ad esempio mischiando sapientemente i punti 2 e 3. Ripeto però: è il quadro minimale, che prevede una buona inizializzazione (punto 1), un buon trattamento delle zone critiche (punti 2 o 3 o anche punti 2 e 3), e una buona gestione della chiusura dei thread (punto 4). Se si gioca al risparmio e si omette qualche passo il disastro è dietro l'angolo. E, se ancora non è chiaro, rispettare queste norme è abbastanza difficile, specialmente se i thread eseguono attività complesse. Ok, nessun problema, seguiamo le istruzioni et voilà!, codice perfetto! Ma...

MA, SPESSO, I PROGRAMMATORI SONO PIGRI

Ebbene si, ho visto molto codice scritto pigramente, e ve ne fornisco un esempio (molto semplificato, ovviamente). Vai col codice!

// threaderrcancel.c -esempio di cancel thread erronea
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

// prototipi globali
static void* myThread(void *arg);
static void mySleep(unsigned int milliseconds);

// main() - funzione main()
int main(int argc, char* argv[])
{
// crea il thread
pthread_t tid;
pthread_create(&tid, NULL, &myThread, NULL);

// aspetto 2 secondi e poi cancello il thread
mySleep(2000);
pthread_cancel(tid);

// join del thread
pthread_join(tid, NULL);

// esco
printf("%s: esco\n", argv[0]);
return 0;
}

// myThread() - thread routine
void* myThread(void *arg)
{
// loop del thread
printf("thread partito\n");
for (;;) {
// il thread fa cose...
// ...

// malloc sul buffer
char *p = (char *)malloc(100);

// simulo un ritardo perchè il thread fa altre cose...
mySleep(2);

// free del buffer
free(p);

// sleep del thread (10 ms)
mySleep(10);
}

// il thread esce
printf("thread finito\n");
pthread_exit(NULL);
}

// mySleep() - wrapper per nanosleep()
static void mySleep(unsigned int milliseconds)
{
struct timespec ts;
ts.tv_sec = milliseconds / 1000;
ts.tv_nsec = (milliseconds % 1000) * 1000000;
nanosleep(&ts, NULL);
}

Ecco, questo è un programma multithread che usa la pthread_cancel() in modo pigro, "alla speraindio"  (nota a parte: ho usato anche il mio vecchio wrapper per nanosleep(): direi che è l'unica parte buona di questo programma). Se compilate ed eseguite con l'ottimo analizzatore dinamico Valgrind (per trovare eventuali memory leak) otterrete (quasi sempre) questo risultato estratto dal logfile del Valgrind:

==10064== HEAP SUMMARY:
==10064== in use at exit: 100 bytes in 1 blocks
==10064== total heap usage: 166 allocs, 165 frees, 18,906 bytes allocated
==10064== 100 bytes in 1 blocks are definitely lost in loss record 1 of 1
==10064== at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==10064== by 0x10930A: myThread (in /home/aldo/Blog/myblog/post2022/post119-xx082022/threaderrcancel)
==10064== by 0x486A608: start_thread (pthread_create.c:477)
==10064== by 0x49A4132: clone (clone.S:95)
==10064== LEAK SUMMARY:
==10064== definitely lost: 100 bytes in 1 blocks

Cosa è successo? È successo che la brusca interruzione del thread non gli ha dato tempo di eseguire la free(3). Con un po' di fortuna la cancellazione potrebbe arrivare dopo l'esecuzione della free(3), ma vi sembra serio scrivere un programma basato sulla fortuna? Ok, vediamo allora una versione abbastanza migliorata del programma precedente, con una funzione di cleanup per liberare la memoria in qualunque momento arrivi la cancellazione. Vai col codice!

// threadcancel.c -esempio di cancel thread
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

// prototipi globali
static void* myThread(void *arg);
static void cleanupHandler(void *arg);
static void mySleep(unsigned int milliseconds);

// main() - funzione main()
int main(int argc, char* argv[])
{
// crea il thread
pthread_t tid;
pthread_create(&tid, NULL, &myThread, NULL);

// aspetto 2 secondi e poi cancello il thread
mySleep(2000);
pthread_cancel(tid);

// join del thread
pthread_join(tid, NULL);

// esco
printf("%s: esco\n", argv[0]);
return 0;
}

// myThread() - thread routine
void* myThread(void *arg)
{
// per usi particolari posso chiamare (nei punti opportuni) anche:
//pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); // default
//pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
//pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL); // default
//pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL);

// loop del thread
printf("thread partito\n");
for (;;) {
// il thread fa cose...
// ...

// malloc sul buffer e pop cleanup di cancellazione
char *p = (char *)malloc(100);
pthread_cleanup_push(cleanupHandler, p);

// simulo un ritardo perchè il thread fa altre cose...
mySleep(2);

// free del buffer e pop cleanup di cancellazione
free(p);
pthread_cleanup_pop(0);

// sleep del thread (10 ms)
mySleep(10);
}

// il thread esce
printf("thread finito\n");
pthread_exit(NULL);
}

// cleanupHandler() - funzione di cleanup di cancellazione
static void cleanupHandler(void *arg)
{
printf("Called clean-up handler\n");
free(arg);
}

// mySleep() - wrapper per nanosleep()
static void mySleep(unsigned int milliseconds)
{
struct timespec ts;
ts.tv_sec = milliseconds / 1000;
ts.tv_nsec = (milliseconds % 1000) * 1000000;
nanosleep(&ts, NULL);
}

Ecco, questo va abbastanza meglio, e il Valgrind ci mostra (sempre) questo:

==10086== HEAP SUMMARY:
==10086== in use at exit: 0 bytes in 0 blocks
==10086== total heap usage: 168 allocs, 168 frees, 19,106 bytes allocated
==10086== All heap blocks were freed -- no leaks are possible

Ok, di nuovo sembra facile... ma provate a scrivere varie funzioni di cleanup (mantenendole con pthread_cleanup_push(3) e pthread_cleanup_pop(3)) per un thread che esegue molte attività complesse e che chiama funzioni esterne che chiamano altre funzioni esterne che chiamano... Insomma, è molto complicato, e lo sarebbe ancora di più usando la gestione delle thread key con pthread_key_create(3) (gestione che, tra l'altro, è la più indicata per liberare la memoria, visto che, in realtà, le funzioni pthread di push e pop sono più adatte per gestire il reset di stati, lock, ecc.).

E quale è la conclusione allora? Semplicissimo; pthread_cancel(3) non si usa! Perché è difficilissimo usarla bene e, anche mettendocela tutta, potrebbe sfuggirci qualche dettaglio che può causare dei veri disastri a run-time. La libpthread fornisce questa funzione perché è una libreria molto completa che cerca di coprire tutti gli aspetti del multithreading, ma la fornisce sottintendendo "Usatela a vostro rischio e pericolo...".

E concludo con una domanda: "Ma secondo voi, perché i nuovi supporti built-in ai thread in C11 (threads) e C++11 (std::thread), pur essendo spesso basati sulla onnipresente libpthread (che da sotto esegue in maniera trasparente il lavoro sporco) non implementano una funzione di cancel? Sarà mica perché fa solo danni?". E con questo spunto di riflessione vi saluto e vi aspetto per la seconda parte dell'articolo, dove vedremo la maniera migliore di fermare un thread (spoiler: è molto più semplice di quello che ci si può aspettare!).

Ciao, e al prossimo post! 

lunedì 11 luglio 2022

ZeroMQ Festival
come si usa ZeroMQ in C - pt.2

Sue: Hai sentito che il film di Philippe ha vinto il primo premio al Festival di Colonia, oggi?.
Mort: Molto eccitante. Ma Eichmann non era di lì?

E va bene, questa volta ci siamo, è il turno della seconda parte di Rifkin's Festival (oops: ZeroMQ Festival), come promesso nella prima parte dell'articolo. Qui sopra vi ho proposto un commento secco e lapidario di Mort (Wallace Shawn), il protagonista alter-ego di Woody Allen in questo bel film, un commento che ben introduce questo articolo: dopo aver parlato dell'interfaccia a ZeroMQ di "basso livello", e cioè la libreria libzmq, è il momento di parlare dell'interfaccia ad "alto livello", la CZMQ (libczmq). Grazie a CZMQ è possibile scrivere codice di rete sintetico e compatto (anzi, secco e lapidario) anche per problemi complessi, problemi che richiederebbero la scrittura di codice lungo e complicato anche aiutandosi con ZeroMQ low-level (e lunghissimo e complicatissimo usando, ad esempio, i classici BSD Socket).

...crepi l'avarizia, oggi ti offrirò un menu high-level...

Allora: CZMQ è una interfaccia C ma è strutturata come se fosse una libreria di classi (nel manuale il termine "class" viene usato frequentemente). Comunque niente paura per chi non è avvezzo al C++: è una interfaccia C al 100%. E questa è la descrizione che danno gli autori stessi della libreria nel manuale:

CZMQ ha questi obiettivi:

  • "wrappare" l'API core di ØMQ in una semantica che sia naturale e porti ad applicazioni più brevi e leggibili.
  • Nascondere le differenze tra le versioni di ØMQ.
  • Fornire uno spazio per lo sviluppo di semantiche API più sofisticate.

E quali sono, in pratica, i vantaggi dell'uso di CZMQ? Facciamo un caso reale: se, per esempio, volessimo risolvere un problema complicato come scrivere un Load Balancing Broker  che segua lo schema di funzionamento seguente (copio una descrizione di questo oggetto da 0MQ - The Guide):

lbbroker

grazie a CZMQ potremmo farlo in maniera abbastanza sintetica e leggibile. Mentre se provassimo a farlo con altri strumenti ci renderemmo subito conto della grande differenza a livello di lunghezza e complicazione. Per quanto riguarda la realizzazione pratica di questo LBB (visto che non amo fare il copia-incolla di cose non mie) vi passo direttamente due link dove viene descritto il codice corrispondente, un doppio codice scritto usando le due librerie (low e high-level): se gli date un occhiata noterete che con CZMQ è decisamente più semplificato e leggibile. Ah, e vi consiglio un utile esercizio: provate a riscrivere (o, perlomeno, a immaginare) lo stesso codice scritto con i BSD Socket... tremo solo a pensarci. Ecco i link: versione con lzmq e versione con CZMQ.

E adesso? Ma non posso chiudere un articolo senza proporre nessun codice mio! Quindi ho pensato che per ben descrivere la differenza di approccio tra low-level e high-level è meglio partire dalle basi, e allora vi propongo un esempio semplicissimo di codice Client/Server scritto in tre versioni, BSD Socket, ZeroMQ low-level e CZMQ.

Ho scritto tutti gli esempi a mo' di codice di test (e non di produzione), quindi sono (ahimè) completamente assenti le importantissime istruzioni di trattamento degli errori (praticamente sono gli scheletri funzionali da completare per entrare in produzione): questo con l'obiettivo di non sviare l'attenzione dal flusso base del codice, quello che ci mostra come si lavora con una libreria rispetto a un'altra. Tutti gli esempi sono composti da due file, client.c e server.c, e sono perfettamente compilabili e funzionanti. Ok, cominciamo con i BSD Socket, vai col codice!

// client.c - un semplice client con BSD Socket
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

// main() - funzione main
int main(void)
{
// creo il socket
printf ("Connessione al server...\n");
int cli_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

// preparo la struttura sockaddr_in per il server remoto
struct sockaddr_in server;
memset(&server, 0, sizeof(struct sockaddr_in));
server.sin_family = AF_INET; // set famiglia di indirizzi
server.sin_addr.s_addr = inet_addr("127.0.0.1");// set indirizzo del server
server.sin_port = htons(5555); // set port del server

// connetto il socket al server remoto
connect(cli_sock, (struct sockaddr *)&server, sizeof(server));

// loop di invio
int cnt = 0;
while (cnt++ < 10) {
// invio la richiesta
send(cli_sock, "Ping", 5, 0);

// ricevo la risposta
char string[10];
recv(cli_sock, string, sizeof(string), 0);
printf("risposta ricevuta: %s\n", string);
}

// disconnetto
close(cli_sock);
return 0;
}
// server.c - un semplice server con BSD Socket
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

// main() - funzione main
int main(void)
{
// creo il socket TCP
printf ("Avvio server...\n");
int srv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

// preparo la struttura sockaddr_in per questo server
struct sockaddr_in server;
memset(&server, 0, sizeof(struct sockaddr_in));
server.sin_family = AF_INET; // set famiglia di indirizzi
server.sin_addr.s_addr = INADDR_ANY; // set indirizzo del server
server.sin_port = htons(5555); // set port del server

// associo l'indirizzo del server al socket e start ascolto
bind(srv_sock, (struct sockaddr *)&server, sizeof(server));
listen(srv_sock, 10);

// accetto connessioni da un client entrante
socklen_t socksize = sizeof(struct sockaddr_in);
struct sockaddr_in client; // struttura sockaddr_in per il client remoto
int cli_sock = accept(srv_sock, (struct sockaddr *)&client, &socksize);
close(srv_sock);

// loop di ricezione
char string[10];
while (recv(cli_sock, string, sizeof(string), 0) != -1) {
printf("richiesta ricevuta: %s\n", string);

// invio la risposta
send(cli_sock, "Pong", 5, 0);
}

// disconnetto
close(cli_sock);
return 0;
}

E passiamo ora al codice in versione libzmq (ZeroMQ low-level):

// client.c - un semplice client ZeroMQ con libzmq
#include <stdio.h>
#include <zmq.h>

// main() - funzione main
int main(void)
{
// creo il context e il requester
printf ("Connessione al server...\n");
void *context = zmq_ctx_new();
void *requester = zmq_socket(context, ZMQ_REQ);

// connetto il requester al responder
zmq_connect(requester, "tcp://localhost:5555");

// loop di invio
int cnt = 0;
while (cnt++ < 10) {
// invio la richiesta
zmq_send(requester, "Ping", 5, 0);

// ricevo la risposta
char string[10];
zmq_recv(requester, string, sizeof(string), 0);
printf("risposta ricevuta: %s\n", string);
}

// disconnetto
zmq_close (requester);
zmq_ctx_destroy (context);
return 0;
}
// server.c - un semplice server ZeroMQ con libzmq
#include <stdio.h>
#include <zmq.h>

// main() - funzione main
int main(void)
{
// creo il context e il responder
printf ("Avvio server...\n");
void *context = zmq_ctx_new();
void *responder = zmq_socket(context, ZMQ_REP);

// associo l'indirizzo del responder al contesto
zmq_bind(responder, "tcp://*:5555");

// loop di ricezione
char string[10];
while (zmq_recv(responder, string, 10, 0) != -1) {
printf("richiesta ricevuta: %s\n", string);

// invio la risposta
zmq_send(responder, "Pong", 5, 0);
}

// disconnetto
zmq_close(responder);
zmq_ctx_destroy(context);
return 0;
}

E già questo codice dimostra che ZeroMQ è una libreria che permette di scrivere codice di rete molto semplificato. Ma con CZMQ possiamo snellirlo ancora di più (ma senza aspettarci miracoli: la libzmq già di per sé un passo avanti rispetto al vero low-level, che è rappresentato dai BSD Socket). Vai di nuovo col codice!

// client.c - un semplice client ZeroMQ con CZMQ
#include <stdio.h>
#include <czmq.h>

// main() - funzione main
int main(void)
{
// create requester
printf ("Connessione al server...\n");
zsock_t *requester = zsock_new_req("tcp://localhost:5555");

// loop di invio
int cnt = 0;
while (cnt++ < 10) {
// invio la richiesta
zstr_send(requester, "Ping");

// ricevo la risposta
char *string;
string = zstr_recv(requester);
printf("risposta ricevuta: %s\n", string);
zstr_free(&string);
}

// disconnetto
zsock_destroy(&requester);
return 0;
}
// server.c - un semplice server ZeroMQ con CZMQ
#include <stdio.h>
#include <czmq.h>

// main() - funzione main
int main(void)
{
// creo il responder
printf ("Avvio server...\n");
zsock_t *responder = zsock_new_rep("tcp://*:5555");

// loop di ricezione
char *string;
while ((string = zstr_recv(responder)) != NULL) {
printf("richiesta ricevuta: %s\n", string);
zstr_free(&string);

// invio la risposta
zstr_send(responder, "Pong");
}

// disconnetto
zsock_destroy(&responder);
return 0;
}

Che ne dite? La differenza tra le tre versioni, pur non essendo eclatante, è evidente, e si nota una filosofia di programmazione abbastanza diversa. E, a parte le considerazioni filosofiche, alla fin fine si può dire che nel percorso da BSD Socket a libzmq a CZMQ, le linee di codice vanno in diminuzione, e questo è il fattore molto importante che stavamo ricercando.

Ok, per oggi può bastare: ZeroMQ passa, a pieni voti, l'esame di "ottima libreria per Software di rete". Lo passa bene già con la libreria low-level e "più meglio" (ah ah ah) con CZMQ, la libreria high-level. Ma, nonostante questo, vi invito a non dimenticare che, sotto sotto (anche sotto ZeroMQ!), ci sono i mitici e insostituibili BSD Socket che possono essere ancora usati direttamente per scrivere ottimo e soddisfacente codice di rete, magari solo un po' più complicato (da scrivere e da leggere). De gustibus...

Ciao, e al prossimo post!

martedì 28 giugno 2022

System? No, grazie!
considerazioni sul perché non usare la system(3) in C (e C++)

[Frase sulla t-shirt regalata a N.Van Orton/M.Douglas]: Sono stato drogato e lasciato per morto in Messico, e tutto quello che ho ottenuto è questa stupida maglietta.

Ebbene si, l'ho fatto un altra volta: nell'ultimo articolo avevo promesso una imminente seconda parte, e invece oggi parleremo d'altro (ma niente paura, la seconda parte su ZeroMQ arriverà presto! Fidatevi!). Succede che, come al solito, ottengo uno spunto da fatti reali (di solito per cose che faccio sul lavoro) e dico "Ma qui c'è materiale per un articolo!", per cui oggi parleremo di system(3). Uhm, ma abbiamo un film collegato? Mah, visto che appartiene alla serie dei "No Grazie!" (ah: ne ho scritti altri e vi invito a leggerli o rileggerli, qui, qui, qui e qui) non farò giochi di parole con il titolo, e quindi ne sceglierò uno a caso tra quelli che ho visto/rivisto ultimamente... si, The Game, va benissimo: è un bel film del grande David Fincher, e non può mancare nella lista di un Cinefilo. Anzi, una attinenza film/articolo c'è: system(3) è una funzione anti-pattern, quindi chi la usa troppo può ridursi come il protagonista della foto qui sotto...

...guardate come mi sono ridotto usando la system(3)...

system(3), è una funzione della libc usata e abusata: è molto comoda e semplice da usare e permette di fare cose, anche complicate, sfruttando applicazioni esterne: proprio questo è, allo stesso tempo, il suo maggior pregio e difetto. Chiariamolo subito: se avete bisogno di scrivere rapidamente una applicazione, diciamo, "sperimentale", o un programma di test (o testing) ad uso interno, o volete semplicemente verificare (rapidamente) se una idea è realizzabile, nessuno vi può criticare se usate la system(3). Ma per una applicazione reale che andrà in produzione è corretto usarla? E vabbè, la risposta è una sola:

NO, ASSOLUTAMENTE NO!

Ok, è ora di entrare nel dettaglio. Cosa è la system(3) e cosa permette di fare? Il manuale della glibc titola semplicemente questo:

system - execute a shell command

quindi ci permette di chiamare, dal nostro programma, un comando esterno del sistema operativo (o meglio: un comando nostro o del sistema operativo chiamabile tramite la Shell).

(...apro una parentesi: come al solito l'articolo è focalizzato sui sistemi POSIX, ma le note che seguono relative all'uso, pregi e difetti della system(3) sono perfettamente adattabili anche a Windows, dove la stessa funzione è disponibile, con l'unica differenza che usa CMD.EXE invece della UNIX Shell o della Linux Bash. Chiudo la parentesi...)

Per cui, ad esempio, se vogliamo che il nostro programma mostri che file sono contenuti in una directory  che si chiama "test" possiamo, molto semplicemente, usare system(3) per chiamare il comando "ls ./test" usando ls(1) di Linux. Facilissimo, no? Vediamo un piccolo esempio di codice che vi farà gioire (spoiler: e subito dopo ve lo smonterò!):

#include <stdlib.h>

// main() - funzione main
int main(int argc, char *argv[])
{
// uso system(3) per leggere il contenuto della directory ./test
system("ls ./test");

exit(EXIT_SUCCESS);
}

Ok, system(3) è indubbiamente comoda da usare, ma si porta dietro una lista di problemi così lunga che si potrebbe scriverne in un libro. Vediamone alcuni:

  1. Non è per nulla portabile: non è possibile garantire al 100% che su tutti i sistemi dove verrà installata la mia applicazione i comandi da eseguire con system(3) siano disponibili.
  2. Apre, come minimo, due processi supplementari: su Linux invoca bash(1) che, a sua volta, invoca il comando da eseguire (alla faccia dell'efficienza).
  3.  Quando si chiama la system(3) il programma principale viene sospeso fino al termine del comando invocato, e non c'è nessuna maniera di controllare efficacemente quello che sta succedendo.
  4. Questo, poi, lo dice direttamente il manuale della system(3): "During execution of the command, SIGCHLD will be blocked, and SIGINT and SIGQUIT will be ignored, in the process that calls system().", e questo non è una semplice descrizione di funzionamento, è la descrizione di un possibile problema.
  5. Anche questo, poi, lo dice direttamente il manuale della system(3): "Do not use system() from a privileged program (a set-user-ID or set-group-ID program, or a program with capabilities)". La sicurezza del sistema può essere molto compromessa se la stringa di comando viene ad una fonte esterna e può essere manipolata: il grande pericolo si chiama Shell Command Injection.
  6. Un altro buco di sicurezza è possibile se il sistema non è completamente sicuro (cosa frequente...) e un utente malintenzionato può, in qualche maniera, alterare il path di ricerca degli eseguibili o sostituire un eseguibile con un altro con lo stesso nome.
  7. Non c'è alcun modo di leggere direttamente l'output del comando eseguito nel programma che invoca la system(3).
  8. Problemi vari ed eventuali: la system(3) in un loop non controllato, la system(3) e la thread-safety problematica, la system(3) per eseguire un comando in background, ecc. Una lunga lista che vi risparmio.

Insomma, è un vero disastro. Come dite? Ah si, sento alcuni che dicono "A me i problemi della system(3) non interessano, tanto io uso la popen(3)!". Ok, ho delle cattive notizie: almeno la metà dei problemi elencati li ha anche la popen(3), che forse non è un disastro ma, direi, è un mezzo disastro, sai che consolazione...

Ah, solo come curiosità: nella lista dei problemi qui sopra quello che mi molesta di più è il numero 3: una applicazione che usa system(3) non ha maniera di controllare quello che sta succedendo con i programmi esterni invocati, e deve sospendersi per aspettare che finiscano l'esecuzione: ma questa vi sembra la descrizione di una buona applicazione da mandare in produzione? E vabbè, continuiamo cosi, facciamoci del male...

Quindi: che bisogna fare se abbiamo una attività buona per la system(3) ma non vogliamo usarla visto che siamo programmatori consapevoli? Abbiamo due soluzioni:

  1. La soluzione semplice ma accettabile: usare fork(2) + execv(3) + waitpid(2): è un po' una emulazione, un po' laboriosa, della system(3), che però evita i problemi elencati sopra: non usa la Shell, ci permette di mantenere il controllo sull'esecuzione, ci permette di evitare i problemi di shell-iniection, ecc. ecc.
  2. La soluzione complicata ma impeccabile: scrivere nel nostro programma il codice che vorremmo far eseguire esternamente con la system(3): in alcuni casi potrebbe risultare abbastanza laborioso, ma avremo tutto dentro il nostro codice, quindi avremo il controllo totale sia dell'esecuzione che della implementazione.

Che dite? Volete un esempio dei due casi? Magari con lo stesso tema di eseguire/emulare un comando "ls ./test"? Eccoli!

Caso 1: fork(2) + execv(3) + waitpid(2)

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <errno.h>

// funzione main()
int main(int argc, char* argv[])
{
pid_t pid = fork();
if (pid == 0) {
// sono il figlio
char *pathname = "/usr/bin/ls";
char *newargv[] = { "ls", "./test", NULL };
execv(pathname, newargv);
exit(EXIT_FAILURE); // exec non ritorna mai
}
else if (pid > 0) {
// sono il padre
int status;
waitpid(pid, &status, 0);
exit(EXIT_SUCCESS);
}
else {
// errore fork()
printf("error: %s\n", strerror(errno));
}
}

Caso 2: implementazione interna

#include <stdio.h>
#include <stdlib.h>
#include <dirent.h>

// main() - funzione main
int main(int argc, char *argv[])
{
// apro la directory
DIR *dir = opendir("./test");
if (dir) {
// leggo una entry alla volta
struct dirent *dir_ent;
while ((dir_ent = readdir(dir)) != NULL) {
// mostro solo se è un file
if (dir_ent->d_type == DT_REG)
printf("%s ", dir_ent->d_name);
}

// chiudo la directory e la linea
printf("\n");
closedir(dir);
}

exit(EXIT_SUCCESS);
}

Che ne dite? Entrambi i casi sono interessanti, funzionali e non usano la system(3)! Se volete testarli basta creare (nella directory dove avete compilato il programma) una directory test con dentro qualche file (io ci ho copiato i sorgenti mostrati in questo articolo). Poi eseguite i programmi, et voilà!

E, già che ci sono, posso mostrarvi dei risultati reali: ho eseguito direttamente (con bash) il comando "ls ./test"  e poi ho eseguito e i tre programmi di test (quello con system(3) e i due senza). Il risultato è stato il seguente:

aldo@Linux $ ls ./test/
forkexecwait.c readdir.c system.c
aldo@Linux $ ./system
forkexecwait.c readdir.c system.c
aldo@Linux $ ./forkexecwait
forkexecwait.c readdir.c system.c
aldo@Linux $ ./readdir
readdir.c forkexecwait.c system.c

che era esattamente quello aspettato. Provare per credere!

Ok, per oggi può bastare: spero di avervi convinto che system(3) è una funzione anti-pattern, e di più non posso aggiungere. Nella seconda parte dell'articolo penso che tratteremo usi di ZeroMQ più sofisticati e usando un approccio high-level... oops! Ma questo è quello che avevo scritto alla fine dello scorso articolo! Speriamo, almeno questa volta, di riuscire a mantenere la promessa...

Ciao, e al prossimo post!