Ao projetar aplicativos de rede de alto desempenho com soquetes sem bloqueio, é importante decidir qual método de monitoramento de eventos de rede usaremos. Existem vários deles, e cada um é bom e ruim à sua maneira. A escolha do método certo pode ser fundamental para a arquitetura do seu aplicativo.
Neste artigo, consideraremos:
- selecione ()
- sondagem ()
- epoll ()
- libevent
Usando select ()
O antigo, comprovado ao longo dos anos, o trabalhador esforçado select () foi criado naqueles dias em que “soquetes” eram chamados “
soquetes de Berkeley ”. Esse método não foi incluído na primeira especificação desses soquetes de Berkeley, pois naquela época ainda não havia conceito de E / S sem bloqueio. Mas em algum momento nos anos 80 ela apareceu e, com ela, selecione (). Desde então, nada mudou significativamente em sua interface.
Para usar select (), o desenvolvedor precisa inicializar e preencher várias estruturas fd_set com descritores e eventos que precisam ser monitorados e, em seguida, chamar select (). Um código típico se parece com isso:
fd_set fd_in, fd_out; struct timeval tv;
Quando select () foi projetado, ninguém esperava que, no futuro, precisaríamos escrever aplicativos multiencadeados que atendessem a milhares de conexões. O Select () possui várias desvantagens significativas que o tornam pouco adequado para trabalhar em tais sistemas. Os principais são:
- O select seleciona as estruturas fd_sets transmitidas a ele, para que nenhuma delas possa ser reutilizada. Mesmo que você não precise alterar nada (por exemplo, depois de receber um dado, você deseja obter mais), as estruturas fd_sets precisarão ser reinicializadas. Bem, ou copie de um backup salvo anteriormente usando FD_COPY. E isso terá que ser feito repetidamente, antes de cada chamada de seleção.
- Para descobrir exatamente qual descritor gerou o evento, você deve pesquisar manualmente todos eles com FD_ISSET. Quando você monitora 2000 descritores, e o evento aconteceu apenas para um deles (que, de acordo com a lei da maldade, será o último da lista) - você desperdiçará muitos recursos do processador.
- Acabei de mencionar 2000 descritores? Fiquei empolgado com isso. O select não suporta tanto. Bem, pelo menos no Linux comum, com o kernel usual. O número máximo de descritores observados simultaneamente é limitado pela constante FD_SETSIZE, que é rigidamente igual a 1024 no Linux. Alguns sistemas operacionais permitem implementar um hack substituindo o valor FD_SETSIZE antes de incluir o arquivo de cabeçalho sys / select.h, mas esse hack não faz parte de algum padrão comum. O mesmo Linux irá ignorá-lo.
- Você não pode trabalhar com descritores de um conjunto observável de outro encadeamento. Imagine um thread executando o código acima. Por isso, iniciou e aguarda eventos em seu select (). Agora imagine que você tem outro encadeamento que monitora a carga geral no sistema e agora ele decidiu que os dados do soquete sock1 não chegaram por muito tempo e que estava na hora de interromper a conexão. Como esse soquete pode ser reutilizado para atender novos clientes, seria bom fechá-lo corretamente. Mas o primeiro tópico é observar esse descritor agora. O que acontecerá se encerrarmos tudo da mesma maneira? Ah, a documentação tem uma resposta para essa pergunta e você não vai gostar: "Se o identificador observado com select () for fechado por outro thread, você terá um comportamento indefinido".
- O mesmo problema aparece ao tentar enviar alguns dados via sock1. Não enviaremos nada até que o select termine seu trabalho.
- A escolha dos eventos que podemos monitorar é bastante limitada. Por exemplo, para determinar se o soquete remoto foi fechado, primeiro você deve monitorar os eventos de chegada de dados e, em segundo lugar, tentar ler esses dados (read retornará 0 para o soquete fechado). Ainda pode ser considerado aceitável ao ler dados de um soquete (leia 0 - o soquete está fechado), mas e se a nossa tarefa atual no momento é enviar dados para esse soquete e não precisarmos ler dados dele agora?
- select coloca um encargo desnecessário para você calcular o “maior descritor” e passá-lo como um parâmetro separado
Obviamente, todas as opções acima não são novidade. Os desenvolvedores de sistemas operacionais estão cientes desses problemas e muitos deles foram levados em consideração ao projetar o método de pesquisa. Nesse ponto, você pode perguntar: por que estamos estudando história antiga agora, e há alguma razão hoje para usar o seleto antigo? Sim, existem duas razões. Não é o fato de que eles serão úteis para você algum dia, mas por que não descobrir sobre eles.
A primeira razão é a portabilidade. O select () está conosco há um milhão de anos. Não importa o que a selva das plataformas de hardware e software lhe traga, se houver uma rede lá, haverá seleção. Pode não haver outros métodos, mas o select será quase garantido. E não pense que agora estou caindo em senilidade senil e lembre-se de algo como cartões perfurados e ENIAC, não. Não existe um método de pesquisa mais moderno
, por exemplo, no Windows XP . Mas selecione é.
O segundo motivo é mais exótico e está relacionado ao fato de que o select pode (teoricamente) funcionar com tempos limite da ordem de um nanossegundo (se o hardware permitir), enquanto a pesquisa e epoll suportam apenas a precisão de milissegundos. Isso não deve desempenhar um papel especial em desktops comuns (ou mesmo servidores), onde você ainda não possui um cronômetro de precisão de nanossegundos de hardware. Mas ainda no mundo existem sistemas em tempo real que possuem esses temporizadores. Então, eu imploro, quando você escreve o firmware de um reator nuclear ou foguete - não tenha preguiça de medir o tempo em nanossegundos. Você sabe, eu quero viver.
O caso descrito acima é provavelmente o único no qual você realmente não tem escolha do que usar (somente o seleto é adequado). No entanto, se você estiver escrevendo um aplicativo regular para trabalhar em hardware comum e operar com um número adequado de soquetes (dezenas, centenas - e não mais), a diferença de pesquisa e desempenho selecionado não será perceptível; portanto, a escolha será baseada em outros fatores.
Polling com poll ()
poll é um método mais novo de soquetes de pesquisa, criado depois que as pessoas começaram a tentar criar serviços de rede grandes e com muita carga. Ele foi projetado muito melhor e não sofre com a maioria das desvantagens do método de seleção. Na maioria dos casos, ao escrever aplicativos modernos, você escolhe entre usar poll e epoll / libevent.
Para usar a enquete, um desenvolvedor precisa inicializar membros da estrutura pollfd com descritores e eventos observáveis e depois chamar poll ().
Um código típico se parece com isso:
A enquete foi criada para resolver os problemas do método select, vamos ver como ficou:
- Não há limite para o número de descritores observados; mais de 1024 podem ser monitorados
- A estrutura pollfd não é modificada, o que torna possível reutilizá-la entre as chamadas para poll () - você só precisa redefinir o campo revents.
- Eventos observados são melhor estruturados. Por exemplo, você pode determinar se um cliente remoto está desconectado sem precisar ler dados do soquete.
Já falamos sobre as deficiências do método de pesquisa: ele não está disponível em algumas plataformas, como o Windows XP. Desde o Vista, ele existe, mas é chamado WSAPoll. O protótipo é o mesmo; portanto, para código independente de plataforma, você pode escrever uma substituição, como:
#if defined (WIN32) static inline int poll( struct pollfd *pfd, int nfds, int timeout) { return WSAPoll ( pfd, nfds, timeout ); } #endif
Bem, a precisão dos tempos limite é de 1 ms, o que não será suficiente muito raramente. No entanto, a pesquisa tem outras desvantagens:
- Assim como no uso de select, é impossível determinar quais descritores geraram os eventos sem uma passagem completa por todas as estruturas observadas e verificar os campos de retorno neles. Pior ainda, também é implementado no kernel do sistema operacional.
- Assim como no select, não há como alterar dinamicamente o conjunto de eventos observado
No entanto, todos os itens acima podem ser considerados relativamente insignificantes para a maioria dos aplicativos clientes. A exceção é provavelmente apenas os protocolos p2p, em que cada um dos clientes pode ser associado a milhares de outros. Esses problemas podem ser ignorados mesmo pela maioria dos aplicativos de servidor. Portanto, a pesquisa deve ser sua preferência padrão em relação à seleção, a menos que um dos dois motivos acima o limite.
Olhando para o futuro, direi que a pesquisa é preferível, mesmo em comparação com o epoll mais moderno (discutido abaixo) nos seguintes casos:
- Você deseja escrever código de plataforma cruzada (epoll é apenas no Linux)
- Você não precisa monitorar mais de 1000 soquetes (o epoll não fornecerá nada de significativo neste caso)
- Você precisa monitorar mais de 1000 soquetes, mas o tempo de conexão com cada um deles é muito pequeno (nesses casos, o desempenho da pesquisa e do epoll será muito próximo - o ganho da espera por menos eventos no epoll será riscado pela sobrecarga de adicioná-los / removê-los)
- Seu aplicativo não foi projetado para alterar eventos de um segmento enquanto outro os espera (ou você não precisa dele)
Sondando epoll ()
epoll é o método mais novo e melhor de aguardar eventos no Linux (e somente no Linux). Bem, não é que o "mais novo" seja direto - ele está no centro desde 2002. Difere da pesquisa e seleciona que fornece uma API para adicionar / remover / modificar a lista de descritores e eventos observados.
O uso do epoll requer preparativos um pouco mais detalhados. O desenvolvedor deve:
- Crie um descritor de epoll chamando epoll_create
- Inicialize a estrutura epoll_event com os eventos e ponteiros necessários para os contextos de conexão. O "contexto" aqui pode ser qualquer coisa, epoll apenas passa esse valor nos eventos retornados
- Chame epoll_ctl (... EPOLL_CTL_ADD) para adicionar um identificador à lista de observáveis
- Ligue para epoll_wait () para aguardar eventos (indicamos exatamente quantos eventos queremos receber por vez, por exemplo, 20). Diferentemente dos métodos anteriores, obtemos esses eventos separadamente, e não nas propriedades das estruturas de entrada. Se observarmos 200 descritores e 5 deles receberem novos dados - epoll_wait retornará apenas 5 eventos. Se ocorrerem 50 eventos, os 20 primeiros serão devolvidos para nós e os 30 restantes aguardarão a próxima ligação, eles não serão perdidos
- Processar eventos recebidos. Será um processamento relativamente rápido, porque não olhamos para os descritores em que nada aconteceu
Um código típico se parece com isso:
Vamos começar com as falhas do epoll - elas são óbvias no código. Este método é mais difícil de usar, você precisa escrever mais código, faz mais chamadas do sistema.
As vantagens também são evidentes:
- epoll retorna uma lista apenas dos descritores para os quais os eventos observados realmente ocorreram. Você não precisa procurar por milhares de estruturas em busca daquela, possivelmente aquela em que o evento esperado funcionou.
- Você pode associar algum contexto significativo a cada evento observado. No exemplo acima, usamos um ponteiro para um objeto da classe de conexão para isso - isso nos salvou outra pesquisa em potencial por uma matriz de conexões.
- Você pode adicionar ou remover soquetes da lista a qualquer momento. Você pode até modificar eventos observados. Tudo funcionará corretamente, isso é oficialmente suportado e documentado.
- Você pode iniciar vários encadeamentos aguardando eventos da mesma fila usando epoll_wait. Algo que de forma alguma pode ser feito com select / poll.
Mas você também precisa se lembrar que epoll não é "uma enquete melhorada". Tem desvantagens em comparação com a pesquisa:
- Alterar os sinalizadores de evento (por exemplo, alternar de READ para WRITE) requer uma chamada extra do sistema epoll_ctl, enquanto que para a pesquisa você apenas altera a máscara de bits (completamente no modo de usuário). A troca de 5.000 soquetes de leitura para gravação exigirá 5.000 chamadas de sistema e alternâncias de contexto para epoll, enquanto para pesquisas será uma operação de bit trivial em um loop.
- Para cada nova conexão, é necessário chamar accept () e epoll_ctl () são duas chamadas do sistema. Se você usar a enquete, haverá apenas uma chamada. Com uma vida útil de conexão muito curta, isso pode fazer a diferença.
- O epoll está disponível apenas no Linux. Outros sistemas operacionais possuem mecanismos semelhantes, mas ainda não completamente idênticos. Você não poderá escrever código com o epoll para que ele construa e funcione, por exemplo, no FreeBSD.
- Escrever código paralelo altamente carregado é difícil. Muitos aplicativos não precisam de uma abordagem tão fundamental, pois seu nível de carga é facilmente processado usando métodos mais simples.
Portanto, o epoll deve ser usado apenas quando tudo o que se segue for verdadeiro:
- Seu aplicativo usa um pool de threads para manipular conexões de rede. O ganho do epoll em um aplicativo de thread único será desprezível e você não deve se preocupar com a implementação.
- Você espera um número relativamente grande de conexões (de 1000 e acima). Em um pequeno número de soquetes observados, o epoll não dará um ganho de desempenho e, se houver literalmente alguns soquetes, ele pode até diminuir a velocidade.
- Suas conexões vivem relativamente longas. Em uma situação em que uma nova conexão transfere apenas alguns bytes de dados e fecha bem ali - a pesquisa funcionará mais rápido, porque precisará fazer menos chamadas do sistema para processá-la.
- Você pretende executar seu código no Linux e somente no Linux.
Se um ou mais dos itens falharem, considere usar poll ou libevent.
libevent
libevent é uma biblioteca que agrupa os métodos de pesquisa listados neste artigo (assim como alguns outros) em uma API unificada. A vantagem aqui é que, depois de escrever o código, você poderá criar e executá-lo em diferentes sistemas operacionais. No entanto, é importante entender que o libevent é apenas um invólucro, dentro do qual todos os métodos acima funcionam, com todas as suas vantagens e desvantagens. O libevent não forçará o select a ouvir mais de 1024 soquetes e o epoll não modificará a lista de eventos sem uma chamada adicional do sistema. Portanto, conhecer as tecnologias subjacentes ainda é importante.
A necessidade de oferecer suporte a diferentes métodos de pesquisa torna a API da biblioteca libevent mais complexa. Ainda assim, seu uso é mais fácil do que escrever manualmente dois mecanismos diferentes de seleção de eventos para, por exemplo, Linux e FreeBSD (usando epoll e kqueue).
Considere usar libevent ao combinar dois eventos:
- Você analisou os métodos de seleção e pesquisa e eles definitivamente não funcionaram para você.
- Você precisa suportar vários sistemas operacionais