How Tech - Systems Programming

How Tech - Systems Programming

io_uring Internals: Asynchronous I/O for Heterogeneous Compute Pipelines

Aug 10, 2026
∙ Paid

1. Introduction

Every I/O interface Linux shipped before 5.1 shared one structural assumption: a thread issues a syscall, traps into the kernel, and either blocks or gets EAGAIN. read(2), aio_read(3), even epoll with non-blocking sockets — all of them pay a syscall trap per operation, and most of them still can’t do true asynchronous file I/O. When your workload is a single accelerator waiting on a single disk, that overhead is noise. When your workload is a scheduler feeding hundreds of thousands of small I/O requests per second across NVMe queues, network sockets, and GPU-staged buffers, the syscall boundary itself becomes the bottleneck.

io_uring solves this by removing the syscall from the hot path entirely. Two ring buffers — a submission queue (SQ) and a completion queue (CQ) — are memory-mapped into both the application and the kernel. The application writes submission queue entries (SQEs) directly into shared memory; the kernel writes completion queue entries (CQEs) directly into shared memory. In the steady state, no syscall is required to submit or reap I/O at all.

This article is a systems-level dive into how that ring architecture actually works: the kernel structures behind it, the CPU-level memory ordering it depends on, and a real concurrency bug you will hit the first time you try to share a ring across threads — caught here with ThreadSanitizer, not asserted from a man page.

It’s worth being precise about what “asynchronous” means here, because io_uring is often mis-described as “non-blocking I/O,” which it is not, strictly. read(2) on a non-blocking fd returns immediately with EAGAIN if data isn’t ready — the caller still has to poll or wait on readiness via epoll. io_uring instead lets the kernel itself decide whether an operation completes inline or gets handed to a worker thread; the caller’s job is only to submit the request and eventually collect the result, regardless of how long the kernel took or which path it used internally. That distinction — asynchronous completion notification versus non-blocking return codes — is the actual architectural shift, and it’s why io_uring subsumes both read/write and epoll-style readiness models under one interface.

User's avatar

Continue reading this post for free, courtesy of Systems.

Or purchase a paid subscription.
© 2026 Sumedh S · Privacy ∙ Terms ∙ Collection notice
Start your SubstackGet the app
Substack is the home for great culture