How Linux Implements io_uring for Asynchronous I/O
The io_uring subsystem, introduced in Linux 5.1, is a
high-performance asynchronous I/O framework designed to eliminate the
scalability bottlenecks of older interfaces like epoll and
aio. This article explores how the Linux kernel implements
io_uring through shared ring buffers, lockless
synchronization, kernel polling threads, and asynchronous worker pools
to maximize I/O throughput while drastically reducing system call
overhead.
Shared Ring Buffers
At the foundation of io_uring are two primary lockless
ring buffers allocated in kernel space and exposed to user space via
mmap():
- Submission Queue (SQ): An array of indices mapping
to a shared buffer of Submission Queue Entries
(
struct io_uring_sqe). User space produces requests by populating SQEs and updating the submission ring's tail. - Completion Queue (CQ): An array of Completion Queue
Entries (
struct io_uring_cqe). The kernel consumes SQ entries, processes them, and writes results to the CQ, updating the completion ring's tail.
Because both user space and the kernel share these memory regions, application threads can submit requests and consume results without context switching into the kernel for every operation. Memory barriers and atomic operations coordinate ring head and tail pointers, allowing single-producer single-consumer lockless access.
System Call Lifecycle and Processing
To initiate I/O, an application initializes an instance using the
io_uring_setup() system call, which creates the queues and
returns a file descriptor. The application then configures its shared
memory mappings.
When operations are ready:
- The application places one or more operations (e.g., reads, writes, network calls, filesystem operations) into the SQ.
- The application invokes the
io_uring_enter()system call to notify the kernel that new entries are ready. A singleio_uring_enter()call can submit hundreds of requests at once and optionally wait for a specified number of completions, combining submission and retrieval into one context switch.
Kernel Polling Mode (SQPOLL)
To achieve true zero-syscall I/O, io_uring provides an
optional kernel polling mode (IORING_SETUP_SQPOLL). When
enabled, the kernel spawns a dedicated kernel thread
(io_uring-sq) that continuously monitors the Submission
Queue.
As soon as user space writes an entry and updates the queue's tail, the kernel thread detects the change and processes the operation immediately. Under sustained loads, an application can perform millions of I/O operations entirely via shared memory without invoking a single system call.
The io-wq Subsystem for Blocking Operations
Not all I/O operations can be executed non-blockingly at the device driver level. For buffered file I/O or metadata lookups, operations may block waiting on disk or locks.
To prevent blocking the calling context or the SQ polling thread,
Linux implements an asynchronous work-queue subsystem named
io-wq. When an operation cannot complete immediately in a
non-blocking fashion, io_uring offloads the operation to an
io-wq worker thread. These threads dynamically scale based
on demand to handle concurrent blocking operations and return their
completions to the primary Completion Queue once finished.
Fixed Resources and Registered Buffers
Linux further optimizes io_uring performance through
resource pre-registration:
- Registered Files
(
IORING_REGISTER_FILES): Avoids the overhead of incrementing and decrementing file descriptor reference counts on every I/O operation by maintaining an internal fixed array of file references. - Registered Buffers
(
IORING_REGISTER_BUFFERS): Eliminates page mapping and pinning overhead. User-space buffers are locked into memory once during initialization, allowing the kernel to map virtual memory directly to physical addresses for direct DMA transfers.
Through shared memory queues, asynchronous worker threads, and
optional zero-syscall kernel polling, the Linux kernel's
io_uring implementation provides a unified, highly scalable
engine capable of handling storage, networking, and generic asynchronous
tasks with minimal CPU overhead.