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.

venerdì 16 dicembre 2022

A History of Sched_yield
considerazioni sull'uso della sched_yield(2) su Linux

Joey: Richie, io sono qui per fare pace... dimmi che devo fare per mettere le cose a posto.
Richie: Una cosa la potresti fare, credo... potresti morire, Joey.

Questo articolo ha un argomento spinoso, esattamente come l'argomento trattato nel bellissimo A History of Violence del Maestro David Cronenberg, caratterizzato da una trama ambigua che è una sequenza di pacifiche e tranquillizzanti scene familiari intervallata da improvvisi momenti di iper-violenza. Qui mi tocca tornare su un argomento che ho trattato solo di striscio, l'uso della system-call sched_yeld(2), che ho usato in un esempio di un mio vecchio articolo e, con l'intenzione di essere il più possibile preciso, sono costretto a rivedere un po' l'esempio, dato che quello proposto lì non era valido universalmente, anzi era valido solo per usi particolari. Mi fascio la testa...

...sched_yield, o non sched_yield, questo è il dilemma...

La spinta alla scrittura di questo articolo mi è venuta leggendo un bell'articolo, questo: To sched_yield() Or Not To sched_yield()? (che ho citato nella didascalia qua sopra, ah ah ah). L'articolo in questione, oltre ad alcune interessanti note teoriche, contiene anche un bel benchmark realizzato su una vera macchina multiprocessore (perfetta per test di multiprocessing e/o multithreading reali), nientepopodimeno che un Cray con 256 CPU (con Linux, ovviamente: vi siete mai chiesto perché i super-computer usano quasi sempre Linux, qualche volta UNIX, e quasi mai Windows? Beh, chiedetevelo...). Visto che io non ho a disposizione un mostro del genere non vi proporrò nessun benchmark, quello del'articolo citato è più che sufficiente per mostrare che usare la sched_yield(2) non sempre è una buona idea. Per chi non lo ricordasse la sched_yield(2) fa questo:

sched_yield - cede il processore

sched_yield() fa sì che il thread chiamante rinunci alla CPU. Il thread viene
spostato alla fine della coda per la sua priorità statica e un nuovo thread
viene eseguito.

E veniamo al dunque: il mio vecchio articolo (in due puntate) parlava più che altro di sleep (se avete voglia potete rileggerlo) e qui vi farò un breve riassunto della solo parte che ci interessa oggi. E allora: avevo proposto una funzione di sleep in millisecondi che, in maniera intelligente, decideva se usare nanosleep(2), usleep(3) o sched_yield(2), in base alle esigenze e disponibilità. Vai col codice!

// Sleep() - una sleep in ms
void Sleep(unsigned int milliseconds)
{
// testa il tempo di sleep per intercettare il valore 0
if (milliseconds > 0) {
// usa nanosleep() o usleep()
#if (_POSIX_C_SOURCE >= 199309L)
struct timespec ts;
ts.tv_sec = milliseconds / 1000;
ts.tv_nsec = ( milliseconds % 1000) * 1000000;
nanosleep(&ts, NULL);
#else
usleep(milliseconds * 1000);
#endif
}
else {
// usa sched_yield() come alternativa Linux alla Sleep(0) di Windows
sched_yield();
}
}

Quindi avevo applicato questa nuova Sleep() a un semplicissimo programma con due thread usandola come sleep di un loop infinito (il codice ve lo risparmio, comunque è consultabile nel vecchio articolo). Grazie a questo codice avevo realizzato un piccolo benchmark per provare la famigerata Sleep(0) (di origine Windows) e l'effetto reale che si otteneva cambiando il valore di sleep del loop: nella tabella seguente ci sono i risultati originali (con una macchina Linux con un i7 con 4 core e 8 thread). Per i casi inferiori al millisecondo avevo modificato il codice per usare direttamente la obsoleta (ma in questo caso comoda) usleep(3), e i dati si riferiscono all'uso di CPU e al numero "i" di cicli effettuati dai thread:

senza sleep i = 5314563827 CPU = 100%
Sleep(0) i = 18863299 CPU = 100% (esegue sched_yield())
Sleep(10) t=10ms i = 900 CPU = 1/2%
Sleep(1) t=1ms i = 9000 CPU = 1/2%
usleep(100) t=100us i = 62970 CPU = 1/2%
usleep(10) t=10us i = 145000 CPU = 1/2%
usleep(1) t=1us i = 170000 CPU = 1/2%
usleep(0) t=0us i = 170000 CPU = 1/2% (test con usleep(0))

Ora, citerò direttamente il vecchio articolo, che diceva:

Il primo caso ci mostra perché qui conviene usare la sleep: i nostri due thread funzionano bene (i due contatori avanzano simultaneamente), e la attività è gestita dal thread-scheduler (facile in questo caso: un thread dell'applicazione su un thread della CPU), ma comunque, se non lavorano volontariamente in maniera cooperativa (o gentile, se preferite) e non rilasciano ogni tanto la CPU, finiscono col mangiarsela tutta (due degli otto CPU-thread, nel mio caso), e in un sistema multi-process non è una buona cosa, no?.

Esaminiamo, ora, il secondo caso che ci dimostra che la famosa Sleep(0)/sched_yield() di cui abbiamo parlato nell'altro articolo non da proprio dei gran risultati: i thread lavorano meno (guardare il counter) ma la CPU va lo stesso al 100%.

Il terzo e il quarto caso ci mostrano che, usando valori dell'ordine di grandezza del time-slice del sistema, la CPU lavora poco e il comportamento è lineare (fino a 1 ms).

I casi successivi mantengono il rispetto della CPU ma il comportamento diventa irregolare (i counter non aumentano proporzionalmente alla riduzione della sleep). Conclusione: in questa tipo di architettura Software (che è un buon riferimento, essendo abbastanza comune) è conveniente usare dei tempi paragonabili al time-slice del sistema, ossia tra 1 e 10 ms.

L'ultimo caso l'ho aggiunto solo per curiosità, per confermare quanto detto nell'altro articolo: usleep(0) non è equivalente a Sleep(0) (magari qualcuno non ci credeva...) e applica il tempo di sleep minimo possibile, che è di 1 us.

E torniamo al presente: come si può ben notare la sched_yield(2) non dava dei gran risultati, però l'avevo lasciata ugualmente nel codice della nuova Sleep() a mo' di "funziona male, ma se proprio volete usarla...". In realtà più che funzionare male è una di quelle system-call che o si usano bene o non si usano (avevamo visto qualcosa di simile nei miei 2 articoli sulla funzione pthread_cancel(3), ricordate?), quindi ci vuole molta cautela e, alla fine della fiera, è possibile che non si debba mai usare, a parte casi particolarissimi (e questo, scusate l'OT, mi risulta vero anche per la Sleep(0) di Windows). Del resto è ben scritto anche nel manuale della sched_yield(2):

Chiamate strategiche a sched_yield() possono migliorare le prestazioni dando ad
altri thread o processi la possibilità di funzionare quando risorse (fortemente)
contese (e.g.: dei mutex) sono state rilasciate dal chiamante. Evitate di
chiamare sched_yield() inutilmente o in modo inappropriato (ad esempio, quando
le risorse necessarie ad altri thread schedulabili sono ancora in possesso del
chiamante), poiché ciò comporterà inutili commutazioni di contesto, con
conseguente peggioramento delle prestazioni del sistema.

e, soprattutto, in questo paragrafo (sempre nel manuale):

sched_yield() è destinato all'uso con politiche di schedulazione in tempo reale
(ad esempio, SCHED_FIFO o SCHED_RR). L'uso di sched_yield() con politiche di
schedulazione non deterministiche come SCHED_OTHER non è specificato e molto
probabilmente significa che il progetto dell applicazione non è corretto.

E, solo come curiosità, vi segnalo anche una nota dal manuale Red Hat Enterprise Linux for Real Time che indica che anche in ambito real-time la sched_yield(2) è da usare con parsimonia (per i processi, ma per i thread il discorso dovrebbe essere simile):

La funzione sched_yield è stata originariamente progettata per indurre un
processore a selezionare un processo diverso da quello in esecuzione. Questo
tipo di richiesta è incline a fallire quando viene emessa da un'applicazione
mal scritta.
Quando la funzione sched_yield() viene utilizzata all'interno di processi con
priorità in tempo reale, può presentare un comportamento inaspettato. Il processo
che ha chiamato sched_yield viene spostato in coda alla coda dei processi in
esecuzione con quella priorità. Quando questo accade in una situazione in cui non
ci sono altri processi in esecuzione con la stessa priorità, il processo che ha
chiamato sched_yield continua la sua esecuzione. Se la priorità di questo processo
è alta, può potenzialmente creare un busy loop, rendendo la macchina inutilizzabile.
In generale, non utilizzare sched_yield su processi in tempo reale.

Ebbene si, il codice che proposi non era realmente universale, perché la sched_yield(2) si dovrebbe usare (con molta cautela) solo quando Linux usa uno scheduler di tipo real-time (SCHED_FIFO o SCHED_RR), e questo è un caso particolare, visto che il caso normale (il default) di Linux è l'uso dello scheduler  SCHED_OTHER. E quindi? Beh, devo aggiornare l'esempio, che ora si presenta così:

// Sleep() - una sleep in ms
void Sleep(unsigned int milliseconds)
{
// check se il tempo di sleep è zero
if (milliseconds == 0)
milliseconds = 1; // forza 1ms

#if (_POSIX_C_SOURCE >= 199309L)
// uso nanosleep(2)
struct timespec ts;
ts.tv_sec = milliseconds / 1000;
ts.tv_nsec = (milliseconds % 1000) * 1000000L;
nanosleep(&ts, NULL);
#else
// uso usleep(3) - NOTA: POSIX.1-2001 dichiara questa funzione come obsoleta
usleep(milliseconds * 1000);
#endif
}

Volendo si potrebbe migliorare questo codice forzando l'uso della sched_yield(2) quando i milliseconds sono 0 e lo scheduler (che si può ottenere usando sched_getscheduler(2)) è SCHED_FIFO o SCHED_RR, ma, essendo questo un caso molto particolare (e da usare con le molle) non mi sembrava il caso di farlo. Notare che ho forzato l'uso di una sleep minima di 1 ms coerentemente con i risultati del mio benchmark qui sopra, dove è dimostrato che un valore ottimale di sleep minima è quello dell'ordine di grandezza del time-slice del sistema.

E per oggi può bastare, ho messo i puntini sulle i, anche se mi è costato un poco ammettere di non avere, a suo tempo, fornito un esempio veramente rigoroso. Eh si, Errare Humanum Est...

Ciao, e al prossimo post! 

lunedì 28 novembre 2022

Atomichunter
come usare le funzioni atomic built-in di GCC in C

Graham: Non è curioso di sapere se Lei è più furbo di quello che cerchiamo?
Lecktor: Stai scherzando? Tu sei più furbo, visto che mi hai preso...
Graham: No, non sono più furbo di Lei.
Lecktor: E allora come hai fatto a prendermi?
Graham: Lei era svantaggiato.
Lecktor: In che senso svantaggiato?
Graham: Lei è pazzo.

Per parlare delle funzioni atomic built-in di GCC ho deciso di fare un temerario abbinamento con il capolavoro Manhunter del Maestro Michael Mann, un film assolutamente imperdibile. Il fatto è che per risolvere un problema pratico (ebbene si, oggi parleremo del mondo reale) a volte un programmatore deve trasformarsi in un cacciatore (fortunatamente non di uomini), e quindi deve investigare, provare, cercare informazioni qui e là per risolvere un caso reale. Si, bisogna fare come il detective Graham del film: beh, magari senza arrivare ai suoi estremi, e chi ha visto il film sa di cosa parlo...

...sai, ti ho preso grazie alle atomic built-in di GCC...

Non so se ricordate un mio articolo di un annetto fa, Atomic: Endgame, in cui decantavo la bontà del nuovo type qualifier _Atomic introdotto con C11 (e, ovviamente, se non lo ricordate siete tenuti a rileggerlo...). Nell'articolo era (spero) ben descritta la nuova funzionalità e si evidenziava che, grazie a _Atomic, la scrittura di applicazioni multi-thread  diventava molto più agevole (era ora!). E perché oggi torniamo sull'argomento? E qui sopraggiunge il problema pratico citato sopra: supponiamo (true story) di dover aggiungere una nuova prestazione a una vecchia applicazione su Linux Embedded usando la piattaforma (e quindi il compilatore) originale, che, per la legge di Murphy, non supporta C11 (anzi supporta a stento C99): che facciamo? Dovremo mica riscrivere il codice (già scritto e testato su piattaforme più recenti) in modo di usare i classici meccanismi di sincronizzazione delle variabili (mutex, ecc.)? No, ci deve essere una soluzione più semplice!

E quindi mi sono Graham-izzato (vedi sopra) e ho scoperto che il nostro amato GCC, grande amico dei programmatori C (e non solo), ha da molto tempo delle funzioni built-in che hanno anticipato le funzionalità di _Atomic ben prima del supporto a C11. Anzi, abbiamo ben due famiglie di built-in denominate __sync_* e __atomic_* , aggiunte con a la seguente sequenza temporale:

  • GCC 4.1: __sync_* built-in (nel 02/2006)
  • GCC 4.4: __sync_* built-in (nel 04/2009,  con supporto ARM)
  • GCC 4.7: __atomic_* built-in (nel 03/2012)
  • GCC 4.9: _Atomic (nel 04/2014, supporto completo)

Considerando la data in cui _Atomic è entrato in GCC il supporto a queste operazioni è stato fornito con un buon anticipo, no? Nella lista qui sopra ho anche evidenziato quando è stata aggiunta la compatibilità con ARM, perché l'argomento di questo articolo è decisamente indirizzato alla programmazione con Linux Embedded, e ARM è una piattaforma molto usata in questo ambito. Tra l'altro si può evidenziare che anche le versioni recenti di GCC continuano a supportare questi built-in, e quindi un eventuale Software "datato" che le usa si può ancora compilare senza problemi con un GCC recente, e senza modificare nulla (questa si che è vera retro-compatibilità! Grazie gcc.gnu.org!).

Nella mia ricerca ho anche notato che la quantità di informazioni a livello base (tipicamente quella fornita da gcc.gnu.org) è notevole in quantità e qualità, però latitano un po' i veri esempi d'uso (il codice!), e quindi ho deciso di contribuire un po' con questo articolo, che interesserà a pochi (credo) ma per quei pochi potrebbe essere una manna dal cielo (vabbé, non esageriamo...).

Che ho fatto allora? Ho scritto una mini-libreria per semplificare l'uso dell funzioni atomic built-in, e ho strutturato la libreria in maniera che si comporti in modo "intelligente" riconoscendo la versione del GCC in uso, per passare automaticamente da _Atomic a __atomic_* oppure a __sinc_*. Ho scritto un header, atoint.h che definisce un nuovo tipo atoint (nome originalissimo che sta per atomic int). Vediamolo: vai col codice!

/* atoint.h - header della libreria atoint
versioni delle funzioni atomiche:
_Atomic: da GCC 4.9 in avanti (40900L)
__atomic: da GCC 4.7 in avanti (40700L)
__sync con ARM: da GCC 4.4 in avanti (40400L)
__sync: da GCC 4.1 in avanti (40100L) */

// GCC_VERSION contiene la versione di GCC come numero (e.g.: GCC4.9 = 40900)
#define GCC_VERSION (__GNUC__ * 10000 + __GNUC_MINOR__ * 100 + __GNUC_PATCHLEVEL__)

#if GCC_VERSION >= 40900L
// da GCC 4.9 in avanti è disponibile _Atomic di C11
#include <stdatomic.h>
typedef atomic_int atoint; // uso atomic_int per atoint
#else
// _Atomic di C11 non è disponibile
typedef int atoint; // uso int per atoint
#endif

#if GCC_VERSION >= 40900L
// uso le funzioni di _Atomic
#define ATOMIC_STORE(ptr, val) atomic_store((ptr), val)
#define ATOMIC_LOAD(ptr) atomic_load(ptr)
#define ATOMIC_INC(ptr) atomic_fetch_add((ptr), 1)
#define ATOMIC_DEC(ptr) atomic_fetch_sub((ptr), 1)
#elif GCC_VERSION >= 40700L
// uso le funzioni di GCC __atomic prefixed built-in
#define ATOMIC_STORE(ptr, val) __atomic_store_n((ptr), val, __ATOMIC_SEQ_CST)
#define ATOMIC_LOAD(ptr) __atomic_load_n((ptr), __ATOMIC_SEQ_CST)
#define ATOMIC_INC(ptr) __atomic_fetch_add((ptr), 1, __ATOMIC_SEQ_CST)
#define ATOMIC_DEC(ptr) __atomic_fetch_sub((ptr), 1, __ATOMIC_SEQ_CST)
#elif GCC_VERSION >= 40100L
// TODO: scrivere la versione con __sync prefixed built-in
// ...
#else
#error Spiacente, il tuo compilatore è troppo vecchio - aggiornalo, per favore.
#endif

// prototipi globali
atoint atoStore(atoint *val, atoint newval); // store
atoint atoLoad(atoint *val); // load
atoint atoPreInc(atoint *value); // prefix increment (++val)
atoint atoPostInc(atoint *value); // postfix increment (val++) (usa il prefix increment)
atoint atoPreDec(atoint *value); // prefix decrement (--val)
atoint atoPostDec(atoint *value); // postfix decrement (val--) (usa il prefix decrement)

Come si può ben vedere il file contiene la definizione del nuovo tipo e i prototipi delle funzioni della libreria. E, soprattutto, contiene degli alias (delle #define) delle funzioni che usano le appropriate atomic built-in in base alla versione del compilatore in uso, versione che è contenuta nella macro GCC_VERSION (è un numero identificativo abbastanza semplice da calcolare, ad esempio per GCC 4.9 è 40900). Con l'uso di alcune semplici direttive #if  per il preprocessore del C possiamo scegliere quali alias attivare automaticamente. In questa versione ho usato come requisito minimo per la famiglia __sync_* il GCC 4.1  (ver.40100), ma chi ha bisogno del supporto ARM dovrebbe usare come requisito minimo il GCC 4.4 (ver.40400). Usando un compilatore ancora più anziano del requisito minimo, la compilazione esce (miseramente) con questo errore: "Spiacente, il tuo compilatore è troppo vecchio - aggiornalo, per favore.".

Visto che non volevo mettere troppa carne al fuoco ho lasciato come TODO  la versione con __sync_*  che è un po' più complicata e avrebbe appesantito un po' il codice. Tra l'altro si dovrebbero/potrebbero aggiungere anche altre funzioni oltre a quelle presentate qui (e si può fare: io, mio malgrado, ho dovuto scrivere una versione full-optional perché mi serviva): magari ci torneremo in un prossimo articolo.

Grazie agli alias delle funzioni la implementazione della libreria è molto semplice e lineare. Vediamola:

// atoint.c - implementazione della libreria atoint
#include "atoint.h"

// store (assegna il valore)
atoint atoStore(atoint *val, atoint newval)
{
ATOMIC_STORE(val, newval);
return *val; // return del valore nuovo
}

// load (legge il valore)
atoint atoLoad(atoint *val)
{
return ATOMIC_LOAD(val);
}

// prefix increment (++val)
atoint atoPreInc(atoint *val)
{
ATOMIC_INC(val); // incrementa atomicamente
return *val; // return del valore nuovo
}

// postfix increment (val++) (usa il prefix increment)
atoint atoPostInc(atoint *val)
{
atoint old = *val; // salva il valore vecchio
atoPreInc(val); // incrementa atomicamente
return old; // return del valore vecchio
}

// prefix decrement (--val)
atoint atoPreDec(atoint *val)
{
ATOMIC_DEC(val); // decrementa atomicamente
return *val; // return del valore nuovo
}

// postfix decrement (val--) (usa il prefix decrement)
atoint atoPostDec(atoint *val)
{
atoint old = *val; // salva il valore vecchio
atoPreDec(val); // decrementa atomicamente
return old; // return del valore vecchio
}

È veramente molto semplice, no? E grazie ai molti commenti, credo che non sono necessarie ulteriori spiegazioni.

E, dulcis in fundo, bisogna provare se la libreria funziona, no? Si, ci vuole un bel programma di test, vai col codice!

#include <stdio.h>
#include <pthread.h>
#include "atoint.h"

// prototipi locali
void* updateCnt(void* arg);

// variabili globali (per un semplice test si possono usare!)
atoint ato_cnt; // un int atomico usato come contatore
int cnt; // un int normale usato come contatore

// funzione main
int main(void)
{
// init contatori
atoStore(&ato_cnt, 0);
cnt = 0;

// avvio 10 thread
pthread_t tid[10];
for (int n = 0; n < 10; ++n)
pthread_create(&tid[n], NULL, &updateCnt, NULL);

// attendo la fine dei 10 thread
for(int n = 0; n < 10; ++n)
pthread_join(tid[n], NULL);

// i risultati!
printf("Il contatore atomico vale: %u\n", ato_cnt);
printf("Il contatore normale vale: %u\n", cnt);

return 0;
}

// updateCnt() - funzione di update dei counter eseguita da ogni thread
void* updateCnt(void* arg)
{
// incremento i counter per 1000 volte
for (int n = 0; n < 1000; ++n) {
// uso prefix increment
atoPreInc(&ato_cnt);
++cnt;
}

return NULL;
}

I più attenti avranno notato che è esattamente lo stesso programma di test mostrato nell'articolo Atomic: Endgame, solo che in questo caso si usa il nuovo tipo atoint invece del tipo standard atomic_int: se il test funzionasse potremmo affermare che la libreria atoint è un buon sostituto della versione standard. Ci riusciremo? Proviamo: prima di tutto se stiamo usando un GCC recente (cosa probabile) dobbiamo forzare l'uso della versione senza _Atomic (ad esempio "truccando"  la define di GCC_VERSION), poi possiamo compilare ed eseguire. Il risultato è questo:

aldo@Linux $ ./test
Il contatore atomico vale: 10000
Il contatore normale vale: 4902

Ma funziona! Il contatore atomico atoint ha contato esattamente tutti gli incrementi e vale 10000 mentre il contatore normale ha perso molti degli incrementi a causa della concorrenza, e, quindi, vale molto meno (e ripetendo l'esecuzione si ottengono sempre valori inferiori a 10000). Se poi vogliamo fare anche "la prova del 9"  possiamo togliere il trucco che forza la versione antica e ricompilare con il nostro GCC recente: con _Atomic,  il risultato dovrebbe essere lo stesso (e se non lo è sarebbe il caso di dire "Houston, abbiamo un problema.").

E, se avete voglia di sperimentare, potete modificare leggermente il codice per usare anche le tre funzioni inutilizzate (atoPostInc(), atoPreDec() e atoPostDec()), vi assicuro che funzionano bene.

Ok, personalmente a questo punto sono soddisfatto e per oggi possiamo concludere qui. La libreria atoint funziona e spero vivamente che possa risultare utile a molti lettori. Nel prossimo articolo cambieremo argomento, ma, come promesso sopra, prima o poi torneremo su questa storia per completare la trattazione... e non trattenete il respiro nell'attesa, mi raccomando!

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!