Comparing I/O Multiplexing Techniques: select, poll, and epoll

Function Prototypes

select

#include <sys/select.h>
int select(int max_fds, fd_set *read_set, fd_set *write_set, 
           fd_set *except_set, struct timeval *timeout);

poll

#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

epoll

#include <sys/epoll.h>
int epoll_create(int size);
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);

Usage Characteristics

select employs three fd_set structures to communicate read, write, and exception events between user and kernel spaces. This design limits the range of supported events and requires reinitialization before each call.

poll separates file descriptor monitoring from event reporting through distinct structures. It supports more event types and doesn't require reconfiguration between calls.

epoll utilizes a kernel-managed event table created via epoll_create. Events are managed through epoll_ctl, while epoll_wait retrieves only active events from this table.

System Limitations

select uses fixed-size arrays, typically supporting up to 1024 file descriptors on 32-bit systems.

poll and epoll support up to the system's maximum open file descriptor limit (typically 65535).

Performance Characteristics

select and poll require copying descriptor sets between user and kernel spaces on each call, while epoll maintains persistent kernel state.

select and poll return all monitored descriptors, requiring O(n) processing time. epoll returns only active descriptors, enabling O(1) processing.

epoll supports edge-triggered mode and EPOLLONESHOT events, reducing unnecessary notifications.

Kernel Efficiency

select and poll use polling algorithms with O(n) complexity for event detection. epoll employs callback mechanisms with O(1) complexity for active event detection.

For highly active connections, select and poll may outperform epoll due to reduced callback overhead. epoll excels with many connections having low activiyt.

Event Notification Modes

Level-triggered (default): Kernel continues notifying while events remain unhandled.

Edge-triggered: Kernel notifies only when events first occur.

select Implementation Details

select returns negative values on errors, positive values when events occur, and zero on timeout.

The max_fds parameter should be set to the highest monitored file descriptor plus one.

Macro operations:

  • FD_CLR(int fd, fd_set *set): Clears descriptor from set
  • FD_ISSET(int fd, fd_set *set): Tests descriptor presence
  • FD_SET(int fd, fd_set *set): Adds descriptor to set
  • FD_ZERO(fd_set *set): Clears entire set

Timeout structure:

struct timeval {
    long tv_sec;    /* seconds */
    long tv_usec;   /* microseconds */
};

Timer implementation example:

void wait_milliseconds(unsigned ms) {
    struct timeval delay;
    delay.tv_sec = ms / 1000;
    delay.tv_usec = (ms % 1000) * 1000;
    int status;
    do {
        status = select(0, NULL, NULL, NULL, &delay);
    } while (status < 0 && errno == EINTR);
}

poll Implementation Details

poll uses a linked list structure to manage file descriptors, avoiding the fixed-size limitations of select.

The system call copies user-space pollfd structures to kernel space, which can become expensive with many descriptors.

Key data structures:

struct pollfd {
    int fd;
    short events;
    short revents;
};

Kernel processing involves:

  1. Validating descriptor counts
  2. Converting timeout values
  3. Copying user data to kernel space
  4. Processing events through device driver poll functions
  5. Returning results to user space

The __pollwait callback function registers current processes with device wait queues, enabling efficient event notification.

epoll Architecture

Core Components

epoll_create establishes a kernel event table using red-black trees for efficient descriptor management.

epoll_ctl manages descriptor registration through:

  • EPOLL_CTL_ADD: Register new descriptor
  • EPOLL_CTL_MOD: Modify existing registration
  • EPOLL_CTL_DEL: Remove descriptor

Event structure:

struct epoll_event {
    uint32_t events;
    epoll_data_t data;
};

typedef union epoll_data {
    void *ptr;
    int fd;
    uint32_t u32;
    uint64_t u64;
} epoll_data_t;

Supported Events

  • EPOLLIN: Readable data available
  • EPOLLOUT: Write capacity available
  • EPOLLRDHUP: Remote connection shutdown
  • EPOLLPRI: Priority data available
  • EPOLLERR: Error condition
  • EPOLLHUP: Connection hangup
  • EPOLLET: Edge-triggered mode
  • EPOLLONESHOT: Single notification mode

Performance Advantages

epoll maintains persistent kernel state, eliminating descriptor copying between calls.

Active events are maintained in a ready list, enabling O(1) retrieval time.

Device drivers register callback functions that populate the ready list directly.

Error Handling

Common epoll error codes:

  • EBADF: Invalid file descriptor
  • EEXIST: Duplicate descriptor addition
  • EINVAL: Invalid parameters
  • ELOOP: Circular epoll nesting
  • ENOENT: Missing descriptor modification
  • ENOMEM: Insufficient memory
  • ENOSPC: Descriptor limit exceeded
  • EPERM: Unsupported file type

Server Implementation Example

#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>

#define MAX_CONNECTIONS 1024
#define BUFFER_SIZE 4096

int configure_nonblocking(int socket_desc) {
    int flags = fcntl(socket_desc, F_GETFL, 0);
    return fcntl(socket_desc, F_SETFL, flags | O_NONBLOCK);
}

int initialize_server() {
    int server_sock = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in server_addr = {
        .sin_family = AF_INET,
        .sin_port = htons(8080),
        .sin_addr.s_addr = INADDR_ANY
    };
    
    bind(server_sock, (struct sockaddr*)&server_addr, sizeof(server_addr));
    listen(server_sock, SOMAXCONN);
    configure_nonblocking(server_sock);
    
    return server_sock;
}

void handle_client_data(int client_fd) {
    char buffer[BUFFER_SIZE];
    ssize_t bytes_read = recv(client_fd, buffer, BUFFER_SIZE, 0);
    
    if (bytes_read > 0) {
        send(client_fd, buffer, bytes_read, 0);
    } else if (bytes_read == 0) {
        close(client_fd);
    }
}

int main() {
    int epoll_fd = epoll_create1(0);
    int server_sock = initialize_server();
    
    struct epoll_event server_event = {
        .events = EPOLLIN,
        .data.fd = server_sock
    };
    epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_sock, &server_event);
    
    struct epoll_event active_events[MAX_CONNECTIONS];
    
    while (1) {
        int event_count = epoll_wait(epoll_fd, active_events, MAX_CONNECTIONS, -1);
        
        for (int i = 0; i < event_count; i++) {
            if (active_events[i].data.fd == server_sock) {
                struct sockaddr_in client_addr;
                socklen_t addr_len = sizeof(client_addr);
                int client_fd = accept(server_sock, (struct sockaddr*)&client_addr, &addr_len);
                
                configure_nonblocking(client_fd);
                struct epoll_event client_event = {
                    .events = EPOLLIN | EPOLLET,
                    .data.fd = client_fd
                };
                epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &client_event);
            } else {
                handle_client_data(active_events[i].data.fd);
            }
        }
    }
    
    close(server_sock);
    close(epoll_fd);
    return 0;
}

Client Implementation Example

#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>

#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 8080

int establish_connection() {
    int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in server_addr = {
        .sin_family = AF_INET,
        .sin_port = htons(SERVER_PORT),
        .sin_addr.s_addr = inet_addr(SERVER_IP)
    };
    
    connect(sock_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));
    return sock_fd;
}

int main() {
    int epoll_fd = epoll_create1(0);
    int sock_fd = establish_connection();
    
    struct epoll_event write_event = {
        .events = EPOLLOUT,
        .data.fd = sock_fd
    };
    epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sock_fd, &write_event);
    
    struct epoll_event active_event;
    epoll_wait(epoll_fd, &active_event, 1, -1);
    
    if (active_event.events & EPOLLOUT) {
        const char *message = "Hello Server";
        send(sock_fd, message, strlen(message), 0);
    }
    
    close(sock_fd);
    close(epoll_fd);
    return 0;
}

Multiple Client Management

A single epoll instance can manage multiple client connections efficiently:

#define MAX_CLIENTS 1000

int client_sockets[MAX_CLIENTS];
struct epoll_event client_events[MAX_CLIENTS];

void setup_clients(int epoll_fd) {
    for (int i = 0; i < MAX_CLIENTS; i++) {
        client_sockets[i] = establish_connection();
        client_events[i].events = EPOLLIN | EPOLLOUT;
        client_events[i].data.fd = client_sockets[i];
        epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_sockets[i], &client_events[i]);
    }
}

void monitor_clients(int epoll_fd) {
    struct epoll_event active_events[MAX_CLIENTS];
    
    while (1) {
        int count = epoll_wait(epoll_fd, active_events, MAX_CLIENTS, -1);
        
        for (int i = 0; i < count; i++) {
            if (active_events[i].events & EPOLLIN) {
                handle_incoming_data(active_events[i].data.fd);
            }
            if (active_events[i].events & EPOLLOUT) {
                handle_outgoing_data(active_events[i].data.fd);
            }
        }
    }
}

Dynamic Descriptor Management

Descriptors can be dynamically added and removed from epoll monitoring:

void add_descriptor(int epoll_fd, int new_fd) {
    struct epoll_event new_event = {
        .events = EPOLLIN | EPOLLET,
        .data.fd = new_fd
    };
    epoll_ctl(epoll_fd, EPOLL_CTL_ADD, new_fd, &new_event);
}

void remove_descriptor(int epoll_fd, int old_fd) {
    epoll_ctl(epoll_fd, EPOLL_CTL_DEL, old_fd, NULL);
    close(old_fd);
}

Windows Compatibility

wepoll provides epoll-like functionality on Windows systems, supporting:

  • High-concurrency network services
  • Cross-platform application porting
  • Thread-safe operation
  • Efficient descriptor management

Key features include single-file integration, comprehensive event support, and compatibility with Windows Vista and later versions.

Tags: io-multiplexing SELECT poll Epoll network-programming

Posted on Sun, 04 Oct 2026 16:20:38 +0000 by ALEXKENT18