Network programming: select, poll, epoll e kqueue
1. Introdução à Programação de Rede com I/O Multiplexado
1.1. O problema do bloqueio em sockets
Em modelos tradicionais de servidores de rede, cada conexão de cliente frequentemente exigia uma thread ou processo dedicado. O modelo fork() por conexão é simples, mas sofre com alto custo de criação de processos e overhead de contexto. O modelo thread por conexão reduz o custo, mas ainda enfrenta limitações de escalabilidade quando o número de conexões simultâneas cresce para milhares.
1.2. Conceito de I/O multiplexado
I/O multiplexado permite que um único processo monitore múltiplos descritores de arquivo (sockets, pipes, arquivos) e seja notificado quando um ou mais deles estiverem prontos para operações de leitura, escrita ou quando ocorrerem exceções. Isso elimina a necessidade de uma thread por conexão, reduzindo consumo de memória e overhead de contexto.
1.3. Visão geral das APIs
select(): a API mais antiga e portável (POSIX), mas limitada.poll(): evolução do select, sem limite fixo de descritores, mas ainda ineficiente.epoll: específica do Linux, escalável para milhares de conexões.kqueue: específica de sistemas BSD e macOS, similar ao epoll em eficiência.
2. select(): a abordagem clássica e portável
2.1. Assinatura e manipulação de fd_set
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
void FD_SET(int fd, fd_set *set);
void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);
2.2. Exemplo prático: servidor TCP com select()
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/select.h>
#define PORT 8080
#define MAX_CLIENTS 10
int main() {
int server_fd, client_fd, max_fd, activity, i;
struct sockaddr_in address;
int addrlen = sizeof(address);
int client_sockets[MAX_CLIENTS];
fd_set readfds;
// Criar socket do servidor
server_fd = socket(AF_INET, SOCK_STREAM, 0);
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);
bind(server_fd, (struct sockaddr *)&address, sizeof(address));
listen(server_fd, 3);
// Inicializar array de clientes
for (i = 0; i < MAX_CLIENTS; i++)
client_sockets[i] = 0;
while (1) {
FD_ZERO(&readfds);
FD_SET(server_fd, &readfds);
max_fd = server_fd;
for (i = 0; i < MAX_CLIENTS; i++) {
int sd = client_sockets[i];
if (sd > 0)
FD_SET(sd, &readfds);
if (sd > max_fd)
max_fd = sd;
}
activity = select(max_fd + 1, &readfds, NULL, NULL, NULL);
if (FD_ISSET(server_fd, &readfds)) {
client_fd = accept(server_fd, (struct sockaddr *)&address,
(socklen_t *)&addrlen);
for (i = 0; i < MAX_CLIENTS; i++) {
if (client_sockets[i] == 0) {
client_sockets[i] = client_fd;
break;
}
}
}
for (i = 0; i < MAX_CLIENTS; i++) {
int sd = client_sockets[i];
if (FD_ISSET(sd, &readfds)) {
char buffer[1024] = {0};
int valread = read(sd, buffer, 1024);
if (valread == 0) {
close(sd);
client_sockets[i] = 0;
} else {
printf("Cliente: %s\n", buffer);
send(sd, buffer, strlen(buffer), 0);
}
}
}
}
return 0;
}
2.3. Limitações do select
- Limite de FD_SETSIZE: normalmente 1024 descritores.
- Cópia de bitsets: o kernel precisa copiar os conjuntos de descritores entre espaço do usuário e kernel.
- Varredura linear O(n): mesmo quando poucos descritores estão ativos, o kernel verifica todos.
3. poll(): evolução do select
3.1. Estrutura struct pollfd
#include <poll.h>
struct pollfd {
int fd; /* descritor de arquivo */
short events; /* eventos de interesse */
short revents; /* eventos ocorridos (retornados) */
};
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
3.2. Exemplo prático: servidor TCP com poll()
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <poll.h>
#define PORT 8080
#define MAX_CLIENTS 10
int main() {
int server_fd, client_fd, i;
struct sockaddr_in address;
int addrlen = sizeof(address);
struct pollfd fds[MAX_CLIENTS + 1];
server_fd = socket(AF_INET, SOCK_STREAM, 0);
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);
bind(server_fd, (struct sockaddr *)&address, sizeof(address));
listen(server_fd, 3);
fds[0].fd = server_fd;
fds[0].events = POLLIN;
for (i = 1; i <= MAX_CLIENTS; i++)
fds[i].fd = -1;
while (1) {
int activity = poll(fds, MAX_CLIENTS + 1, -1);
if (fds[0].revents & POLLIN) {
client_fd = accept(server_fd, (struct sockaddr *)&address,
(socklen_t *)&addrlen);
for (i = 1; i <= MAX_CLIENTS; i++) {
if (fds[i].fd == -1) {
fds[i].fd = client_fd;
fds[i].events = POLLIN;
break;
}
}
}
for (i = 1; i <= MAX_CLIENTS; i++) {
if (fds[i].fd != -1 && (fds[i].revents & POLLIN)) {
char buffer[1024] = {0};
int valread = read(fds[i].fd, buffer, 1024);
if (valread == 0) {
close(fds[i].fd);
fds[i].fd = -1;
} else {
send(fds[i].fd, buffer, strlen(buffer), 0);
}
}
}
}
return 0;
}
3.3. Comparação com select()
- Sem limite fixo: pode monitorar qualquer número de descritores.
- Sem cópia de bitsets: usa array de struct, mais eficiente.
- Ainda varredura linear O(n): o kernel percorre todo o array de descritores.
4. epoll: escalabilidade no Linux
4.1. Criação e configuração
#include <sys/epoll.h>
int epoll_create1(int flags);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
struct epoll_event {
uint32_t events; /* Eventos (EPOLLIN, EPOLLOUT, etc.) */
epoll_data_t data; /* Dados associados (fd, ponteiro) */
};
4.2. Modos de operação
- Level-triggered (padrão): notifica enquanto o descritor estiver pronto.
- Edge-triggered: notifica apenas quando o estado muda (exige I/O não bloqueante).
4.3. Exemplo prático: servidor TCP com epoll
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#define PORT 8080
#define MAX_EVENTS 10
int set_nonblocking(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}
int main() {
int server_fd, epoll_fd, nfds, i;
struct sockaddr_in address;
int addrlen = sizeof(address);
struct epoll_event ev, events[MAX_EVENTS];
server_fd = socket(AF_INET, SOCK_STREAM, 0);
set_nonblocking(server_fd);
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);
bind(server_fd, (struct sockaddr *)&address, sizeof(address));
listen(server_fd, 3);
epoll_fd = epoll_create1(0);
ev.events = EPOLLIN;
ev.data.fd = server_fd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &ev);
while (1) {
nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
for (i = 0; i < nfds; i++) {
if (events[i].data.fd == server_fd) {
int client_fd = accept(server_fd,
(struct sockaddr *)&address, (socklen_t *)&addrlen);
set_nonblocking(client_fd);
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = client_fd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &ev);
} else {
char buffer[1024];
int client_fd = events[i].data.fd;
int valread = read(client_fd, buffer, sizeof(buffer));
if (valread <= 0) {
close(client_fd);
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, client_fd, NULL);
} else {
send(client_fd, buffer, valread, 0);
}
}
}
}
close(epoll_fd);
return 0;
}
4.4. Vantagens do epoll
- O(1): não depende do número total de descritores.
- Callback-driven: apenas descritores ativos são retornados.
- Edge-triggered: permite melhor desempenho com I/O não bloqueante.
5. kqueue: a alternativa BSD/macOS
5.1. Conceitos básicos
#include <sys/types.h>
#include <sys/event.h>
#include <sys/time.h>
int kqueue(void);
int kevent(int kq, const struct kevent *changelist, int nchanges,
struct kevent *eventlist, int nevents,
const struct timespec *timeout);
struct kevent {
uintptr_t ident; /* identificador (fd, pid, etc.) */
int16_t filter; /* filtro (EVFILT_READ, EVFILT_WRITE, etc.) */
uint16_t flags; /* flags (EV_ADD, EV_DELETE, EV_ONESHOT, etc.) */
uint32_t fflags; /* flags específicas do filtro */
intptr_t data; /* dados específicos do filtro */
void *udata; /* dados do usuário */
};
5.2. Exemplo prático: servidor TCP com kqueue
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/event.h>
#include <sys/time.h>
#define PORT 8080
#define MAX_EVENTS 10
int main() {
int server_fd, kq, client_fd, nev, i;
struct sockaddr_in address;
int addrlen = sizeof(address);
struct kevent change, events[MAX_EVENTS];
server_fd = socket(AF_INET, SOCK_STREAM, 0);
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);
bind(server_fd, (struct sockaddr *)&address, sizeof(address));
listen(server_fd, 3);
kq = kqueue();
EV_SET(&change, server_fd, EVFILT_READ, EV_ADD, 0, 0, NULL);
kevent(kq, &change, 1, NULL, 0, NULL);
while (1) {
nev = kevent(kq, NULL, 0, events, MAX_EVENTS, NULL);
for (i = 0; i < nev; i++) {
if (events[i].ident == server_fd) {
client_fd = accept(server_fd,
(struct sockaddr *)&address, (socklen_t *)&addrlen);
EV_SET(&change, client_fd, EVFILT_READ, EV_ADD, 0, 0, NULL);
kevent(kq, &change, 1, NULL, 0, NULL);
} else if (events[i].filter == EVFILT_READ) {
char buffer[1024];
int client_fd = events[i].ident;
int valread = read(client_fd, buffer, sizeof(buffer));
if (valread <= 0) {
close(client_fd);
} else {
send(client_fd, buffer, valread, 0);
}
}
}
}
close(kq);
return 0;
}
5.3. Diferenças chave do kqueue
- Suporte a múltiplos tipos de eventos: sockets, arquivos, sinais, processos, timers.
- API mais flexível: permite monitorar mudanças em diretórios, notificações de processo filho.
- Integração nativa: no kernel BSD, oferece baixa latência.
5.4. Comparação com epoll
- Semântica similar: ambos são O(1) e orientados a eventos.
- kqueue mais flexível: suporta mais tipos de filtros nativamente.
- epoll mais difundido: no ecossistema Linux, há mais ferramentas e documentação.
6. Estratégias Avançadas e Padrões de Uso
6.1. I/O não bloqueante com buffers parciais
Ao usar edge-triggered com epoll ou kqueue, é essencial configurar sockets como não bloqueantes e realizar loops de leitura/escrita até que EAGAIN ou EWOULDBLOCK seja retornado.
6.2. Timeouts e temporizadores
Com kqueue, é possível adicionar timers usando EVFILT_TIMER. Com epoll, timers podem ser implementados via timerfd_create() ou controlando o parâmetro timeout de epoll_wait().
6.3. Tratamento de erros
EPOLLHUPeEPOLLERR: indicam fechamento ou erro na conexão.EV_EOFno kqueue: indica fim do arquivo ou fechamento da conexão.
7. Portabilidade e Escolha da API
7.1. Estratégias de abstração
Bibliotecas como libevent e libuv abstraem as diferenças entre plataformas, permitindo usar select, poll, epoll ou kqueue transparentemente. Para código nativo, use #ifdef __linux__ para epoll e #ifdef __APPLE__ ou #ifdef __FreeBSD__ para kqueue.
7.2. Benchmarks conceituais
- select/poll: adequados para dezenas a centenas de conexões.
- epoll/kqueue: necessários para milhares de conexões simultâneas.
- Edge-triggered: oferece melhor desempenho, mas exige cuidado com I/O parcial.
7.3. Considerações finais
- Use
select()para scripts simples e portabilidade máxima. - Use
poll()quando precisar de mais descritores queFD_SETSIZE. - Use
epollno Linux ekqueueem BSD/macOS para aplicações de alta
performance.
A escolha da API correta depende do ambiente alvo, da escala do projeto e dos requisitos de latência. Para servidores modernos que precisam sustentar dezenas de milhares de conexões simultâneas, epoll (Linux) e kqueue (BSD/macOS) são as únicas opções viáveis. Em cenários de middleware ou ferramentas portáteis, investir em uma abstração como libevent ou libuv reduz significativamente o custo de manutenção.
8. Conclusão
A multiplexação de I/O é uma técnica fundamental para construir servidores de rede eficientes em C. Cada API apresentada — select, poll, epoll e kqueue — representa um degrau evolutivo na capacidade de gerenciar múltiplas conexões com cada vez menos sobrecarga.
selectoferece portabilidade máxima, mas sofre com limites rígidos e desempenho O(n).pollelimina o limite deFD_SETSIZE, porém ainda realiza varredura linear.epollekqueuerevolucionam o modelo com notificações orientadas a eventos e complexidade O(1), sendo indispensáveis para sistemas de alta concorrência.
Dominar essas APIs permite ao programador C construir desde pequenos utilitários de rede até servidores robustos que sustentam milhões de conexões simultâneas. O conhecimento de como e quando aplicar cada uma delas é uma habilidade essencial no desenvolvimento de sistemas de rede modernos.