Simple instance of I/O multiplexing select/poll/epoll multiplexer

select & poll

#include <sys/select.h>
 
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
 
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
 
// Four macros closely combined with select:
void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);

select:

#include <stdio.h>
#include <poll.h>

int main() {
    struct pollfd fds[1];
    int timeout = 5000; // set the timeout to 5 seconds

    fds[0].fd = 0; // standard input file descriptor is 0
    fds[0].events = POLLIN; // focus on readable events

    while (1) {
        int ready = poll(fds, 1, timeout); // call the poll function to wait for the event to occur

        if (ready == -1) {
            perror("poll");
            break;
        } else if (ready == 0) {
            printf("Timeout\\
");
        } else {
            // check if standard input is ready
            if (fds[0]. revents & POLLIN) {
                char buffer[1024];
                int bytesRead = read(0, buffer, sizeof(buffer));
                if (bytesRead > 0) {
                    // process data read from standard input
                    printf("Read %d bytes: %.*s\\
", bytesRead, bytesRead, buffer);
                } else if (bytesRead == 0) {
                    printf("EOF\\
");
                    break;
                } else {
                    perror("read");
                    break;
                }
            }
        }
    }

    return 0;
}

poll:

#include <stdio.h>
#include <poll.h>

int main() {
    struct pollfd fds[1];
    int timeout = 5000; // set the timeout to 5 seconds

    fds[0].fd = 0; // standard input file descriptor is 0
    fds[0].events = POLLIN; // focus on readable events

    while (1) {
        int ready = poll(fds, 1, timeout); // call the poll function to wait for the event to occur

        if (ready == -1) {
            perror("poll");
            break;
        } else if (ready == 0) {
            printf("Timeout\\
");
        } else {
            // check if standard input is ready
            if (fds[0]. revents & POLLIN) {
                char buffer[1024];
                int bytesRead = read(0, buffer, sizeof(buffer));
                if (bytesRead > 0) {
                    // process data read from standard input
                    printf("Read %d bytes: %.*s\\
", bytesRead, bytesRead, buffer);
                } else if (bytesRead == 0) {
                    printf("EOF\\
");
                    break;
                } else {
                    perror("read");
                    break;
                }
            }
        }
    }

    return 0;
}

The implementation of poll is very similar to select, except that the way to describe the fd collection is different. poll uses the pollfd structure instead of the fd_set structure of select. poll solves the problem of the maximum number of file descriptors, but it also needs to copy all fd from user mode to Kernel mode also needs to linearly traverse all fd collections, so it is only a distinction in implementation details from select, and there is no essential difference.

epoll:

epoll API

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

int main() {
    int epoll_fd = epoll_create1(0); // create epoll instance
    if (epoll_fd == -1) {
        perror("epoll_create1");
        return 1;
    }

    struct epoll_event event;
    event.events = EPOLLIN; // focus on readable events
    event.data.fd = 0; // standard input file descriptor is 0

    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, 0, &event) == -1) {
        perror("epoll_ctl");
        close(epoll_fd);
        return 1;
    }

    struct epoll_event events[1];
    int max_events = 1;
    int timeout = 5000; // set the timeout to 5 seconds

    while (1) {
        int ready = epoll_wait(epoll_fd, events, max_events, timeout); // call the epoll_wait function to wait for events to occur

        if (ready == -1) {
            perror("epoll_wait");
            break;
        } else if (ready == 0) {
            printf("Timeout\\
");
        } else {
            // check if standard input is ready
            if (events[0].events & EPOLLIN) {
                char buffer[1024];
                int bytesRead = read(0, buffer, sizeof(buffer));
                if (bytesRead > 0) {
                    // process data read from standard input
                    printf("Read %d bytes: %.*s\\
", bytesRead, bytesRead, buffer);
                } else if (bytesRead == 0) {
                    printf("EOF\\
");
                    break;
                } else {
                    perror("read");
                    break;
                }
            }
        }
    }

    close(epoll_fd);

    return 0;
}

Use code examples to better understand and distinguish the differences between them.

Where is the high performance of epoll compared to select/poll?

epoll has several performance advantages over select and poll:

  1. Event notification method: select and poll need to traverse the entire file descriptor collection to find ready descriptors when an event occurs, while epoll uses the callback mechanism to return only ready file descriptors, avoiding the overhead of traversing the entire collection.

  2. Higher scalability: The time complexity of select and poll is O(n), where n is the number of monitored file descriptors, As the number of file descriptors increases, the performance drops significantly. However, epoll uses a Red-Black Tree to store file descriptors, with a time complexity of O(log n) and better scalability.

  3. More efficient kernel notification mechanism: select and poll need to check the state change of the file descriptor through polling or timer, and epoll uses the kernel’s event notification mechanism (such as epoll_wait) to directly notify the application program when the file descriptor state changes, avoiding invalid polling and timer overhead.

  4. Support large-scale concurrency: Since epoll uses a red-black tree to store file descriptors, it can support large-scale concurrent connections. In contrast, select and poll perform poorly for large collections of file descriptors.