Nei titoli e nei testi troverete qualche rimando cinematografico (ebbene si, sono un cinefilo). Se non vi interessano fate finta di non vederli, già che non sono fondamentali per la comprensione dei post...

Di questo blog ho mandato avanti, fino a Settembre 2018, anche una versione in Spagnolo. Potete trovarla su El arte de la programación en C. Buona lettura.

lunedì 19 giugno 2023

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

Nebula: Drax, siediti! [a Drax che è disteso]
Drax: È qui per questo!
Star-Lord: Drax, si chiama divano. Non è un letto.
Drax: Perché è così lungo allora?
Star-LordMa che...
Drax: Beh, trovo difficile credere che non abbia scopi multipli. È così allettante.

Ed eccoci alla seconda parte di Guardians of the CAN bus, un ciclo di articoli ispirati al bellissimo Guardiani della Galassia Vol.3 (ripassatevi la prima parte, please...). Nel dialogo qui sopra si può evidenziare lo spirito del film, con Drax il Distruttore sempre pronto a scatenare equivoci esilaranti... Eppure non è affatto un film comico, è molto più profondo, esattamente come questo articolo che potrebbe sembrare semi-serio ma è (spero) una concreta guida introduttiva ai misteri del CAN bus (e vabbé, io spero che lo sia...). In questa seconda parte parleremo di utilities, setup e filtri... uh, molta roba, cominciamo allora!

...secondo me il CAN bus si usa meglio da distesi...

Come promesso nell'articolo precedente il primo argomento che tratteremo riguarda la maniera di eseguire e testare Software per CAN bus (come i due esempi proposti nella prima parte) su una macchina Linux. In questo siamo abbastanza fortunati: il CAN bus oltre ad essere perfettamente integrato (attraverso SocketCAN) nel kernel  Linux, è anche ben supportato a livello utente (anzi, a livello programmatore), visto che fornisce un utilissimo set di utilità, le can-utils, pensate proprio per il testing, e dispone anche di un dispositivo virtuale che è perfetto per provare il Software sviluppato anche senza disporre di un dispositivo fisico "reale". Con le can-utils possiamo fare un sacco di operazioni, tra cui queste:

  • candump: visualizza, filtra e registra i dati CAN su file
  • canplayer: riproduce i file di log del CAN.
  • cansend: invia un singolo frame.
  • cangen: genera un traffico CAN casuale.
  • cansequence: invia e controlla una sequenza di frame CAN con payload crescente.
  • cansniffer: visualizza le differenze di contenuto dei dati CAN.

E queste sono solo le funzioni principali! Ce ne sono anche molte secondarie! Ma, visto che in rete ci sono già delle buone guide descrittive di can-utils, non voglio riscoprire l'acqua calda, e quindi vi consiglio di leggere, ad esempio, direttamente la descrizione su GitHub.

E passiamo al lato pratico: una volta scritto il codice vorremo provarlo, no? (credo che questa sia sempre una attività raccomandabile...) E allora dobbiamo chiedere al nostro amico Linux di attivare il dispositivo CAN, che può essere uno vero o, come detto sopra, uno virtuale che si comporta esattamente come uno reale (questo virtual device è un vero gioiellino che aiuta moltissimo il lavoro di programmazione). Per attivare la comunicazione CAN possiamo usare un semplice Bash Script di setup come il seguente:

#!/bin/bash

# aggiunge il nuovo device
sudo ip link add dev vcan0 type vcan

# abilita il nuovo device
sudo ip link set vcan0 up

Come indicato nei commenti (che devono essere sempre presenti e chiari sia nel C che nel Bash) il primo comando aggiunge un device di tipo rete (stiamo usando SocketCAN) che abbiamo chiamato vcan0 ed è di tipo virtuale (è un "vcan"). Il seguente comando usa anche lui l'interfaccia di rete e dice semplicemente a Linux di attivare il device. Tutto qua!

È semplicissimo, no? E se il dispositivo è "reale" che faremo? Useremo un tipo "can" invece di "vcan" e, per coerenza, lo chiameremo can0 invece di vcan0. Ricordatevi che vcan0 e can0 sono solo nomi (e possiamo usarne anche "di fantasia"), bisogna solo tenere presente che il nostro codice dovrà, poi, riferirsi al nome assegnato in setup al dispositivo. E il numero 0? Si mette per ricordare che possiamo trattare più bus alla volta, quindi mettere un numero progressivo è una buona idea.

Adesso che abbiamo installato e attivato il nostro device CAN possiamo compilare ed eseguire gli esempi dell'altra volta (canrecv.c e cansend.c), ottenendo questi risultati:

aldo@Linux $ gcc canrecv.c -o canrecv
aldo@Linux $ ./canrecv
id:0x100 dlc:8 data: 0 1 2 3 4 5 6
aldo@Linux $

Qui sopra canrecv si era messo in attesa, e il risultato (id:0x100 dlc:8 data: 0 1 2 3 4 5 6) è apparso, ovviamente, solo quando in un altro terminale ho eseguito cansend :

aldo@Linux $ gcc cansend.c -o cansend
aldo@Linux $ ./cansend
aldo@Linux $

sono soddisfazioni...

E adesso possiamo passare al prossimo punto, che, direi, si potrebbe chiamare "Funzioni avanzate di SocketCAN: filtri ed errori".  L'argomento che mi preme trattare per primo è quello dei filtri, ossia di come limitare la ricezione dei messaggi a "ricevo solo i can_id che mi interessano", anche perché dobbiamo ricordare che CAN bus è, per l'appunto, un bus, quindi potrebbe esserci un certo affollamento di dispositivi e, quindi, di messaggi che circolano.

Il metodo di filtraggio è relativamente semplice, e si basa sull'attributo CAN_RAW_FILTER del socket in uso, e si deve settare usando la system-call setsockopt(2) a cui bisogna passare la lista dei filtri da applicare, usando la apposita struttura can_filter descritta in uno degli header file di SocketCAN, e cioè linux/can.h. Ma forse è meglio fornire un semplice esempio facendo cantare il codice: di seguito vi mostro canrecvfilt.c, che, come indica il nome, è una canrecv.c con i filtri. 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>

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

// set di 2 filtri (ma possono essere di più) e attivazione con setsockopt(2)
struct can_filter rfilter[2];
rfilter[0].can_id = 0x100;
rfilter[0].can_mask = CAN_SFF_MASK;
rfilter[1].can_id = 0x200;
rfilter[1].can_mask = CAN_SFF_MASK;
setsockopt(sockfd, SOL_CAN_RAW, CAN_RAW_FILTER, &rfilter, sizeof(rfilter));

// 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? È identica alla canrecv.c dello scorso articolo però ha alcune linee aggiunte tra la bind(2) e la read(2) (ma non era obbligatorio metterle proprio li, eh!): si riempie un array di filtri (in questo caso ne ho messi 2 ma se ne possono aggiungere a piacere) e si passa l'array alla setsockopt(2). Ogni struttura can_filter dell'array contiene il can_id che si vuole far passare e la maschera da applicare al filtro (serve per evitare di usare anche i bit non significativi per il filtraggio): nell'esempio ho usato CAN_SFF_MASK, che è la maschera di default del Standard CAN, mentre per il Extended CAN  bisogna usare un altra maschera (ma questo lo vedremo nella terza parte del ciclo di articoli).

Per verificare il funzionamento dell'esempio basta compilare (ed eseguire) canrecvfilt.c e modificare cansend.c per inviare, ad esempio, un messaggio con can_id=0x300, e, magicamente, succederà che la canrecvfilt non riceve nulla! Poi si può provare a spedire messaggi con can_id=0x100 o 0x200 per verificare, invece, la corretta ricezione dei messaggi. È semplicissimo!

L'esempio che ho scritto è del tipo "classico" , ma sono possibili anche delle varianti: ad esempio si può filtrare al contrario, e cioè: "questi can_id non li voglio ricevere", ma questi sono dettagli che vi lascio il piacere di approfondire a parte.

Ok, la seconda parte del ciclo la chiudiamo qui. Nella terza parte (ebbene si! Ci sarà una terza parte, e l'ho deciso in corsa...) parleremo della gestione degli errori e, poi, di Extended CAN e CAN FD ovvero le due varianti del tipo "Classical": sono meno usate, ma hanno caratteristiche interessanti, e possono essere necessarie in alcuni progetti, quindi è meglio essere preparati all'uopo. Tenetevi pronti ma non siate impazienti: sicuramente il nuovo articolo non arriverà domani (e neanche dopodomani, ah ah ah).

Ciao, e al prossimo post!

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!