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ì 26 aprile 2023

The Big Select
come usare la select(2) in C - pt.2

Big Lebowski: Cos'è... cos'è che fa di un uomo un uomo, signor Lebowski?
Drugo: Non... non lo so, signore.
Big Lebowski: Essere pronti a fare ciò che è più giusto. A qualunque costo. Non è questo che fa di un uomo un uomo?
Drugo: Sì, quello e un paio di testicoli.

Nel capolavoro The Big Lebowski, il Lebowski "grande" diceva al Drugo che un vero uomo fa sempre la cosa giusta, a qualunque costo (e vabbè, poi il Drugo con il suo solito acume aggiungeva un piccolo dettaglio...). Riportando questo al nostro caso, e cioè all'uso della ottima e indispensabile system-call select(2), potrebbe sembrare che usarla non sia proprio la cosa più giusta, visto che, come anticipato nello scorso articolo, ci sono delle controindicazioni. Ok, è venuto il momento di verificare se è il caso di usarla o no!

...cioè, spiegami bene 'sta storia delle controindicazioni...

Allora, il manuale della select(2) inizia la descrizione con un Warning, il che dovrebbe preoccuparci:

WARNING: select() can monitor only file descriptors numbers that
are less than FD_SETSIZE (1024) — an unreasonably low limit for
many modern applications — and this limitation will not change.
All modern applications should instead use poll(2) or epoll(7),
which do not suffer this limitation.

e certo, se lo dice il Linux Programmer's Manual c'è poco da dubitare. E poi, chi siamo noi per dubitare del manuale? Eppure io, in questo caso particolare, dubito, e non perché voglia mettere in dubbio quanto sopra (e ci mancherebbe! Il manuale è la bibbia del programmatore Linux!), ma perché mi sembra che il Warning sia strettamente riferito a un dettaglio implementativo di Linux (e, infatti, il manuale POSIX della pselect(3p) non ne parla) e, soprattutto, la frase "an unreasonably low limit for many modern applications" è un po' eccessiva: secondo me le applicazioni che devono maneggiare più di 1024 file descriptors alla volta sono l'eccezione e non la regola. L'importante è ricordarsene al momento opportuno, e se necessario usare, come consigliato, poll(2) o epoll(7).

E a proposito della system-call poll(2) si può aggiungere che ha, esattamente come la select(2), una "versione p" che si chiama ppoll(2), che differisce dalla versione "normale" più o meno come la select(2) differisce dalla pselect(2):

  • Usa una struct timespec per il timeout mentre la poll(2) usa un int  (in  millisecondi).
  • Il timeout ha un comportamento differente: il valore del timeout viene mantenuto costante durante l'attività della ppoll(2), mentre potrebbe venire aggiornato (decrementato) durante l'azione della poll(2).
  • Ha un ulteriore argomento sigmask che, come indica il nome, permette di personalizzare i segnali POSIX durante l'esecuzione della ppoll(2): rimpiazza la sigmask del processo con la nuova sigmask e poi reinstalla quella originale al termine dell'attività della chiamata.

Se avete scritto del Software che usa la select(2) e volete convertirlo rapidamente alla poll(2) potete seguire questo semplice esempio (adattandolo alle esigenze del caso):

////////////////////////////////////////////////////////////////////////////////
VERSIONE CON LA SELECT
////////////////////////////////////////////////////////////////////////////////

// sorvegliamo stdin (fd 0) per verificare se viene scritto qualcosa
fd_set rfds;
FD_ZERO(&rfds); // azzero il set
FD_SET(0, &rfds); // aggiungo stdin (il fd 0) al set

// set del timeout a 5 secondi
struct timeval tv;
tv.tv_sec = 5; // set di 5 sec
tv.tv_usec = 0; // set di 0 usec (utile per aggiungere frazioni di secondo)

// chiamo select(2)
int retval = select(1, &rfds, NULL, NULL, &tv);

// uso retval
// ...

////////////////////////////////////////////////////////////////////////////////
VERSIONE CON LA POLL
////////////////////////////////////////////////////////////////////////////////

// sorvegliamo stdin (fd 0) per verificare se viene scritto qualcosa
struct pollfd rfds;
rfds.fd = 0; // aggiungo stdin (il fd 0) al set
rfds.events = POLLIN; // per verificare se ci sono dati da leggere

// set del timeout a 5 secondi
int timeout = 5000; // set di 5000 ms

// chiamo poll(2)
int retval = poll(&rfds, 1, timeout);

// uso retval
// ...

facile, no?

La epoll(7), invece, è tutta un'altra storia: è una vera e propria API presente solo su Linux che permette, usando le varie funzioni a disposizione (epoll_create(2), epoll_create1(2), epoll_ctl(2), epoll_wait(2)) un uso veramente molto sofisticato (ma, ahimè, abbastanza complicato) della gestione degli eventi di I/O: da usare proprio per esigenze particolari, direi.

E veniamo al famoso 10000, il numero scelto, non a caso, nell'articolo precedente per anticipare i problemi della select(2): visto il limite di 1024 file descriptors, la select(2) non è la system-call più adatta a evitare il famigerato C10K problem, che descrive esattamente il caso di un mega-server che deve trattare moltissime connessioni contemporanee: l'autore del primo articolo che trattò l'argomento scelse a titolo esemplificativo il numero 10000 (che, ai tempi, nel 1999, era un numero enorme per l'Hardware disponibile, mentre ora questo numero sembra persino piccolissimo per i mega-server attuali). Ecco spiegato il mistero del 10000 (e leggetevi l'articolo citato sopra, è molto interessante).

Ed ora, come promesso nella prima parte dell'articolo, vi propongo un uso un po' originale della select(2): non so se ricordate il ciclo di articoli sulle POSIX IPC, dove nel capitolo riservato alle Message Queue avevo mostrato due programmi reader e writer (in pratica un server e un client) che usavano le funzioni mq_send(3) e mq_receive(3). Queste due funzioni hanno delle versioni "con timeout", che si chiamano, rispettivamente, mq_timedend(3) e mq_timedrecv(3). Queste versioni sono, in alcuni casi, molto utili, e sono assenti nelle analoghe chiamate send(2) e recv(2) della classica interfaccia BSD Socket. Per colmare questa mancanza ho scritto due nuove funzioni, timedSend() e timedRecv() che usano, ovviamente, la select(2) per la gestione del timeout. Le due nuove funzioni hanno gli stessi parametri delle versioni "normali" con l'aggiunta di un ulteriore parametro "timeout_ms" che serve a impostare, per l'appunto, il timeout. E allora vediamole, 'ste funzioni: vai col codice!

include <stdio.h>
#include <errno.h>
#include <sys/select.h>
#include <sys/types.h>
#include <sys/socket.h>
#include "timedSendRecv.h" // contiene solo i prototipi

// timedRecv - una recv(2) con timeout
ssize_t timedRecv(int sockfd, void *buf, size_t len, int flags, unsigned int timeout_ms)
{
// test se timeout_ms è maggiore di 0
if (timeout_ms) {
// timeout maggiore di 0: set del timeout della select(2)
struct timeval tv;
tv.tv_sec = timeout_ms / 1000;
tv.tv_usec = (timeout_ms % 1000) * 1000;

// eseguo select(2) e (eventualmente) la recv(2)
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(sockfd, &readfds);
int rc = select(sockfd + 1, &readfds, NULL, NULL, &tv);
if (rc == -1) {
// select(2) fallita: ritorno errore senza cambiare errno
return -1;
} else if (rc) {
// dati disponibili: eseguo recv(2)
return recv(sockfd, buf, len, flags);
} else {
// timeout scaduto: ritorno errore con errno=ETIMEDOUT (utile per il chiamante)
errno = ETIMEDOUT;
return -1;
}
}
else {
// timeout_ms è 0: eseguo direttamente la recv(2)
return recv(sockfd, buf, len, flags);
}
}

// timedSend - una send(2) con timeout
ssize_t timedSend(int sockfd, const void *buf, size_t len, int flags, unsigned int timeout_ms)
{
// test se timeout_ms è maggiore di 0
if (timeout_ms) {
// timeout maggiore di 0: set del timeout della select(2)
struct timeval tv;
tv.tv_sec = timeout_ms / 1000;
tv.tv_usec = (timeout_ms % 1000) * 1000;

// eseguo select(2) e (eventualmente) la send(2)
fd_set writefds;
FD_ZERO(&writefds);
FD_SET(sockfd, &writefds);
int rc = select(sockfd + 1, NULL, &writefds, NULL, &tv);
if (rc == -1) {
// select(2) fallita: ritorno errore senza cambiare errno
return -1;
} else if (rc) {
// dati disponibili: eseguo send(2)
return send(sockfd, buf, len, flags);
} else {
// timeout scaduto: ritorno errore con errno=ETIMEDOUT (utile per il chiamante)
errno = ETIMEDOUT;
return -1;
}
}
else {
// timeout_ms è 0: eseguo direttamente la send(2)
return send(sockfd, buf, len, flags);
}
}

Che ne dite? Sono relativamente semplici, stra-commentate (non credo che ci sia nulla da aggiungere) e funzionano anche bene! Io le uso da molto tempo anche in progetti reali, e permettono alcuni "giri di codice" interessanti, per esempio quando è necessario fare un loop di lettura non bloccante. Provare per credere!

Ok, per oggi può bastare, e anzi può bastare anche per l'argomento select/poll (su cui si potrebbe scrivere un libro, ma per il momento ci fermiamo qui). Per il prossimo articolo non vi prometto nessun argomento in particolare (che poi non mantengo le promesse e mi devo pure scusare). L'unica cosa che vi prometto è che sarà sicuramente molto interessante! (ehm, che modestia...).

Ciao, e al prossimo post!

domenica 19 marzo 2023

The Big Select
come usare la select(2) in C - pt.1

Poliziotto: E nella valigetta?
Drugo: Oh, beh, documenti, solo documenti. Già, solo i miei documenti. Documenti di lavoro.
Poliziotto: Che lavoro fa?
Drugo: Sono disoccupato.

Stavo cercando di ultimare la seconda parte dell'oramai mitico articolo sulla programmazione real-time e mi sono reso conto che in anni e anni di onorata carriera divulgativa (ehm...) non ho mai parlato della select(2). Non sia mai! La select(2) è una funzione così importante che non si può rimandare ulteriormente. L'altro articolo dovrà aspettare ancora un po' (anzi, smetto di parlarne, quando arriverà sarà una sorpresa): oggi si parla di select(2) e farò come il drugo del capolavoro dei fratelli Coen: lui si che è un tipo concreto (vedi il dialogo qui sopra) uno con degli obiettivi precisi e diretti (si, si, lo so, e i più attenti se ne saranno già accorti: il film e il dialogo li ho già usati per un altro articolo... ma l'ho rivisto da poco (il film) e non ho resistito alla tentazione di ri-utilizzarlo, chiedo venia...).

...sorseggiavo il mio White Russian e pensavo: "Ma... e la select(2)?"...

E allora veniamo al dunque: la select(2) è una system-call importantissima che permette (come dice il manuale) di eseguire il "synchronous I/O multiplexing", e cioè permette di sorvegliare più canali di I/O alla volta (che tipo di canali? Pensate ai socket, ai file aperti, ecc.) per verificare quando sono pronti per una nuova operazione di read/write. Cioè, in pratica, permette di eseguire in un singolo thread di esecuzione quello che spesso si esegue (in maniera ingiustificata) in multithreading (e scusate se è poco!). Un piccolo esempio: un buon Server TCP che serve 10000 Client: secondo voi è più efficiente e funzionale aprire 10000 thread che aspettano i dati dai Client o usare il multiplexing ? Se qualcuno pensa che è meglio aprire 10000 thread il mio consiglio è:

  • mettere le scarpe da running e correre per 10 Km a buon ritmo (un metro per ogni thread...).
  • dopodiché fare una bella doccia rilassante e ripensare all'argomento con la mente (ora) decisamente più aperta.
  • a questo punto, se si preferiscono ancora i 10000 thread, c'è da considerare l'idea di cambiare mestiere.

Ma, ovviamente, scherzo: ci sono in giro Server TCP e Web con multithread "spinto", scritti da gente brava e competente, in grado di servire ben più di 10000 connessioni alla volta (usando, però, mostruose risorse Hardware di CPU e RAM). In ogni caso io continuo a pensare che il mutithreading  viene usato spesso a sproposito per semplice pigrizia progettuale, e quando posso lo evito (ah, dimenticavo: il numero 10000 qui sopra non l'ho scelto a caso, ci ritorneremo nella seconda parte dell'articolo).

Eppure, nonostante gli evidenti meriti, la select(2) è abbastanza misconosciuta, e penso che i motivi siano due:

  1. Non è immediatamente evidente dove e quando sia utile usarla.
  2. Non è semplicissima da usare, visto che lavora in simbiosi con ben quattro macro, che preparano l'ambiente di esecuzione e testano i risultati.

E allora cerchiamo di fare chiarezza, siamo qui per questo! Per quanto riguarda il punto 1 lo abbiamo già descritto sopra, e la parola magica è "multiplexing" (anche se, in realtà, ci sono anche altri usi interessanti che vedremo prossimamente). Una volta chiarito dove e quando usarla si può passare al come, e credo che può tornare utile questa piccola lista che ho scritto, con le descrizioni degli argomenti della select(2) e delle quattro macro abbinate:

// select() - gestisce il synchronous I/O multiplexing su un set di descrittori di file
int select(
int nfds, // fd con il numero più alto (+1) nei 3 set sorvegliati
fd_set *readfds, // set di fd da sorvegliare per "ready for reading"
fd_set *writefds, // set di fd da sorvegliare per "ready for writing"
fd_set *exceptfds, // set di fd da sorvegliare per eventi eccezionali
struct timeval *timeout); // tempo di bloccaggio del set durante la sorveglianza

// FD_CLR() - rimuove il file descriptor fd dal set di descrittori
FD_CLR(
int fd, // file descriptor da rimuovere dal set
fd_set *set); // set di file descriptor

// FD_SET() - cerca il file descriptor fd nel set di descrittori
FD_ISSET(
int fd, // file descriptor da cercare nel
fd_set *set); // set di file descriptor

// FD_SET() - aggiunge il file descriptor fd al set di descrittori
FD_SET(
int fd, // file descriptor da aggiungere al set
fd_set *set); // set di file descriptor

// FD_SET() - rimuove tutti i file descriptor dal set di descrittori
FD_ZERO(
fd_set *set); // set di file descriptor da svuotare

E tutto questo lo trovate anche nel manuale, eh! E, sicuramente, con più dettagli e con migliori descrizioni, ma lo specchietto qui sopra è una specie di quick-reference guide per chi non ha voglia di leggersi le mille spiegazioni del manuale (che, in questo caso, sono un po' complesse e magari contribuiscono a far passare la voglia di usare la select(2)...). Nella stessa pagina del manuale si descrive anche una system-call "gemella", la pselect(2), che è sostanzialmente identica a parte queste caratteristiche:

  • Ha un sesto argomento sigmask che, come indica il nome, permette di personalizzare i segnali POSIX surante l'esecuzione della pselect(2): rimpiazza la sigmask del processo con la nuova sigmask e poi reinstalla quella originale al termine dell'attività della pselect(2). Questa è, evidentemente, una funzionalità molto utile e interessante.
  • Usa una struct timeval per il timeout (invece di una struct timespec): questo cambio tipo è abbastanza irrilevante, ma è associato a un comportamento differente: il valore del timeout viene mantenuto costante durante l'attività della pselect(2), mentre potrebbe venire aggiornato (decrementato) durante l'azione della select(2): anche questo fatto è da tenere in conto scrivendo il codice.

Un ultimo appunto lo merita il descrittore exceptfds: per "eventi eccezionali" non si intendono gli errori ma, tipicamente, messaggi speciali ("Urgent Messages") generati da alcuni protocolli: un buon esempio è il messaggio "out-of-band" che si può ricevere su un socket TCP (ma questo è un argomento molto particolare che necessiterebbe un articolo a parte: diciamo che il set exceptfds si usa poco e in casi molto specifici).

E quindi come si usa la select? Diciamo che per un uso "classico" sono sufficienti questi cinque passi:

  1. Si definiscono, usando il tipo fd_set, i set di descrittori di file da sorvegliare.
  2. Si inizializzano i set usando la macro FD_ZERO (per azzerare il set) seguita da FD_SET (per aggiungere un file al set).
  3. Si prepara il timeout riempiendo una struct timeval (ovvero si scrivono i secondi e i microsecondi che compongono il nostro timeout)
  4. Si lancia la select(2) con gli argomenti preparati nei punti precedenti.
  5. Si testa il risultato della select(2) per eseguire le varie ed eventuali operazioni che necessitiamo.

E qui casca a fagiolo un bell'esempio pratico ed elementare, lo stesso presente nel manuale, tradotto e con qualche commento in più (perché inventarne uno nuovo? Questo è veramente ben fatto). In quest'esempio si sorveglia il descrittore 0 che non è nient'altro che il famoso standard input "stdin" e, in base all'attività sullo stdin (ossia se scriviamo o no qualcosa sulla tastiera), visualizzeremo il risultato corrispondente: nell'esempio il timeout è di 5 secondi e quindi, se non scriviamo nulla, apparirà dopo 5 secondi la scritta "nessun dato disponibile" , ma se scriviamo qualcosa prima che scada il timeout, apparirà la scritta "ci sono dati disponibili". Semplicissimo, no? Vai col codice!

#include <stdio.h>
#include <stdlib.h>
#include <sys/select.h>

// funzione main()
int main(void)
{
fd_set rfds;
struct timeval tv;
int retval;

// sorvegliamo stdin (fd 0) per verificare se viene scritto qualcosa
FD_ZERO(&rfds); // azzero il set
FD_SET(0, &rfds); // aggiungo stdin (il fd 0) al set

// set del timeout a 5 secondi
tv.tv_sec = 5; // set di 5 sec
tv.tv_usec = 0; // set di 0 usec (utile per aggiungere frazioni di secondo)

retval = select(1, &rfds, NULL, NULL, &tv);
/* N.B. da qui in avanti il valore di tv cambia dinamicamente:
bisogna tenerlo in conto nel caso di usarlo! */

if (retval == -1) {
// retval == -1 indica che la select() ha fallito
perror("select()");
}
else if (retval) {
/* retval > 0 indica che qualcuno ha scritto
qua si poteva anche usare questo test: FD_ISSET(0, &rfds) > 0 */
printf("ci sono dati disponibili!\n");
}
else {
// retval == 0 indica che è scaduto il timeout
printf("nessun dato disponibile\n");
}

exit(EXIT_SUCCESS);
}

Ok, per oggi può bastare. Spero che questa introduzione abbia fatto comprendere la potenza e l'utilità della sistem-call select(2) e abbia fatto venire la voglia a qualcuno di usarla in qualche progetto reale. Nella seconda parte dell'articolo parleremo delle criticità della select(2) (spoiler: ahimè, ce ne sono! Ad esempio quel 10000 usato qua sopra...), parleremo delle possibili alternative e, dulcis in fundo, proporrò un esempio di uso "non proprio canonico" della select(2) che potrebbe interessare a molti. Non state in pena, ci risentiremo presto!

Ciao, e al prossimo 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!