Existem muitos detalhes "inferiores".
Primeiro, considere que o kernel possui uma lista de processos e, a qualquer momento, alguns desses processos estão em execução e outros não. O kernel permite a cada processo em execução uma fatia do tempo da CPU, depois o interrompe e passa para o próximo. Se não houver processos executáveis, o kernel provavelmente emitirá uma instrução como HLT para a CPU que suspenderá a CPU até que haja uma interrupção de hardware.
Em algum lugar do servidor, há uma chamada do sistema que diz "me dê algo para fazer". Existem duas grandes categorias de maneiras pelas quais isso pode ser feito. No caso do Apache, ele chama accept
um soquete que o Apache abriu anteriormente, provavelmente escutando na porta 80. O kernel mantém uma fila de tentativas de conexão e adiciona a essa fila toda vez que um TCP SYN é recebido. Como o kernel sabe que um TCP SYN foi recebido depende do driver do dispositivo; para muitas placas de rede, provavelmente há uma interrupção de hardware quando os dados da rede são recebidos.
accept
pede que o kernel retorne para mim a próxima iniciação de conexão. Se a fila não estava vazia, accept
apenas retorna imediatamente. Se a fila estiver vazia, o processo (Apache) será removido da lista de processos em execução. Quando uma conexão é iniciada posteriormente, o processo é retomado. Isso é chamado de "bloqueio", porque, para o processo que a chama, accept()
parece uma função que não retorna até ter um resultado, que pode levar algum tempo a partir de agora. Durante esse período, o processo não pode fazer mais nada.
Uma vez accept
retornado, o Apache sabe que alguém está tentando iniciar uma conexão. Em seguida, chama o fork para dividir o processo Apache em dois processos idênticos. Um desses processos continua a processar a solicitação HTTP, o outro chama accept
novamente para obter a próxima conexão. Portanto, sempre há um processo mestre que não faz nada além de chamar accept
e gerar subprocessos e, em seguida, há um subprocesso para cada solicitação.
Isso é uma simplificação: é possível fazer isso com threads em vez de processos, e também é possível com fork
antecedência, para que haja um processo de trabalho pronto quando uma solicitação for recebida, reduzindo assim a sobrecarga da inicialização. Dependendo de como o Apache estiver configurado, ele poderá executar uma dessas ações.
Essa é a primeira categoria ampla de como fazê-lo, e é chamado de bloqueio IO porque o sistema chama como accept
e read
e write
que operam em sockets irá suspender o processo até que eles têm algo a voltar.
A outra maneira ampla de fazer isso é chamada de E / S não-bloqueadora ou baseada em eventos ou assíncrona . Isso é implementado com chamadas do sistema como select
ou epoll
. Cada um deles faz a mesma coisa: você fornece a eles uma lista de soquetes (ou, em geral, descritores de arquivos) e o que você quer fazer com eles, e o kernel bloqueia até que esteja pronto para fazer uma dessas coisas.
Com este modelo, você pode dizer ao kernel (com epoll
): "Diga-me quando houver uma nova conexão na porta 80 ou novos dados para ler em qualquer uma dessas 9471 outras conexões que eu abri". epoll
bloqueia até que uma dessas coisas esteja pronta, então você faz. Então você repete. Chama sistema como accept
e read
e write
nunca mais bloco, em parte porque sempre que você chamá-los, epoll
apenas lhe disse que eles estão prontos para que não haveria nenhuma razão para bloquear, e também porque quando você abre o soquete ou o arquivo que você especificar que você quer que eles no modo sem bloqueio, para que essas chamadas falhem em EWOULDBLOCK
vez de bloquear.
A vantagem deste modelo é que você precisa de apenas um processo. Isso significa que você não precisa alocar estruturas de pilha e kernel para cada solicitação. O Nginx e o HAProxy usam esse modelo, e é uma grande razão pela qual eles podem lidar com muito mais conexões que o Apache em hardware semelhante.