cserverclientepollepollet

epoll() EPOLLET event handling is skipped


I wrote a simple client-server application. When I started testing, I noticed that not all events are processed properly when the EPOLLET flag is set for socket fd.

In the loop, I connected to the server and sent some data to it. For the test, I made 10,000 connections, and from the server side I counted each event, whether it was an event on a socket descriptor or a client one. And always according to the logs, the server took less than expected. (for 10000 iterations approximately (~ 9200). And I don’t understand what this could be connected with.

Am I handling the events correctly or am I missing something? Maybe my approach to testing is not quite right (I counted output of lines every time something happened on socket fd)

Short compiled code

client.c:

#include <assert.h>
#include <netdb.h>
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>

#define DEF_IP             "127.0.0.1"
#define DEF_PORT           "3940"
#define DEF_MESSAGES_COUNT 10000

typedef struct addrinfo addrinfo;

static int conn_init();

int main() {
    int      i, fd_socket;
    ssize_t  bytes_send;
    uint64_t num;

    for (i = 0; i < DEF_MESSAGES_COUNT; i++) {
        fd_socket = conn_init();

        num = htobe64(i);
        bytes_send = send(fd_socket, &num, sizeof(num), 0);
        assert(bytes_send > -1);

        printf("I! Client: sent [%d], bytes: [%ld]\n", i, bytes_send);
        close(fd_socket);
    }

    printf("sended: %d messages ([0] - [%d])\n", i, i - 1);
    return 0;
}

static int conn_init() {
    addrinfo  info_hints = {0};
    addrinfo *info_server;
    int       fd_socket, ret;

    info_hints.ai_family = AF_UNSPEC;
    info_hints.ai_socktype = SOCK_STREAM;

    ret = getaddrinfo(DEF_IP, DEF_PORT, &info_hints, &info_server);
    assert(ret == 0);

    fd_socket = socket(info_server->ai_family, info_server->ai_socktype, info_server->ai_protocol);
    assert(fd_socket > -1);

    ret = connect(fd_socket, info_server->ai_addr, info_server->ai_addrlen);
    assert(ret == 0);

    freeaddrinfo(info_server);
    return fd_socket;
}

server.c:

#include <assert.h>
#include <fcntl.h>
#include <netdb.h>
#include <stdbool.h>
#include <stdio.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>

typedef struct addrinfo         addrinfo;
typedef struct epoll_event      epoll_event;
typedef struct sockaddr_storage sockaddr_storage;
typedef struct sockaddr         sockaddr;

#define DEF_IP         "127.0.0.1"
#define DEF_PORT       "3940"
#define DEF_MAX_EVENTS 1000
#define DEF_BACKLOG    1000

static int  conn_handle_socket(int fd_socket);
static void epoll_add(int fd_epoll, int fd, uint32_t flag);

int main() {
    int         fd_socket, fd_connect, fd_epoll, ret;
    epoll_event events[DEF_MAX_EVENTS];
    addrinfo    info_hints = {0};
    addrinfo *  info_server;

    info_hints.ai_family = AF_UNSPEC;      // IPv4 или IPv6
    info_hints.ai_socktype = SOCK_STREAM;  // TCP
    info_hints.ai_flags = AI_PASSIVE;      // use my IP

    ret = getaddrinfo(DEF_IP, DEF_PORT, &info_hints, &info_server);
    assert(ret == 0);

    fd_socket = socket(info_server->ai_family, info_server->ai_socktype, info_server->ai_protocol);
    assert(fd_socket > -1);

    ret = setsockopt(fd_socket, SOL_SOCKET, SO_REUSEADDR, (const char *)&(int){1}, sizeof(int));
    assert(ret == 0);

    ret = bind(fd_socket, info_server->ai_addr, info_server->ai_addrlen);
    assert(ret == 0);

    freeaddrinfo(info_server);

    ret = listen(fd_socket, DEF_BACKLOG);
    assert(ret == 0);

    // init epoll
    fd_epoll = epoll_create1(0);
    assert(fd_epoll > 0);

    // monitor socket fd
    epoll_add(fd_epoll, fd_socket, EPOLLET);

    printf("I! Server: is ready\n");

    while (1) {
        int num_events = epoll_wait(fd_epoll, events, DEF_MAX_EVENTS, -1);
        assert(num_events > -1);

        for (int i = 0; i < num_events; i++) {
            int fd_tmp = events[i].data.fd;

            if (fd_tmp == fd_socket) {
                printf("I! Server: SOCKET FD\n");
                fflush(stdout);

                // accept client
                fd_connect = conn_handle_socket(fd_socket);
                epoll_add(fd_epoll, fd_connect, EPOLLONESHOT);
            } else if (events[i].events & EPOLLIN) {
                printf("I! Server: CLIENT FD\n");
                fflush(stdout);

                // 'processing' clients
                assert(epoll_ctl(fd_epoll, EPOLL_CTL_DEL, fd_tmp, NULL) == 0);
                close(fd_tmp);
            }
        }
    }

    assert(epoll_ctl(fd_epoll, EPOLL_CTL_DEL, fd_socket, NULL) == 0);
    assert(epoll_ctl(fd_epoll, EPOLL_CTL_DEL, STDIN_FILENO, NULL) == 0);
    close(fd_socket);
    close(fd_epoll);
    return 0;
}

static int conn_handle_socket(int fd_socket) {
    int              fd_connect;
    sockaddr_storage addr_connected;
    socklen_t        sin_size;

    sin_size = sizeof(addr_connected);
    fd_connect = accept(fd_socket, (sockaddr *)&addr_connected, &sin_size);
    assert(fd_connect > -1);  // ignore EAGAIN || EWOULDBLOCK

    return fd_connect;
}

static void epoll_add(int fd_epoll, int fd, uint32_t flag) {
    int         flags, ret;
    epoll_event ev = {0};

    ev.events = EPOLLIN;
    if (flag == EPOLLET || flag == EPOLLONESHOT) {
        ev.events |= flag;
    }
    ev.data.fd = fd;

    ret = epoll_ctl(fd_epoll, EPOLL_CTL_ADD, fd, &ev);
    assert(ret == 0);

    flags = fcntl(fd, F_GETFL, 0);
    assert(flags > -1);

    flags |= O_NONBLOCK;

    ret = fcntl(fd, F_SETFL, flags);
    assert(ret > -1);
}

Solution

  • In ET(edge-triggered) mode, if multiple events generated on the same fd, it will only trigger once.

    for (int i = 0; i < num_events; i++) {
            int fd_tmp = events[i].data.fd;
    
            if (fd_tmp == fd_socket) {
                printf("I! Server: SOCKET FD\n");
                fflush(stdout);
    
                // accept client
                fd_connect = conn_handle_socket(fd_socket);
                epoll_add(fd_epoll, fd_connect, EPOLLONESHOT);
            } else if (events[i].events & EPOLLIN) {
                printf("I! Server: CLIENT FD\n");
                fflush(stdout);
    
                // 'processing' clients
                assert(epoll_ctl(fd_epoll, EPOLL_CTL_DEL, fd_tmp, NULL) == 0);
                close(fd_tmp);
            }
        }
    

    it only accepts one client for each epoll_wait returns. The other clients are still in the backlog of listenfd.

    There are two solutions:

    1. Use LT (level triggered). Because in LT mode, epoll_wait will keep returning as long as FDs have incoming events to handle.
    2. As per man(7) of epoll

    An application that employs the EPOLLET flag should use nonblocking file descriptors to avoid having a blocking read or write starve a task that is handling multiple file descriptors. The suggested way to use epoll as an edge-triggered (EPOLLET) interface is as follows: a) with nonblocking file descriptors; and b) by waiting for an event only after read(2) or write(2) return EAGAIN. And accept is a kind of read as well.