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.

Visualizzazione post con etichetta fork. Mostra tutti i post
Visualizzazione post con etichetta fork. Mostra tutti i post

mercoledì 22 febbraio 2023

Licorice System pt.2
come scrivere una system(3) con cattura dello stdout in C

Alana: Lo sapevo! Sapevo che era quello che stavi pensando. Pensi sempre alle cose, pensatore! Tu, pensatore! Tu pensi cose!

Nell'ultimo articolo avevo annunciato un seguito incentrato sui metodi di programmazione real-time, ma ho deciso di rimandarlo, perché ho avuto la necessità di aggiungere (per motivi miei esterni al blog) una nuova funzionalità a una versione migliorata della system(3) che avevo proposto in un altro articolo, e mi è sembrata una buona idea fare una seconda puntata con un po' di dettagli (spero interessanti). Il fatto è che bisognerebbe sempre essere come il "pensatore" della frase citata qui sopra (tratta dal bel Licorice Pizza del Maestro Paul Thomas Anderson). Pensare, pensare, pensare... quello si che è un gran lavoro, e i programmatori ne sanno qualcosa, no?

...ho pensato ripetutamente, ma non ricordo cosa...

Riepilogando, vi ricordo che questo articolo è, di fatto, la terza parte di System? No, grazie!, anche se il titolo originale si era già perso nella seconda parte (e, già che ci sono, vi ricordo che di articoli della famiglia "No Grazie!" ne ho scritti altri, e vi invito a leggerli o rileggerli, quiqui, qui e qui).

Nella prima parte avevo descritto i mille problemi della system(3), una vera funzione anti-pattern da non usare mai, e nella seconda parte avevo proposto una funzione, la toutSystem(), che correggeva vari problemi, tra cui il più grave:

"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."

e quanto sopra implica anche che, oltre alla mancanza di controllo, potremmo avere la nostra applicazione (o un thread dell'applicazione) completamente bloccata da una system(3) che non è ritornata! Con la toutSystems() questo problema sparisce, perché si introduce un fondamentale timeout oltre il quale il nostro comando esterno viene bloccato restituendo un adeguato tracciamento dell'errore. Molto bene, no?

Poi, però, ho pensato che alla toutSystem() manca qualcosa: Ok, non si blocca e ci permette di conoscere l'esito, buono o cattivo, della nostra esecuzione (attraverso il semplice codice di ritorno) ma... e se avessimo anche bisogno di ricevere dei dati dal comando esterno eseguito? Magari scritti, come è logico aspettarsi, nello standard output (stdout per gli amici)? Si può fare? Ma certo! vai col codice!

#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <time.h>
#include <fcntl.h>
#include <sys/wait.h>

// prototipi locali
static int toutSystemStdout(const char* command, unsigned int timeout_ms, char *dest,
size_t n);
static void mySleep(unsigned int milliseconds);

#define TOUT_SLEEP 500 // intervallo di sleep per il loop busy wait del timeout

// funzione main()
int main(int argc, char *argv[])
{
// test con un programma che scrive sullo stdout e esce
printf("main: eseguo toutSystemStdout(\"./test\", 5000, buf, sizeof(buf))\n");
char buf[256]; // verificare se questo può andare in overflow
if (toutSystemStdout("./test", 5000, buf, sizeof(buf)) != -1)
printf("%s: toutSystemStdout: cmd output: %s\n", __func__, buf);
else
printf("%s: toutSystemStdout error\n", __func__);

return 0;
}

// toutSystem() - una system(3) con timeout e catturo dello stdout del comando
static int toutSystemStdout(
const char *command, // il comando shell da eseguire (e.g.: cp -v file1 file2)
unsigned int timeout_ms, // timeout in ms: 0 significa senza timeout
char *dest, // buffer destinazione per lo standard output del comando
size_t n) // size del buffer
{
char errmsg_buf[256];

// creo una pipe in modo nonblocking
int pipefd[2]; // pipefd[0] = lato input della pipe; pipefd[1] = lato output della pipe
if (pipe2(pipefd, O_NONBLOCK) == -1) {
// errore pipe
printf("%s: errore pipe: %s\n",
__func__, strerror_r(errno, errmsg_buf, sizeof(errmsg_buf)));
return -1;
}

// fork + exec + wait
pid_t pid = fork();
if (pid == 0) {
// figlio
//

// chiudo pipefd[0] che è il lato input/read della pipe
close(pipefd[0]);

// uso dup2() invece di close()+dup() per evitare eventuali race-condition
dup2(pipefd[1], STDOUT_FILENO); // per il stdout

// chiudo pipefd[1] che è il lato output/write della pipe che già non serve
close(pipefd[1]);

// eseguo il comando come lo esegue la system(3)
execl("/bin/sh", "sh", "-c", command, (char *) NULL);

// questo viene eseguito solo se fallisce exec (exec non ritorna mai)
printf("%s: figlio: processo %d: errore exec: %s\n",
__func__, getpid(), strerror_r(errno, errmsg_buf, sizeof(errmsg_buf)));
exit(EXIT_FAILURE);
}
else if (pid > 0) {
// padre
//

// chiudo pipefd[1] che è il lato output/write della pipe
close(pipefd[1]);

// attesa uscita del figlio
printf("%s: padre: processo %d: attesa uscita del figlio\n", __func__, getpid());
int rc_wait;
int status;
if (timeout_ms > 0) { // check timeout
// busy wait con timeout
int cnt_wait = 0;
while (read(pipefd[0], dest, n) != 0) {
mySleep(TOUT_SLEEP);
if (++cnt_wait > timeout_ms / TOUT_SLEEP) {
// figlio non uscito prima del timeout: return errore
printf("%s: padre: processo %d: waitpid timeout scaduto\n",
__func__, getpid());
return -1;
}
}

rc_wait = waitpid(pid, &status, WNOHANG);
}
else {
// wait senza timeout
while (read(pipefd[0], dest, n) != 0)
mySleep(TOUT_SLEEP);

rc_wait = waitpid(pid, &status, 0);
}

// attesa terminata: analizzo il risultato
if (rc_wait != pid) {
// errore waitpid
printf("%s: padre: processo %d: errore waitpid (%s)\n",
__func__, getpid(), strerror_r(errno, errmsg_buf, sizeof(errmsg_buf)));
return -1;
}
else {
// processo terminato: return risultato
int result = -1;
if (WIFEXITED(status)) {
// questo è l'unico risultato accettato come successo
result = 0;
printf("%s: padre: processo %d: pid %d uscito (status=%d)\n",
__func__, getpid(), pid, WEXITSTATUS(status));
}
else if (WIFSIGNALED(status))
printf("%s: padre: processo %d: pid %d ucciso dal segnale %d\n",
__func__, getpid(), pid, WTERMSIG(status));
else if (WIFSTOPPED(status))
printf("%s: padre: processo %d: pid %d fermato dal segnale %d\n",
__func__, getpid(), pid, WSTOPSIG(status));
else
printf("%s: padre: processo %d: pid %d con stato sconosciuto (status=%d)\n",
__func__, getpid(), pid, status);

return result;
}
}
else {
// errore fork
printf("%s: processo %d: errore fork: %s\n",
__func__, getpid(), strerror_r(errno, errmsg_buf, sizeof(errmsg_buf)));
return -1;
}
}

// 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);
}

Come avrete notato dal codice e, come al solito, dai commenti prolissi, la nuova funzione che ho originalissimamente chiamato toutSystemStdout() mantiene la struttura della versione precedente però con un bel po' di cambi, perché questo di intercettare nel processo padre lo standard output del processo figlio non è esattamente un gioco da ragazzi. Il trucco consiste, fondamentalmente, nell'usare una pipe per mettere in comunicazione i due processi, ma bisogna farlo con un certo stile, se no non funziona nulla. Riassumo i punti più importanti commentati nel codice:

  • Si aggiunge un buffer (con la relativa lunghezza) al prototipo della funzione, per salvare i dati scritti nello stdout dal comando esterno eseguito.
  • Si crea una pipe (con pipe2(2)) di cui verrà usato solo il canale di comunicazione da processo figlio a processo padre. La pipe deve essere nonblocking per rispettare la natura della nostra pseudo-system con timeout.
  • I descrittori del canale di scrittura della pipe deve essere duplicato sullo standard output, e bisogna usare dup2(2) invece della sequenza close(2)+dup(2): questo perché dup2(2) esegue internamente la sequenza in maniera atomica (evitando problemi di race-condition), e voi sapete già quanto ci teniamo alla robustezza della programmazione in ambito multitasking/multithreading, no?
  • Il ciclo di busy-wait per la gestione del timeout si fa, ora, sulla lettura (con read(2)) dello stdout invece che sul waitpid(2): ovviamente se non si ha bisogno di leggere lo stdout del comando esterno è consigliabile usare la normale toutSystem(): non esistono funzioni universali, per ogni caso d'uso bisogna usare sempre la funzione più adatta. Ricordatelo!

Ah, prima che mi dimentichi: la funzione qui sopra è un esempio didattico: nella versione di produzione bisognerebbe mettere qualche chiusura e qualche controllo in più: ad esempio ne manca uno (solo possibile, credo) sull'overflow del buffer destinazione dei dati scritti. Ma questo compito ve lo lascio a voi, sbizzarritevi!

Per provare questa nuova funzione ho scritto un piccolissimo programma, test.c, da usare come comando esterno: scrive in loop cinque volte "ciao" e poi esce dopo aver eseguito una sleep(3) di 4 secondi: la sleep serve per misurare se funziona ancora il timeout (che continua ad essere la parte fondamentale della funzione): chiamando la toutSystemStdout() con un timeout di 5 secondi si può giocare con la sleep(3) di test.c per verificare il buon funzionamento della nuova funzione (a parte verificare che riesca veramente a catturare i dati dello stdout del comando esterno). Il codice di test.c è questo:

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

// funzione main()
int main(int argc, char* argv[])
{
for (int i = 0; i < 5; i++)
fprintf(stdout, "ciao %d ", i);

sleep(4);

return 0;
}

Se eseguiamo il nostro programma di prova della toutSystemStdout() con timeout=5sec e test.c compilato con sleep=4 otteniamo:

aldo@Linux $ ./toutSystemStdout
main: eseguo toutSystemStdout("./test", 5000, buf, sizeof(buf))
toutSystemStdout: padre: processo 19866: attesa uscita del figlio
toutSystemStdout: padre: processo 19866: pid 19867 uscito (status=0)
main: toutSystemStdout: cmd output: ciao 0 ciao 1 ciao 2 ciao 3 ciao 4

mentre, se eseguiamo ancora con timeout=5sec e test.c compilato con sleep=6sec otteniamo:

aldo@Linux $ ./toutSystemStdout
main: eseguo toutSystemStdout("./test", 5000, buf, sizeof(buf))
toutSystemStdout: padre: processo 19880: attesa uscita del figlio
toutSystemStdout: padre: processo 19880: waitpid timeout scaduto
main: toutSystemStdout error

Come volevasi dimostrare: nel primo caso (quello buono) ottengo i dati scritti dal comando test sullo stdout, mentre nel secondo caso ottengo (come sperato) un errore di timeout senza dati. Provare per credere!

E anche per oggi può bastare. nel prossimo articolo torneremo sulla programmazione real-time... o magari no, chissà che non mi tocchi rimandarlo ancora una volta, non si sa mai!

Ciao, e al prossimo post!

mercoledì 26 ottobre 2022

Licorice System
come scrivere una system(3) con timeout in C

Gary: Signore e signori posso avere la vostra attenzione? Lasciate che vi presenti la futura signora Alana Valentine.
Alana: Idiota.

Questo articolo è la seconda parte (non prevista, devo ammetterlo) di System? No, grazie!, quindi dovrebbe ripetere lo stesso titolo; però poco tempo fa ho visto il bel Licorice Pizza del Maestro Paul Thomas Anderson e non ho resistito alla tentazione di agganciarlo a questo post. E, a maggior ragione, l'ho fatto anche perché questo secondo articolo non è più un "No grazie!" ma è una variazione sul tema con una proposta interessante. Interessante come il film in oggetto, pieno di frasi lapidarie come quella vista sopra. Un piccolo gioiellino che ci ha regalato (grazie Paul!) un P.T.Anderson in veste leggera e spensierata, ma sempre a modo suo (ah, divagando: ne ho scritti altri di “No Grazie!” e vi invito a leggerli o rileggerli, quiqui, qui e qui).

...corri, se no scade il timeout della nuova system...

E allora: immagino che tutti avete ben chiari i difetti della system(3) che ne fanno una funzione anti-pattern (e se non li avete chiari correte a rileggere l'articolo, svelti, prima che scappi!). Come ricorderete avevo elencato una lunga serie di problemi, indicando questo come il più grave:

"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."

e dopo calcavo la mano in questa maniera:

"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..."

Per cui, di cosa parleremo oggi? Ma di una system(3) con timeout, proprio quello che ci serve! In realtà l'idea originale era di fare una versione che risolvesse anche tutti gli altri problemi della lista (e in effetti ne ho scritta una) ma non volevo gettare troppa carne al fuoco, e quindi, per il momento, vi propongo questa che risolve il problema più importante (e scusate se è poco!).

Ok, è venuto il momento di far cantare il codice, che è pieno di commenti (che dovrebbero essere sufficienti a spiegare il tutto), ma comunque aggiungerò anche qualche nota in coda. Vai col codice!

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

// prototipi locali
static int toutSystem(const char* command, unsigned int timeout_ms);
static void mySleep(unsigned int milliseconds);

#define TOUT_SLEEP 500 // intervallo di sleep per il loop busy wait del timeout

// funzione main()
int main(int argc, char* argv[])
{
// test con uno shell-script ma funziona con qualsiasi comando disponibile
printf("main: eseguo toutSystem(\"./script.sh > out.txt\", 5000)\n");
toutSystem("./script.sh > out.txt", 5000);

return 0;
}

// toutSystem() - una system(3) con timeout
static int toutSystem(
const char *command, // il comando shell da eseguire (e.g.: cp -v file1 file2)
unsigned int timeout_ms) // timeout per waitpid(2) in ms: 0 significa senza timeout
{
char errmsg_buf[256];

// fork + exec + wait
pid_t pid = fork();
if (pid == 0) {
// figlio: eseguo il comando
printf("%s: figlio: processo %d: eseguo il comando\n",
__func__, getpid());
execl("/bin/sh", "sh", "-c", command, (char *) NULL);

// questo viene eseguito solo se fallisce exec (exec non ritorna mai)
printf("%s: figlio: processo %d: errore exec: %s\n",
__func__, getpid(), strerror_r(errno, errmsg_buf, sizeof(errmsg_buf)));
exit(EXIT_FAILURE);
}
else if (pid > 0) {
// padre: attesa uscita del figlio
printf("%s: padre: processo %d: attesa uscita del figlio\n",
__func__, getpid());
int rc_wait;
int status;
if (timeout_ms > 0) { // check timeout
// busy wait con timeout
int cnt_wait = 0;
while ((rc_wait = waitpid(pid, &status, WNOHANG)) == 0) {
mySleep(TOUT_SLEEP);
if (++cnt_wait > timeout_ms / TOUT_SLEEP) {
// figlio non uscito prima del timeout: return errore
printf("%s: padre: processo %d: waitpid timeout scaduto\n",
__func__, getpid());
return -1;
}
}
}
else {
// wait senza timeout
rc_wait = waitpid(pid, &status, 0);
}

// figlio uscito: return risultato
if (rc_wait != pid) {
// waitpid error
printf("%s: padre: processo %d: errore waitpid (%s)\n",
__func__, getpid(), strerror_r(errno, errmsg_buf, sizeof(errmsg_buf)));
return -1;
}
else {
// processo terminato: return risultato
int result = -1;
if (WIFEXITED(status)) {
// questo è l'unico risultato accettato come successo
result = 0;
printf("%s: padre: processo %d: pid %d uscito (status=%d)\n",
__func__, getpid(), pid, WEXITSTATUS(status));
}
else if (WIFSIGNALED(status))
printf("%s: padre: processo %d: pid %d ucciso dal segnale %d\n",
__func__, getpid(), pid, WTERMSIG(status));
else if (WIFSTOPPED(status))
printf("%s: padre: processo %d: pid %d fermato dal segnale %d\n",
__func__, getpid(), pid, WSTOPSIG(status));
else
printf("%s: padre: processo %d: pid %d con stato sconosciuto (status=%d)\n",
__func__, getpid(), pid, status);

return result;
}
}
else {
// errore fork
printf("%s: errore fork: %s\n",
__func__, strerror_r(errno, errmsg_buf, sizeof(errmsg_buf)));
return -1;
}
}

// 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);
}

Come avrete notato la nuova funzione, che ho battezzato toutSystem() (un nome originalissimo...) è scritta sulla falsariga dell'esempio con  fork(2) + exec(3) + wait(2) del precedente articolo, che era una delle soluzioni proposte come buona alternativa alla system(3) e che fa al caso nostro per sviluppare questa nuova versione.

Il funzionamento è (relativamente) semplice: la funzione esegue fork(2) e si sdoppia in padre e figlio. Il figlio esegue il comando <command> esattamente come lo fa, internamente, la system(3), per cui usa execl(3) per creare una sub-shell di esecuzione (e fino a qui siamo abbastanza in linea con la system(3) e con alcuni dei i suoi difetti, sigh). La parte buona, però la esegue il padre che, invece di limitarsi ad aspettare l'uscita del figlio, esegue un loop in stile busy wait  usando in maniera "intelligente" waitpid(3) per un tempo mai superiore ai millisecondi indicati dall'argomento <timeout_ms>. Alla fine del loop si testa il risultato dell'attesa per decidere se ritornare un errore o un esito, e il risultato che ci preme è stato conseguito: la toutSystem() non può bloccare indefinitamente il nostro programma (al contrario di quello che può fare la famigerata system(3)), ma lo blocca al massimo per la durata del timeout. E, comunque, ho lasciato la possibilità di simulare il comportamento "classico" (ossia: attesa indefinita) usando zero come timeout.

L'esempio qui sopra contiene anche un main() di prova, così gli increduli potranno compilare e verificare direttamente il funzionamento. Si può eseguire qualsiasi comando: io nell'esempio ho eseguito uno shell script che ho scritto proprio per verificare l'efficacia del timeout. Lo script (che poteva essere anche un semplice codice C compilato, eh!) è questo:

#!/bin/bash

for i in 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
do
echo $i
sleep 1
done

Lo script scrive per 15 secondi un numero progressivo nel file "out.txt" su cui è rediretto il comando di toutSystem() nel main(): toutSystem("./script.sh > out.txt", 5000). Se compilate ed eseguite l'output sarà questo:

main: eseguo toutSystem("./script.sh > out.txt", 5000)
toutSystem: padre: processo 16463: attesa uscita del figlio
toutSystem: figlio: processo 16464: eseguo il comando
toutSystem: padre: processo 16463: waitpid timeout scaduto

che indica che la nostra chiamata a toutSystem() è uscita per timeout dopo 5 secondi (lo script eseguito lavora per 15 secondi, quindi è ancora attivo in quel momento), e difatti noterete che il file "out.txt" si riempirà 10 secondi dopo l'uscita del programma principale. Provare per credere!

Ok, per oggi può bastare: la toutSystem() è già perfettamente utilizzabile nella forma proposta, ed è una ottima alternativa all'uso della system(3) che, non mi stanco mai di dirlo, non si dovrebbe mai usare. Vi consiglio,  come utile esercitazione, di provare a modificare la toutSystem() per ovviare anche agli altri problemi elencati nell'articolo System? No, grazie!. Io l'ho fatto e vi assicuro che non è un lavoro molto complicato (e prossimamente vi mostrerò la mia soluzione, promesso).

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!