Segnala che è pronto per ritorno.
select
attende eventi che in genere sono al di fuori del controllo del programma. In sostanza, chiamando select
, il tuo programma dice "Non ho niente da fare fino a..., per favore sospendi il mio processo".
La condizione che specifichi è un insieme di eventi, ognuno dei quali ti sveglierà.
Ad esempio, se stai scaricando qualcosa, il tuo loop dovrebbe attendere l'arrivo di nuovi dati, un timeout se il trasferimento è bloccato o l'utente da interrompere, che è esattamente ciò che select
fa.
Quando hai più download, i dati che arrivano su una qualsiasi delle connessioni attivano l'attività nel tuo programma (devi scrivere i dati su disco), quindi dovresti fornire un elenco di tutte le connessioni di download a select
nell'elenco dei descrittori di file da controllare per "lettura".
Quando carichi i dati da qualche parte nello stesso momento, usi nuovamente select
per vedere se la connessione attualmente accetta dati. Se l'altro lato è in dialup, riconoscerà i dati solo lentamente, quindi il tuo buffer di invio locale è sempre pieno e qualsiasi tentativo di scrivere più dati si bloccherebbe fino a quando lo spazio del buffer non sarà disponibile o fallirà. Passando il descrittore di file che stiamo inviando a a select
come descrittore di "scrittura", riceviamo una notifica non appena lo spazio del buffer è disponibile per l'invio.
L'idea generale è che il tuo programma diventi guidato dagli eventi , ovvero reagisce a eventi esterni da un ciclo di messaggi comune anziché eseguire operazioni sequenziali. Dici al kernel "questo è l'insieme di eventi per i quali voglio fare qualcosa", e il kernel ti dà un insieme di eventi che si sono verificati. È abbastanza comune che due eventi si verifichino contemporaneamente; ad esempio, un riconoscimento TCP è stato incluso in un pacchetto di dati, questo può rendere lo stesso fd sia leggibile (i dati sono disponibili) che scrivibile (i dati riconosciuti sono stati rimossi dal buffer di invio), quindi dovresti essere pronto a gestire tutti gli eventi prima di chiamare select
di nuovo.
Uno dei punti migliori è quel select
fondamentalmente ti dà una promessa che un'invocazione di read
o write
non bloccherà, senza dare alcuna garanzia sulla chiamata stessa. Ad esempio, se è disponibile un byte di spazio nel buffer, puoi tentare di scrivere 10 byte e il kernel tornerà e dirà "Ho scritto 1 byte", quindi dovresti essere pronto a gestire anche questo caso. Un approccio tipico consiste nell'avere un buffer "dati da scrivere su questo fd" e fintanto che non è vuoto, fd viene aggiunto al set di scrittura e l'evento "scrivibile" viene gestito tentando di scrivere tutto i dati attualmente nel buffer. Se il buffer è vuoto in seguito, va bene, in caso contrario, attendi di nuovo su "scrivibile".
Il set "eccezionale" viene utilizzato raramente:viene utilizzato per i protocolli che hanno dati fuori banda in cui è possibile bloccare il trasferimento dei dati, mentre altri dati devono essere trasmessi. Se il tuo programma attualmente non può accettare dati da un descrittore di file "leggibile" (ad esempio, stai scaricando e il disco è pieno), non vuoi includere il descrittore nel set "leggibile", perché non puoi gestire l'evento e select
ritornerebbe immediatamente se invocato di nuovo. Se il destinatario include fd nel set "eccezionale" e il mittente chiede al suo stack IP di inviare un pacchetto con dati "urgenti", il destinatario viene quindi svegliato e può decidere di scartare i dati non gestiti e risincronizzarsi con il mittente . Il telnet
il protocollo lo utilizza, ad esempio, per la gestione di Ctrl-C. A meno che tu non stia progettando un protocollo che richieda tale funzionalità, puoi facilmente ometterlo senza alcun danno.
Esempio di codice obbligatorio:
#include <sys/types.h>
#include <sys/select.h>
#include <unistd.h>
#include <stdbool.h>
static inline int max(int lhs, int rhs) {
if(lhs > rhs)
return lhs;
else
return rhs;
}
void copy(int from, int to) {
char buffer[10];
int readp = 0;
int writep = 0;
bool eof = false;
for(;;) {
fd_set readfds, writefds;
FD_ZERO(&readfds);
FD_ZERO(&writefds);
int ravail, wavail;
if(readp < writep) {
ravail = writep - readp - 1;
wavail = sizeof buffer - writep;
}
else {
ravail = sizeof buffer - readp;
wavail = readp - writep;
}
if(!eof && ravail)
FD_SET(from, &readfds);
if(wavail)
FD_SET(to, &writefds);
else if(eof)
break;
int rc = select(max(from,to)+1, &readfds, &writefds, NULL, NULL);
if(rc == -1)
break;
if(FD_ISSET(from, &readfds))
{
ssize_t nread = read(from, &buffer[readp], ravail);
if(nread < 1)
eof = true;
readp = readp + nread;
}
if(FD_ISSET(to, &writefds))
{
ssize_t nwritten = write(to, &buffer[writep], wavail);
if(nwritten < 1)
break;
writep = writep + nwritten;
}
if(readp == sizeof buffer && writep != 0)
readp = 0;
if(writep == sizeof buffer)
writep = 0;
}
}
Tentiamo di leggere se abbiamo spazio nel buffer disponibile e non c'è stata alcuna fine del file o errore sul lato di lettura, e tentiamo di scrivere se abbiamo dati nel buffer; se viene raggiunta la fine del file e il buffer è vuoto, allora abbiamo finito.
Questo codice si comporterà chiaramente in modo non ottimale (è un codice di esempio), ma dovresti essere in grado di vedere che è accettabile che il kernel faccia meno di quanto abbiamo chiesto sia in lettura che in scrittura, nel qual caso torniamo indietro e diciamo "ogni volta che sei pronto", e che non leggiamo o scriviamo mai senza chiederci se verrà bloccato.
Dalla stessa pagina man:
All'uscita, gli insiemi vengono modificati sul posto per indicare quali descrittori di file hanno effettivamente cambiato stato.
Quindi usa FD_ISSET()
sui set passati da selezionare per determinare quali FD sono diventati pronti.