Ci sono molti dettagli "inferiori".
Innanzitutto, considera che il kernel ha un elenco di processi e, in un dato momento, alcuni di questi processi sono in esecuzione e altri no. Il kernel consente a ciascun processo in esecuzione una parte del tempo della CPU, quindi lo interrompe e passa al successivo. Se non ci sono processi eseguibili, il kernel probabilmente invierà un'istruzione come HLT alla CPU che sospende la CPU fino a quando non si verifica un interrupt di processo.
Da qualche parte nel server c'è una chiamata di sistema che dice "dammi qualcosa da fare". Esistono due grandi categorie di modi in cui ciò può essere fatto. Nel caso di Apache, chiama accept
un socket che Apache ha precedentemente aperto, probabilmente in ascolto sulla porta 80. Il kernel mantiene una coda di tentativi di connessione e si aggiunge a quella coda ogni volta che viene ricevuto un SYN TCP . Il modo in cui il kernel sa che è stato ricevuto un SYN TCP dipende dal driver del dispositivo; per molte schede di rete è probabilmente presente un interrupt di processo alla ricezione dei dati di rete.
accept
chiede al kernel di restituirmi la prossima iniziazione della connessione. Se la coda non era vuota, accept
ritorna immediatamente. Se la coda è vuota, il processo (Apache) viene rimosso dall'elenco dei processi in esecuzione. Quando in seguito viene avviata una connessione, il processo viene ripreso. Questo si chiama "blocco", perché al processo che lo chiama, accept()
sembra una funzione che non ritorna fino a quando non ha un risultato, che potrebbe essere tra qualche tempo. Durante quel periodo il processo non può fare nient'altro.
Una volta accept
restituito, Apache sa che qualcuno sta tentando di avviare una connessione. Quindi chiama fork per dividere il processo Apache in due processi identici. Uno di questi processi continua a elaborare la richiesta HTTP, l'altro chiama accept
nuovamente per ottenere la connessione successiva. Pertanto, esiste sempre un processo principale che non fa altro che chiamare accept
e generare sottoprocessi, quindi esiste un processo secondario per ogni richiesta.
Questa è una semplificazione: è possibile farlo con i thread anziché con i processi, ed è anche possibile farlo in fork
anticipo, quindi c'è un processo di lavoro pronto per iniziare quando viene ricevuta una richiesta, riducendo così il sovraccarico di avvio. A seconda di come è configurato Apache, può fare una di queste cose.
Questa è la prima vasta categoria di come farlo, e si chiama blocco IO perché il sistema chiama come accept
e read
e write
che operano su socket sospenderà il processo fino a quando non hanno qualcosa da restituire.
L'altro modo ampio per farlo è chiamato IO non bloccante o basato su eventi o asincrono . Questo è implementato con chiamate di sistema come select
o epoll
. Ognuno di loro fa la stessa cosa: tu dai loro un elenco di socket (o in generale, descrittori di file) e cosa vuoi fare con loro, e il kernel si blocca fino a quando non è pronto a fare una di quelle cose.
Con questo modello, potresti dire al kernel (con epoll
), "Dimmi quando c'è una nuova connessione sulla porta 80 o nuovi dati da leggere su una di queste 9471 altre connessioni che ho aperto". epoll
si blocca fino a quando una di queste cose è pronta, quindi lo fai. Quindi ripeti. Chiamate di sistema come accept
e read
e write
non a blocchi, in parte perché ogni volta che li chiami, epoll
appena detto che sono pronti così non ci sarebbe alcun motivo per bloccare, e anche perché quando si apre la presa o il file si specifica che le si vuole in modalità non bloccante, quindi quelle chiamate falliranno EWOULDBLOCK
invece di bloccare.
Il vantaggio di questo modello è che è necessario un solo processo. Ciò significa che non è necessario allocare uno stack e le strutture del kernel per ogni richiesta. Nginx e HAProxy usano questo modello, ed è un grande motivo per cui possono gestire così tante più connessioni di Apache su hardware simile.