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

Guardians of the CAN bus
come usare il CAN bus in C - pt.1

Drax: E uccideremo chiunque si metta sulla nostra strada!
Star-Lord: No! non uccideremo nessuno.
Drax: Uccidiamo un po' di gente...
Star-Lord: Non uccidiamo un po' di gente.
DraxUccidiamo solo una persona. Una stupida persona che nessuno ama...
Star-Lord: Ora stai solo rendendo la cosa triste...

In questo articolo prendo spunto dal bellissimo Guardiani della Galassia Vol.3 che ho visto da poco e con gran piacere. Questo gruppo di guerrieri abituati a grandi battaglie interstellari spesso si perdono (con notevole humor) in discussioni surreali come quella sopra, che rendono divertente un film dalla forte carica drammatica: un mix che il bravissimo James Gunn maneggia alla perfezione. 

E cosa centra con tutto questo il Controller Area Network (CAN bus per gli amici) che è il protagonista del post di oggi? Centra, centra... il CAN bus è un protocollo che unisce una apparente semplicità (e un bus seriale su doppino intrecciato + GND) a una molteplicità di prestazioni tipiche dei protocolli sofisticati (tipo Ethernet): è orientato ai messaggi, è insensibile ai disturbi elettromagnetici (è un ottimo bus di campo), è multiplexato (più dispositivi sullo stesso bus), è protetto dal Data Collision, è veloce (1 Mbit/s)... insomma, il CAN bus è un vero Guardiano della Galassia! Tra l'altro è nato per uso Automotive, che è uno degli usi più restrittivi esistenti, dove "l'affidabilità è prima di tutto": e grazie a queste caratteristiche l'uso si è allargato anche in molti altri settori. Viva il CAN bus!

...qualcuno si è ricordato di portare il CAN bus?...

E qui ci vorrebbe una breve descrizione di come funziona il CAN bus a livello elettrico, però dubito che riuscirei a farne una veramente esaustiva, visto che non è proprio il mio campo (a meno di non fare copia-e-incolla, ah ah ah), per cui vi rimando alle decine di descrizioni che si trovano in rete (ad esempio questa). 

Quello che conta per noi è come si può rappresentare questo protocollo con il Software e, osservando la disposizione dei dati "elettrici", salta all'occhio che il tipo di trasmissione è basato sul concetto di "frame", quindi di piccoli pacchetti di dati che contengono un identificatore unico ID di 11 bit che permette di "dare un nome" al pacchetto, un campo DLC che specifica la lunghezza del campo dati contenuto nel pacchetto e, infine, il vero e proprio campo Data di 0-8 byte che è l'unico campo a larghezza variabile (controllata dal campo DLC). Gli altri bit del pacchetto di dati non è necessario descriverli in questa sede, comunque tra di loro troviamo alcuni nomi famigliari come il SOF, il EOF, il CRC, ecc. Vi mostro una immagine qualsiasi delle mille che si trovano in rete:

Ecco, come ben visibile nel disegno, il frame ha una piccola sezione dati, di massimo 8 byte (per la versione classica, mentre per la versione estesa CAN FD sono 64 byte), e quindi rispetto a altri protocolli come Ethernet, il formato dati del CAN bus è più, per modo di dire "ristretto", basandosi su pacchetti di dimensioni ridotte (i frame, per l'appunto) che permettono di scambiare pochi dati alla volta, quindi non è fatto per trasmettere rapidamente "qualunque cosa" (pensate a un un file transfer con TCP/IP sotto Ethernet), ma piccoli gruppi di dati in maniera super-sicura e super-efficiente: è un vero bus industriale, perfetto per trasferire misure di sensori, setpoint, comandi, ecc.

E come si usa il CAN bus a livello Software? Riferendoci alla struttura del frame descritta sopra si può dire che un driver Software per CAN può limitarsi a isolare le tre parti "utili" del frame, e cioè: ID, DLC e Data, che sono sufficienti per gestire la comunicazione. Sotto Linux funziona così, e abbiamo a disposizione ben due maniere di operare (e due driver):

  • SocketCAN che è, come dice il nome, un network driver  inserito nella famiglia BSD socket.
  • can4linux che è un classico device driver a caratteri.

In questo articolo parleremo di SocketCAN, che ha vari vantaggi, tra cui:

  • È inserito di default nel Kernel Linux, quindi è perfettamente integrato nel sistema.
  • Permette di usare la classica interfaccia socket, che è veramente il prezzemolino della programmazione low-level.
  • È quello che conosco meglio, perché lo uso normalmente, mentre l'altro no... (e questo è l'attributo fondamentale, ah ah ah).

E veniamo a SocketCAN: considerato il tipo di protocollo socket-like c'è da aspettarsi che il Software di comunicazione abbia un aspetto familiare, tipo i notissimi Client/Server TCP o UDP (tipo quelli visti qui e qui), e in effetti è così, a parte un piccolo dettaglio: qui non ci sono Client e Server, tutti i dispositivi leggono e scrivono senza bisogno di instaurare una connessione, quindi è come se fossero tutti Server. Per mostrarvi un caso reale semplificato ho scritto due semplici esempi specializzati, in cui uno scrive (cansend.c) e l'altro legge (canrecv.c). E vediamo gli esempi... vai col codice!

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <net/if.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <linux/can.h>
#include <linux/can/raw.h>

// cansend - funzione main()
int main(int argc, char *argv[])
{
// creo il socket
int sockfd;
if ((sockfd = socket(PF_CAN, SOCK_RAW, CAN_RAW)) == -1) {
// errore socket()
printf("%s: errore socket() (%s)\n", argv[0], strerror(errno));
return EXIT_FAILURE;
}

// set degli attributi di i/o
struct ifreq ifr;
memset(&ifr, 0, sizeof(ifr));
snprintf(ifr.ifr_name, sizeof(ifr.ifr_name), "%s", "vcan0");
if (ioctl(sockfd, SIOCGIFINDEX, &ifr) == -1) {
// errore ioctl()
printf("%s: errore socket() (%s)\n", argv[0], strerror(errno));
close(sockfd);
return EXIT_FAILURE;
}

// set attributi dell'indirizzo
struct sockaddr_can addr;
memset(&addr, 0, sizeof(addr));
addr.can_family = AF_CAN;
addr.can_ifindex = ifr.ifr_ifindex;

// assegna l'indirizzo al socket
if (bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1) {
// errore bind()
printf("%s: errore bind() (%s)\n", argv[0], strerror(errno));
close(sockfd);
return EXIT_FAILURE;
}

// compongo il frame
struct can_frame frame;
memset(&frame, 0, sizeof(struct can_frame));
frame.can_id = 0x100U;
frame.can_dlc = 8;
snprintf(frame.data, sizeof(frame.data), "0123456");

// invio il frame
if (write(sockfd, &frame, sizeof(struct can_frame)) != sizeof(struct can_frame) == -1) {
// errore write()
printf("%s: errore write() (%s)\n", argv[0], strerror(errno));
close(sockfd);
return EXIT_FAILURE;
}

close(sockfd);
return EXIT_SUCCESS;
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <net/if.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <linux/can.h>
#include <linux/can/raw.h>

// canrecv - funzione main()
int main(int argc, char *argv[])
{
// creo il socket
int sockfd;
if ((sockfd = socket(PF_CAN, SOCK_RAW, CAN_RAW)) == -1) {
// errore socket()
printf("%s: errore socket() (%s)\n", argv[0], strerror(errno));
return EXIT_FAILURE;
}

// set degli attributi di i/o
struct ifreq ifr;
memset(&ifr, 0, sizeof(ifr));
snprintf(ifr.ifr_name, sizeof(ifr.ifr_name), "%s", "vcan0");
if (ioctl(sockfd, SIOCGIFINDEX, &ifr) == -1) {
// errore ioctl()
printf("%s: errore socket() (%s)\n", argv[0], strerror(errno));
close(sockfd);
return EXIT_FAILURE;
}

// set attributi dell'indirizzo
struct sockaddr_can addr;
memset(&addr, 0, sizeof(addr));
addr.can_family = AF_CAN;
addr.can_ifindex = ifr.ifr_ifindex;

// assegna l'indirizzo al socket
if (bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1) {
// errore bind()
printf("%s: errore bind() (%s)\n", argv[0], strerror(errno));
close(sockfd);
return EXIT_FAILURE;
}

// ricevo il frame
struct can_frame frame;
if (read(sockfd, &frame, sizeof(struct can_frame)) == -1) {
// errore read()
printf("%s: errore read() (%s)\n", argv[0], strerror(errno));
close(sockfd);
return EXIT_FAILURE;
}

// mostro il frame ricevuto
printf("id:0x%x dlc:%d data: ", frame.can_id, frame.can_dlc);
for (int i = 0; i < frame.can_dlc; i++)
printf("%c ", frame.data[i]);

printf("\n");

close(sockfd);
return EXIT_SUCCESS;
}

Visto? Sembrano due Server UDP, ma ancora più semplificati. Compilando ed eseguendo (poi vedremo come...) si nota che cansend si blocca in attesa che canrecv gli invii un frame, e una volta ricevuto lo mostra ed esce. Nella realtà un applicazione SocketCAN tipicamente scrive e legge, per cui è opportuno gestire azioni di read/write in maniera non-blocking, ma questi dettagli dipendono molto dal tipo di oggetto che si vuole realizzare. Ovviamente per testare questi programmi bisogna disporre di un CAN bus attivo nel sistema: nel prossimo articolo vedremo come si può (sotto Linux) lavorare e testare il Sotfware pur non disponendo di veri dispositivi Hardware CAN da provare (giurin giuretta che sarà veramente il tema del prossimo articolo). Già che ci siamo vi mostro anche un piccolo estratto dell'header can.h di Linux, dove si nota come è stato tradotto in Sofware il formato del frame Hardware:

/**
* struct can_frame - basic CAN frame structure
* @can_id: the CAN ID of the frame and CAN_*_FLAG flags, see above.
* @can_dlc: the data length field of the CAN frame
* @data: the CAN frame payload.
*/
struct can_frame {
canid_t can_id; /* 32 bit CAN_ID + EFF/RTR/ERR flags */
__u8 can_dlc; /* data length code: 0 .. 8 */
__u8 data[8] __attribute__((aligned(8)));
};

Visto? Vengono trattati solo i campi ID, DLC e Data del frame, e sono stati rinominati, rispettivamente, can_id, can_dlc e data. Il campo can_id è lungo 32 bit, di cui 11 sono dedicati al vero e proprio ID del frame, mentre gli altri hanno usi un po' speciali (gestione degli errori, ecc.).

Ok, per oggi può bastare. Nella seconda parte dell'articolo vi spiegherò come testare gli esempi mostrati usando un device CAN virtuale, e se avanza tempo magari parlerò anche dei filtri e degli errori. E, come vi ho già raccomandato altre volte, non trattenete il respiro nell'attesa! (che potrebbe risultare dannoso per la salute, ah ah ah).

Ciao, e al prossimo post! 

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!