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:
- Non è immediatamente evidente dove e quando sia utile usarla.
- 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 fileint select(int nfds, // fd con il numero più alto (+1) nei 3 set sorvegliatifd_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 eccezionalistruct timeval *timeout); // tempo di bloccaggio del set durante la sorveglianza// FD_CLR() - rimuove il file descriptor fd dal set di descrittoriFD_CLR(int fd, // file descriptor da rimuovere dal setfd_set *set); // set di file descriptor// FD_SET() - cerca il file descriptor fd nel set di descrittoriFD_ISSET(int fd, // file descriptor da cercare nelfd_set *set); // set di file descriptor// FD_SET() - aggiunge il file descriptor fd al set di descrittoriFD_SET(int fd, // file descriptor da aggiungere al setfd_set *set); // set di file descriptor// FD_SET() - rimuove tutti i file descriptor dal set di descrittoriFD_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:
- Si definiscono, usando il tipo fd_set, i set di descrittori di file da sorvegliare.
- Si inizializzano i set usando la macro FD_ZERO (per azzerare il set) seguita da FD_SET (per aggiungere un file al set).
- Si prepara il timeout riempiendo una struct timeval (ovvero si scrivono i secondi e i microsecondi che compongono il nostro timeout)
- Si lancia la select(2) con gli argomenti preparati nei punti precedenti.
- 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 qualcosaFD_ZERO(&rfds); // azzero il setFD_SET(0, &rfds); // aggiungo stdin (il fd 0) al set// set del timeout a 5 seconditv.tv_sec = 5; // set di 5 sectv.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 fallitoperror("select()");}else if (retval) {/* retval > 0 indica che qualcuno ha scrittoqua si poteva anche usare questo test: FD_ISSET(0, &rfds) > 0 */printf("ci sono dati disponibili!\n");}else {// retval == 0 indica che è scaduto il timeoutprintf("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!
Nessun commento:
Posta un commento