How to Use the io_uring Event Mechanism on Modern Linux Kernels with libfiber

libfiber abstracts the Linux kernel's io_uring subsystem behind a unified event interface that can be enabled at compile time with HAS_IO_URING=yes and activated at runtime using event_set(FIBER_EVENT_IO_URING) to achieve high-performance asynchronous I/O.

The iqiyi/libfiber library provides a portable coroutine framework that leverages modern Linux kernel features to minimize I/O overhead. By implementing an io_uring event mechanism, libfiber allows applications to batch submissions and reduce syscall overhead without abandoning the familiar fiber-based programming model. This guide explains how to compile, configure, and code against libfiber's io_uring backend using the actual source implementation.

Compiling libfiber with io_uring Support

Before utilizing the io_uring event mechanism, you must ensure the library is built with the appropriate flag. The build system controlled by c/Makefile checks for HAS_IO_URING.

make HAS_IO_URING=yes

When enabled, the Makefile appends -DHAS_IO_URING to CFLAGS, exposing the io_uring-specific code paths in c/src/event/event_io_uring.c. This compile-time flag is essential because it conditionally includes the <liburing.h> headers and the EVENT_URING structure definition.

Selecting the io_uring Backend at Runtime

With the library compiled for io_uring support, choosing the backend requires calling event_set() before creating any event loops. This function stores the desired mode in the thread-local variable __event_mode defined in c/src/event.h.

#include "fiber/libfiber.h"

int main(void) {
    /* Select io_uring as the underlying event mechanism */
    event_set(FIBER_EVENT_IO_URING);
    
    /* Initialize the event loop with 1024 ring entries */
    EVENT *ev = event_create(1024);
    
    /* Application logic using fiber_create(), fiber_wait_read(), etc. */
    
    event_free(ev);
    return 0;
}

The event_create() function in c/src/event.c inspects __event_mode and dispatches to event_io_uring_create() when the value equals FIBER_EVENT_IO_URING. This design ensures that the same application code can run on older kernels by simply changing the event_set() parameter or omitting it entirely to use the default epoll backend.

Architecture of the io_uring Event Mechanism

Understanding how libfiber maps high-level fiber operations to io_uring syscalls helps optimize application performance. The implementation centers on three phases: initialization, submission, and completion handling.

Ring Initialization and Structure

The function event_io_uring_create() in c/src/event/event_io_uring.c allocates an EVENT_URING structure and initializes the kernel ring buffer via io_uring_queue_init_params(). This setup records the submission queue entry (SQE) size and wires generic EVENT callbacks—including event_wait, add_read, and add_write—to io_uring-specific implementations.

After initialization, the code checks for IORING_FEAT_FAST_POLL to verify that the kernel supports busy-wait polling for sockets. If this feature is unavailable, the library degrades gracefully rather than failing.

Submission Strategy and Linked Timeouts

All I/O operations route through helper macros TRY_SUBMMIT and SUBMMIT, which batch SQEs and invoke io_uring_submit() only when the ring buffer fills or an explicit flush occurs. This zero-copy approach reuses the liburing SQE buffer directly.

When a fiber calls fiber_wait_read() or fiber_wait_write() (implemented in c/src/fiber_io.c), the backend eventually executes event_uring_add_read() or event_uring_add_write(). These functions prepare operations using io_uring_prep_readv(), io_uring_prep_poll_add(), or similar liburing helpers. For timeouts, libfiber constructs a linked timeout SQE using io_uring_prep_link_timeout(), ensuring the timeout cancels automatically if the I/O completes first.

Completion Handling and Fiber Resumption

The event loop blocks in event_uring_wait(), which calls submit_and_wait() to park the thread until at least one completion queue entry (CQE) arrives. The peek_more function then drains additional CQEs to minimize context switches.

Each CQE is converted to a high-level FILE_EVENT via the IO_URING_CTX payload and dispatched to handle_read() or handle_write(). These callbacks resume the suspended fiber by invoking fe->r_proc or fe->w_proc, effectively bridging the gap between kernel-level asynchronous notifications and user-level cooperative multitasking.

Practical Examples

Building an io_uring-Enabled Echo Server

The following implementation demonstrates how standard libfiber APIs automatically utilize io_uring operations when the backend is active. Note that fiber_wait_read() and accept() trigger io_uring_prep_poll_add and io_uring_prep_accept respectively behind the scenes.

/* echo_server.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include "fiber/libfiber.h"

#define LISTEN_PORT 12345
#define BUF_SIZE    4096

static void client_handler(ACL_FIBER *fiber, void *ctx)
{
    int fd = *(int *)ctx;
    char buf[BUF_SIZE];
    ssize_t n;

    for (;;) {
        /* Triggers io_uring_prep_poll_add + read operation */
        if (fiber_wait_read(fd) < 0) break;

        n = read(fd, buf, sizeof(buf));
        if (n <= 0) break;

        /* Triggers io_uring_prep_poll_add + write operation */
        if (fiber_wait_write(fd) < 0) break;
        write(fd, buf, (size_t)n);
    }
    close(fd);
}

int main(void)
{
    /* 1. Select io_uring backend */
    event_set(FIBER_EVENT_IO_URING);
    
    /* 2. Create event loop with 2048-entry ring */
    EVENT *ev = event_create(2048);
    if (!ev) {
        perror("event_create");
        return 1;
    }

    /* 3. Standard socket setup */
    int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in addr = {0};
    addr.sin_family = AF_INET;
    addr.sin_port   = htons(LISTEN_PORT);
    addr.sin_addr.s_addr = INADDR_ANY;
    bind(listen_fd, (struct sockaddr *)&addr, sizeof(addr));
    listen(listen_fd, 128);

    for (;;) {
        /* Uses io_uring_prep_accept when backend is io_uring */
        int client_fd = accept(listen_fd, NULL, NULL);
        if (client_fd < 0) continue;
        
        fiber_create(client_handler, &client_fd);
    }

    event_free(ev);
    return 0;
}

Accessing Raw liburing Operations

For operations not wrapped by libfiber's high-level API, retrieve the underlying struct io_uring pointer using event_handle():

#include <liburing.h>
#include "fiber/libfiber.h"

void custom_statx(EVENT *ev)
{
    /* Cast the handle to the io_uring structure */
    struct io_uring *ring = (struct io_uring *)event_handle(ev);
    
    struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
    struct statx stx;
    
    io_uring_prep_statx(sqe, AT_FDCWD, "/etc/passwd", 0,
                        STATX_BASIC_STATS, &stx);
    io_uring_sqe_set_data(sqe, NULL);
    io_uring_submit(ring);
    
    /* Completion integrates with normal libfiber event loop */
}

This accesses the internal ring initialized in event_io_uring.c while maintaining compatibility with the library's completion handling.

Summary

  • Compile libfiber with HAS_IO_URING=yes to enable the io_uring code paths in c/src/event/event_io_uring.c.
  • Select the backend by calling event_set(FIBER_EVENT_IO_URING) before event_create(), which stores the mode in __event_mode and dispatches to event_io_uring_create().
  • Submit operations through batched SQEs using the TRY_SUBMMIT macro, with automatic fallback to epoll on older kernels.
  • Handle completions via event_uring_wait(), which drains CQEs and resumes fibers through handle_read() and handle_write() callbacks.

Frequently Asked Questions

What Linux kernel version is required for libfiber's io_uring support?

The io_uring event mechanism requires Linux kernel 5.1 or newer for basic operation, with full feature support (including IORING_FEAT_FAST_POLL) available from 5.5 onwards. If the kernel lacks io_uring syscalls, libfiber automatically falls back to epoll or kqueue, allowing the same binary to run on older systems without modification.

How does libfiber handle timeouts with io_uring?

When a fiber specifies a timeout (e.g., fiber_wait_read() with a deadline), libfiber creates a linked timeout SQE using io_uring_prep_link_timeout(). This ensures the timeout operation cancels automatically if the primary I/O completes first, preventing resource leaks and unnecessary wakeups.

Can I use raw liburing calls alongside libfiber's high-level APIs?

Yes. The event_handle() function returns the address of the internal struct io_uring ring initialized in event_io_uring_create(). You can submit custom SQEs using standard liburing functions, and completions will integrate with libfiber's event loop, though you must ensure your custom completions do not conflict with the library's internal IO_URING_CTX payload management.

Where does the actual io_uring submission happen in the source code?

The submission logic resides in c/src/event/event_io_uring.c. The SUBMMIT macro calls io_uring_submit() when the SQE ring fills or when explicit flushing occurs. Individual operations like event_uring_add_read() prepare SQEs using io_uring_prep_readv() or io_uring_prep_poll_add(), depending on whether a direct read or a poll-then-read strategy is optimal.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →