I/O bloccante e non bloccante
Scopra perché i cicli di eventi sono importanti.
I/O bloccante e non bloccante è una lezione C Academy gratuita su CoddyKit. Questa è la lezione 1 di 4. Puoi leggere la lezione completa qui gratuitamente — poi esercitati direttamente nel browser con un editor di codice integrato e un tutor IA disponibile 24/7. Fa parte del percorso di apprendimento C Academy, e i tuoi progressi si sincronizzano tra il web e l'app CoddyKit. Il corso C Academy include 4 lezioni in totale.
Cosa significa blocking
Una chiamata di sistema blocking sospende il thread chiamante finché l'operazione non può procedere. Quando si chiama recv() su un socket senza dati, il kernel mette il thread in attesa finché non arrivano i byte.
È semplice da comprendere: una connessione, un thread, codice lineare. Il costo emerge quando è necessario servire migliaia di client contemporaneamente.
ssize_t n = recv(fd, buf, sizeof buf, 0);
/* thread sleeps here until data or error */
if (n > 0) handle(buf, n);Il problema della scalabilità
Con l'I/O bloccante, un client che rimane in attesa blocca l'intero thread. La soluzione classica è un thread (o processo) per connessione.
Funziona fino a un certo punto, ma 10.000 thread significano 10.000 stack, un intenso context switching e un notevole overhead dello scheduler. Questo è il famoso problema C10k, che ha spinto i server verso architetture event-driven.
Modalità non bloccante
Un socket non-blocking non si mette mai in attesa. Se una chiamata non può completarsi immediatamente, restituisce subito -1 e imposta errno su EAGAIN o EWOULDBLOCK.
Ora il codice deve occuparsi di riprovare in seguito. In questo modo un singolo thread può gestire molti socket senza rimanere mai bloccato su uno di essi.
ssize_t n = recv(fd, buf, sizeof buf, 0);
if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
/* no data right now, try again later */
}Impostare O_NONBLOCK con fcntl
Si passa un descrittore alla modalità non bloccante aggiungendo il flag O_NONBLOCK con fcntl(). Occorre sempre leggere prima i flag correnti, quindi applicare un OR con il bit, per non sovrascrivere le altre impostazioni.
Lo stesso helper viene utilizzato per socket in ascolto, socket client accettati e pipe.
int set_nonblocking(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
if (flags == -1) return -1;
return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}Gestire le letture parziali
L'I/O non bloccante rende normali le operazioni parziali. Una chiamata recv() può restituire meno byte di quelli richiesti e send() può accettare solo una parte del buffer.
È necessario tenere traccia di quanti dati sono stati inviati o ricevuti e riprendere da lì. Non si deve mai presumere che una singola chiamata trasferisca tutti i byte.
size_t sent = 0;
while (sent < len) {
ssize_t w = send(fd, buf + sent, len - sent, 0);
if (w < 0) { if (errno == EAGAIN) break; else return -1; }
sent += w;
}Il busy-waiting è sbagliato
Il modo ingenuo di usare i socket non bloccanti consiste nel passare in ciclo su tutti e riprovare continuamente. Questo busy-wait consuma il 100% della CPU anche quando non accade nulla.
Ciò che serve davvero è chiedere al kernel: "indica quali descrittori sono pronti e lasciami dormire fino a quel momento". È esattamente ciò che offre la notifica di disponibilità.
Notifica di disponibilità
Il multiplexing dell'I/O consente a un thread di attendere contemporaneamente molti descrittori e di risvegliarsi solo quando almeno uno è pronto. È il kernel a occuparsi del monitoraggio.
Le interfacce classiche sono select() e poll(). Funzionano, ma a ogni chiamata analizzano nuovamente tutti i descrittori, diventando costose su larga scala.
fd_set rfds;
FD_ZERO(&rfds);
FD_SET(fd, &rfds);
select(fd + 1, &rfds, NULL, NULL, NULL);Perché select e poll non scalano
Sia select() sia poll() sono O(n): a ogni chiamata si passa al kernel l'intero insieme di descrittori, che li analizza tutti; poi è necessario analizzarli nuovamente per trovare quelli pronti.
select() è inoltre limitato da FD_SETSIZE (spesso 1024). Con migliaia di connessioni, questo overhead diventa predominante.
Entra in scena epoll
epoll è la soluzione scalabile di Linux. Si registra una volta l'interesse per un descrittore e il kernel mantiene una struttura dati interna che ne monitora la disponibilità.
Ogni attesa restituisce solo i descrittori effettivamente pronti, quindi il costo cresce in base alle connessioni attive, non a quelle totali. Il costo è quindi approssimativamente O(1) per evento pronto.
int epfd = epoll_create1(0);
/* register fds once, then wait for ready events */Socket non bloccanti ed epoll
epoll e i socket non bloccanti lavorano insieme. epoll indica che un descrittore è pronto; le chiamate non bloccanti consentono di svuotarlo senza mettersi mai in attesa.
È opportuno impostare sempre O_NONBLOCK sui socket passati a epoll. In caso contrario, un risveglio spurio o una lettura parziale potrebbero bloccare il thread del singolo event loop.
set_nonblocking(conn_fd);
struct epoll_event ev = { .events = EPOLLIN, .data.fd = conn_fd };
epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, &ev);Il modello mentale
Immagini il server come un ciclo: si blocca in epoll_wait(), riceve un piccolo elenco di descrittori pronti, esegue operazioni non bloccanti su ciascuno e ripete.
Il thread dorme quando non c'è attività e si risveglia solo per svolgere lavoro effettivo. Ora un solo thread può servire in modo efficiente decine di migliaia di connessioni.
Verifica rapida
Verifichi la propria comprensione dei socket non bloccanti.
Riepilogo
L'I/O bloccante è semplice, ma impegna un thread per connessione e non è adatto a grandi quantità di connessioni. L'I/O non bloccante restituisce immediatamente EAGAIN invece di mettersi in attesa.
Interrogare i socket in un ciclo stretto spreca CPU, quindi si utilizza la notifica di disponibilità. select/poll sono O(n); epoll scala fino a molte migliaia di connessioni. Ora configureremo epoll.
Domande Frequenti
La lezione «I/O bloccante e non bloccante» è gratuita?
Sì — il testo completo di «I/O bloccante e non bloccante» è gratuito qui sul web. Per esercitarvi in modo interattivo (un editor di codice integrato e un tutor IA 24/7) e sbloccare il resto del corso C Academy, passa a CoddyKit PRO. Il corso C Academy include 4 lezioni in totale.
Cosa imparerò in «I/O bloccante e non bloccante»?
Scopra perché i cicli di eventi sono importanti. Eserciti C Academy con codice pratico che esegui direttamente nel browser, e un tutor IA 24/7 risponde alle tue domande mentre lavori sulla lezione.
Ho bisogno di esperienza per iniziare C Academy?
Non è richiesta alcuna esperienza precedente. C Academy su CoddyKit è strutturato per principianti e studenti avanzati, quindi puoi iniziare da qui o dall'inizio e procedere al tuo ritmo. Questa è la lezione 1 di 4.
Quanto tempo richiede la lezione «I/O bloccante e non bloccante»?
La maggior parte delle lezioni CoddyKit richiede circa 5–10 minuti. Ogni lezione è breve e interattiva, quindi fai progressi costanti e riprendi esattamente da dove hai lasciato su web e app.
Posso scrivere ed eseguire codice in questa lezione C Academy?
Sì. Ogni lezione C Academy include un editor di codice integrato, quindi scrivi ed esegui codice reale direttamente nel tuo browser e ricevi feedback istantaneo dall'IA — nessuna configurazione locale necessaria.
Tutte le lezioni di questo corso
- I/O bloccante e non bloccante
- Configurare epoll
- Il ciclo di eventi
- Un semplice server echo