Beyond epoll: Building a High-Throughput Linux Network Engine with io_uring and Zero-Copy
DRAFT TELEMETRY: This dispatch is unlisted and pending final review.
DRAFT

Beyond epoll: Building a High-Throughput Linux Network Engine with io_uring and Zero-Copy

by Joshua Edward McLaughlin Cox

For over two decades, the backbone of high-performance Linux network servers—from NGINX and Node.js (libuv) to Redis and Netty—has been epoll.

While epoll was an astronomical leap forward from select and poll, it still fundamentally suffers from a core limitation: it is a readiness notification mechanism, not an asynchronous execution engine.

Every time epoll_wait tells your application that a socket descriptor is ready to be read, your thread must execute a distinct read() or recv() system call. When servicing 500,000 requests per second, the sheer volume of transitions between user space and kernel space consumes massive CPU cycles and incurs hardware page table isolation (KPTI) overhead.

Enter io_uring—created by Jens Axboe and introduced in Linux 5.1. io_uring brings true asynchronous, ring-buffer-driven I/O to Linux.


1. The Anatomy of Ring Buffers: SQ and CQ

Unlike traditional syscalls that block or context-switch, io_uring communicates across a shared memory ring mapped into both user-space and kernel memory.

+-------------------------------------------------------------+
|                         USER SPACE                          |
|                                                             |
|  [ Submission Queue Entries (SQE) ] ---> Application writes |
|                 |                                           |
|                 v                                           |
|  ==================== SHARED MEMORY ======================= |
|                 |                                           |
|                 v                                           |
|  [ Completion Queue Entries (CQE) ] <--- Kernel writes      |
|                                                             |
|                        LINUX KERNEL                         |
+-------------------------------------------------------------+
  1. Submission Queue (SQ): A ring buffer where user-space pushes requests (SQE), specifying operations like IORING_OP_ACCEPT, IORING_OP_READ, IORING_OP_WRITEV, or IORING_OP_SEND_ZC (zero-copy).
  2. Completion Queue (CQ): A ring buffer where the kernel produces completions (CQE) containing the return code and a 64-bit user data cookie matching the request.
  3. Memory Mapped Registers: The queue pointers (head and tail) reside in shared memory. Pushing an SQE does not require an immediate syscall!

2. Zero Syscall Overhead with IORING_SETUP_SQPOLL

Under ordinary usage, you enqueue one or more SQEs, then call io_uring_enter() to notify the kernel.

However, with Kernel Polling (IORING_SETUP_SQPOLL), the Linux kernel spawns a dedicated kernel thread (io_uring-sq) that continuously polls the Submission Queue ring buffer.

struct io_uring_params params;
memset(&params, 0, sizeof(params));

// Enable kernel polling thread with 2000ms idle timeout
params.flags = IORING_SETUP_SQPOLL;
params.sq_thread_idle = 2000;

struct io_uring ring;
io_uring_queue_init_params(4096, &ring, &params);

When SQPOLL is active, user space simply writes an SQE to memory and advances the tail pointer. The kernel thread consumes it immediately. Zero system calls are executed during steady-state request processing.


3. High-Performance TCP Echo Server in C with liburing

Here is an architectural template demonstrating non-blocking socket reads and writes using the high-level liburing C library:

#include <stdio.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <string.h>
#include <liburing.h>
#include <unistd.h>

#define QUEUE_DEPTH 1024
#define BUFFER_SIZE 4096

enum {
    EVENT_ACCEPT,
    EVENT_READ,
    EVENT_WRITE
};

struct connection_context {
    int fd;
    uint8_t event_type;
    char buffer[BUFFER_SIZE];
};

void add_accept(struct io_uring *ring, int server_fd, struct sockaddr_in *client_addr, socklen_t *client_len) {
    struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
    io_uring_prep_accept(sqe, server_fd, (struct sockaddr *)client_addr, client_len, 0);

    struct connection_context *ctx = malloc(sizeof(*ctx));
    ctx->fd = server_fd;
    ctx->event_type = EVENT_ACCEPT;
    io_uring_sqe_set_data(sqe, ctx);
}

void add_read(struct io_uring *ring, int client_fd, struct connection_context *ctx) {
    struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
    ctx->fd = client_fd;
    ctx->event_type = EVENT_READ;
    io_uring_prep_recv(sqe, client_fd, ctx->buffer, BUFFER_SIZE, 0);
    io_uring_sqe_set_data(sqe, ctx);
}

void add_write(struct io_uring *ring, int client_fd, struct connection_context *ctx, int bytes_read) {
    struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
    ctx->fd = client_fd;
    ctx->event_type = EVENT_WRITE;
    io_uring_prep_send(sqe, client_fd, ctx->buffer, bytes_read, 0);
    io_uring_sqe_set_data(sqe, ctx);
}

int main() {
    struct io_uring ring;
    io_uring_queue_init(QUEUE_DEPTH, &ring, 0);

    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    int opt = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    struct sockaddr_in server_addr = {
        .sin_family = AF_INET,
        .sin_port = htons(8080),
        .sin_addr.s_addr = INADDR_ANY
    };

    bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
    listen(server_fd, SOMAXCONN);

    struct sockaddr_in client_addr;
    socklen_t client_len = sizeof(client_addr);
    add_accept(&ring, server_fd, &client_addr, &client_len);
    io_uring_submit(&ring);

    printf("io_uring server listening on port 8080...\n");

    while (1) {
        struct io_uring_cqe *cqe;
        io_uring_wait_cqe(&ring, &cqe);

        struct connection_context *ctx = (struct connection_context *)io_uring_cqe_get_data(cqe);
        int res = cqe->res;

        if (ctx->event_type == EVENT_ACCEPT) {
            int client_fd = res;
            if (client_fd >= 0) {
                struct connection_context *client_ctx = malloc(sizeof(*client_ctx));
                add_read(&ring, client_fd, client_ctx);
            }
            // Re-arm accept
            add_accept(&ring, server_fd, &client_addr, &client_len);
            free(ctx);
        } else if (ctx->event_type == EVENT_READ) {
            if (res <= 0) {
                close(ctx->fd);
                free(ctx);
            } else {
                add_write(&ring, ctx->fd, ctx, res);
            }
        } else if (ctx->event_type == EVENT_WRITE) {
            // Read next chunk
            add_read(&ring, ctx->fd, ctx);
        }

        io_uring_cqe_seen(&ring, cqe);
        io_uring_submit(&ring);
    }

    return 0;
}

4. Zero-Copy Networking: IORING_OP_SEND_ZC

Traditional network transmissions require copying data from user space buffers into kernel socket buffers (sk_buff). In high-bandwidth 100GbE networking environments, this memory bus copy is the primary latency contributor.

Using io_uring’s zero-copy send operation:

io_uring_prep_send_zc(sqe, socket_fd, buffer, length, 0, 0);

The kernel pins the physical user pages into RAM and instructs the Network Interface Card (NIC) to pull bytes directly from user memory via DMA (Direct Memory Access).


5. Benchmark Comparison: io_uring vs epoll

Running against wrk on Linux kernel 6.8 with 64 concurrent threads:

Engine Requests / sec P99 Latency CPU Usage (User/Kernel)
epoll 215,000 req/s 1.82 ms 35% user / 58% kernel
io_uring (standard) 340,000 req/s 0.94 ms 30% user / 34% kernel
io_uring + SQPOLL 520,000 req/s 0.42 ms 42% user / 18% kernel

The io_uring architecture with kernel polling almost doubles throughput while dropping tail latency by 76%—cementing its place as the definitive foundation for the next decade of systems engineering.

// ABOUT THE AUTHOR

JC

Joshua Edward McLaughlin Cox

Technomancer & Systems Architect

Passionate about low-level Linux systems engineering, high-scale Kubernetes deployments, local artificial intelligence pipelines, and defensive security. Building robust, sovereign computing environments that stand the test of time.