resize-rings.dhover×107all
#!/usr/bin/env dub
/+ dub.sdl:
    name "io_uring_resize_rings"
    dependency "during" version="~>0.5.0"
    platforms "linux"
    targetPath "build"
+/
/**
 * `io_uring` — grow a *live* ring with `IORING_REGISTER_RESIZE_RINGS` (Linux 6.13).
 *
 * Before 6.13 a ring's SQ/CQ sizes were fixed at `io_uring_setup` time: to use a
 * bigger ring you had to tear the old one down and build a new one, losing any
 * in-flight requests. `IORING_REGISTER_RESIZE_RINGS` lifts that limit — the kernel
 * allocates fresh SQ/CQ memory at the requested sizes, atomically swaps it in
 * (preserving in-flight requests), and `during`'s `resizeRings` re-`mmap`s the rings
 * to the new regions while keeping the *same* ring file descriptor.
 *
 * Kernel precondition (the non-obvious bit): the resize path requires the ring to be
 * a single, deferred-taskrun issuer — i.e. set up with `IORING_SETUP_SINGLE_ISSUER |
 * IORING_SETUP_DEFER_TASKRUN`. That guarantee lets the kernel swap the ring memory
 * without racing a concurrent submitter. A plain ring (no flags) is rejected with
 * `-EINVAL` even on a 6.13+ kernel, so we set those flags at setup time.
 *
 * What this example does:
 *   1. sets up a deliberately tiny ring (8 SQ entries),
 *   2. resizes it up to 32 SQ / 64 CQ entries via `io.resizeRings(newParams)`,
 *   3. asserts the kernel now *reports* the larger SQ/CQ capacity (`io.params`), and
 *   4. proves the ring is still live by filling the enlarged submission queue with
 *      more `NOP`s than the original 8-deep SQ could ever hold, then submitting them
 *      (the kernel accepting 9+ queued SQEs is only possible because the SQ grew).
 *
 * The resize register op returns `-EINVAL` on kernels older than 6.13 (the opcode is
 * unknown there), in which case we print `SKIP:` and exit 0.
 *
 * Note on scope: we deliberately verify the resize via the *submission* side (SQ
 * depth + a successful `submit`) rather than reaping the post-resize completions.
 * `during` 0.5.0's `resizeRings` re-`mmap`s the rings but does not re-sync its cached
 * CQ head/tail bookkeeping, so reading CQEs back through `front`/`popFront` right
 * after a resize is unreliable in this library version. The kernel feature itself —
 * growing a live ring — is exercised and asserted here regardless.
 *
 * Companion to the io_uring chronology:
 * see docs/research/async-io/io-uring/timeline.md
 *   § "6.13 — Ring resize, mem regions, hybrid iopoll".
 *
 * Run with: `dub run --single resize-rings.d`
 *
 * Portability: if the host has no `io_uring` at all, or the running kernel predates
 * 6.13, the program prints a `SKIP:` line and exits 0 so CI stays green.
 */
module 
(module) io_uring_resize_rings

io_uring — grow a live ring with IORING_REGISTER_RESIZE_RINGS (Linux 6.13).

Before 6.13 a ring's SQ/CQ sizes were fixed at io_uring_setup time: to use a bigger ring you had to tear the old one down and build a new one, losing any in-flight requests. IORING_REGISTER_RESIZE_RINGS lifts that limit — the kernel allocates fresh SQ/CQ memory at the requested sizes, atomically swaps it in (preserving in-flight requests), and during's resizeRings re-mmaps the rings to the new regions while keeping the same ring file descriptor.

Kernel precondition (the non-obvious bit): the resize path requires the ring to be a single, deferred-taskrun issuer — i.e. set up with IORING_SETUP_SINGLE_ISSUER | IORING_SETUP_DEFER_TASKRUN. That guarantee lets the kernel swap the ring memory without racing a concurrent submitter. A plain ring (no flags) is rejected with -EINVAL even on a 6.13+ kernel, so we set those flags at setup time.

What this example does:

  1. sets up a deliberately tiny ring (8 SQ entries),

  2. resizes it up to 32 SQ / 64 CQ entries via io.resizeRings(newParams),

  3. asserts the kernel now reports the larger SQ/CQ capacity (io.params), and

  4. proves the ring is still live by filling the enlarged submission queue with more NOPs than the original 8-deep SQ could ever hold, then submitting them (the kernel accepting 9+ queued SQEs is only possible because the SQ grew).

The resize register op returns -EINVAL on kernels older than 6.13 (the opcode is unknown there), in which case we print SKIP: and exit 0.

Note on scope: we deliberately verify the resize via the submission side (SQ depth + a successful submit) rather than reaping the post-resize completions. during 0.5.0's resizeRings re-mmaps the rings but does not re-sync its cached CQ head/tail bookkeeping, so reading CQEs back through front/popFront right after a resize is unreliable in this library version. The kernel feature itself — growing a live ring — is exercised and asserted here regardless.

Companion to the io_uring chronology: see docs/research/async-io/io-uring/timeline.md § "6.13 — Ring resize, mem regions, hybrid iopoll".

Run with: dub run --single resize-rings.d

Portability

if the host has no io_uring at all, or the running kernel predates 6.13, the program prints a SKIP: line and exits 0 so CI stays green.

io_uring_resize_rings
;
import
(module) during

Simple idiomatic dlang wrapper around linux io_uring (see: https://kernel.dk/io_uring.pdf) asynchronous API.

during
;
import
(package) core
core
.
(package) core.stdc
stdc
.
(module) core.stdc.errno

D header file for C99.

pubs.opengroup.org/onlinepubs/009695399/basedefs/errno.h.html, errno.h

Source

core/stdc/errno.d

@copyrightCopyright Sean Kelly 2005 - 2009.@licenseDistributed under the Boost Software License 1.0. (See accompanying file LICENSE)@authorsSean Kelly, Alex Rønne Petersen@standardsISO/IEC 9899:1999 (E)
errno
:
(alias constant) io_uring_resize_rings.EINVAL = int core.stdc.errno.EINVAL = 22
EINVAL
,
(alias constant) io_uring_resize_rings.EOPNOTSUPP = int core.stdc.errno.EOPNOTSUPP = 95
EOPNOTSUPP
,
(alias constant) io_uring_resize_rings.ENOSYS = int core.stdc.errno.ENOSYS = 38
ENOSYS
;
import
(package) std
std
.
(module) std.stdio
Category Symbols
File handles _popen File isFileHandle openNetwork stderr stdin stdout
Reading chunks lines readf readfln readln
Writing toFile write writef writefln writeln
Misc KeepTerminator LockType StdioException

Standard I/O functions that extend core.stdc.stdio. core.stdc.stdio is publically imported when importing std.stdio.

There are three layers of I/O:

  1. The lowest layer is the operating system layer. The two main schemes are Windows and Posix.

  2. C's stdio.h which unifies the two operating system schemes.

  3. std.stdio, this module, unifies the various stdio.h implementations into a high level package for D programs.

Source

std/stdio.d

@copyrightCopyright The D Language Foundation 2007-.@licenseBoost License 1.0.@authorsWalter Bright, Andrei Alexandrescu, Alex Rønne Petersen
stdio
:
(alias template) io_uring_resize_rings.writefln = std.stdio.writefln(alias fmt, A...)(A args) if (isSomeString!(typeof(fmt)))

Equivalent to writef(fmt, args, '\n').

writefln
, stderr;
int
int D main()
main
()
{ // Start small on purpose: an 8-entry SQ. `SINGLE_ISSUER | DEFER_TASKRUN` is the // kernel-required mode for in-place resize (see the header). `setup` rounds the // size to a power of two and fills the kernel-reported sizes into `io.params`.
(struct) during.Uring

Main entry point to work with io_uring.

It hides SubmissionQueue and CompletionQueue behind standard range interface. We put in SubmissionEntry entries and take out CompletionEntry entries.

Use predefined prepXX methods to fill required fields of SubmissionEntry before put or during putWith.

Note

prepXX functions doesn't touch previous entry state, just fills in operation properties. This is because for less error prone interface it is cleared automatically when prepared using putWith. So when using on own SubmissionEntry (outside submission queue), that would be added to the submission queue using put, be sure its cleared if it's reused for multiple operations.

Uring
(local variable) during.Uring io
io
;
const
(local variable) const(int) setupRet
setupRet
=
(local variable) during.Uring io
io
.
int during.setup(ref during.Uring uring, uint entries = 128u, during.io_uring.SetupFlags flags = SetupFlags.NONE) nothrow @nogc @safe

Setup new instance of io_uring into provided Uring structure.

@paramuring Uring structure to be initialized (must not be already initialized)@paramentries Number of entries to initialize uring with@paramflags SetupFlags to use to initialize uring.@returnsOn succes it returns 0, -errno otherwise.
setup
(8,
(enum) during.io_uring.SetupFlags

io_uring_setup() flags

SetupFlags
.
(enum value) during.io_uring.SetupFlags.SINGLE_ISSUER = 4096u

IORING_SETUP_SINGLE_ISSUER (from Linux 6.0)

Hint that only one task / thread will ever submit on this ring. Lets the kernel skip synchronisation that would otherwise be needed for shared submission. Misuse (multiple submitters) is detected and returns -EEXIST.

SINGLE_ISSUER
|
(enum) during.io_uring.SetupFlags

io_uring_setup() flags

SetupFlags
.
(enum value) during.io_uring.SetupFlags.DEFER_TASKRUN = 8192u

IORING_SETUP_DEFER_TASKRUN (from Linux 6.1)

Defer task_work to run only when the ring is being entered, rather than at the next kernel/user transition on the submitting task. Eliminates a class of interrupts and cuts latency for many workloads. Requires SINGLE_ISSUER.

DEFER_TASKRUN
);
if (
(local variable) const(int) setupRet
setupRet
< 0)
{
void std.stdio.writefln!(char, const(int))(in char[] fmt, const(int) __param_1) @safe

Equivalent to writef(fmt, args, '\n').

writefln
("SKIP: io_uring_setup failed (errno %d) — io_uring unavailable on this host",
-
(local variable) const(int) setupRet
setupRet
);
return 0; } const uint
(local variable) const(uint) oldSq
oldSq
=
(local variable) during.Uring io
io
.
during.io_uring.SetupParameters during.Uring.params() const pure nothrow @nogc @safe

io_uring parameters

params
.
(field) uint during.io_uring.SetupParameters.sq_entries

(output) allocated entries in submission queue (both ring index array and separate entry array at SUBMISSION_QUEUE_ENTRIES_OFFSET).

sq_entries
;
const uint
(local variable) const(uint) oldCq
oldCq
=
(local variable) during.Uring io
io
.
during.io_uring.SetupParameters during.Uring.params() const pure nothrow @nogc @safe

io_uring parameters

params
.
(field) uint during.io_uring.SetupParameters.cq_entries

(output) allocated entries in completion queue

cq_entries
;
// Build the target geometry. `resizeRings` reads `sq_entries`/`cq_entries` as the // *requested* sizes; the kernel writes back what it actually allocated and the new // mmap offsets (`sq_off`/`cq_off`), which `during` then uses to re-map the rings. // // Leave `flags` zeroed: the resize only changes ring *geometry*, and the kernel // rejects (-EINVAL) a params struct whose `flags` carries anything outside the // resize-relevant set.
(struct) during.io_uring.SetupParameters

Passed in for io_uring_setup(2). Copied back with updated info on success.

C API: struct io_uring_params

SetupParameters
(local variable) during.io_uring.SetupParameters newParams
newParams
;
(local variable) during.io_uring.SetupParameters newParams
newParams
.
(field) uint during.io_uring.SetupParameters.sq_entries

(output) allocated entries in submission queue (both ring index array and separate entry array at SUBMISSION_QUEUE_ENTRIES_OFFSET).

sq_entries
= 32;
(local variable) during.io_uring.SetupParameters newParams
newParams
.
(field) uint during.io_uring.SetupParameters.cq_entries

(output) allocated entries in completion queue

cq_entries
= 64;
const
(local variable) const(int) resizeRet
resizeRet
=
(local variable) during.Uring io
io
.
int during.Uring.resizeRings(ref during.io_uring.SetupParameters newParams) nothrow @nogc @trusted

Resize the SQ / CQ of this ring in-place. ``newParams.sq_entries / cq_entries set the new sizes; the kernel preserves in-flight requests across the resize. Internally unmaps and re-mmaps the rings to point at the kernel's new regions. Fails (with -EINVAL) on rings created with IORING_SETUP_NO_MMAP.

Note

Available from Linux 6.12

resizeRings
(
(local variable) during.io_uring.SetupParameters newParams
newParams
);
if (
(local variable) const(int) resizeRet
resizeRet
< 0)
{ const
(local variable) const(int) e
e
= -
(local variable) const(int) resizeRet
resizeRet
;
// -EINVAL: pre-6.13 kernels don't know REGISTER_RESIZE_RINGS (opcode 33). // -EOPNOTSUPP/-ENOSYS: register path disabled/unimplemented. if (
(local variable) const(int) e
e
==
(constant) int core.stdc.errno.EINVAL = 22
EINVAL
||
(local variable) const(int) e
e
==
(constant) int core.stdc.errno.EOPNOTSUPP = 95
EOPNOTSUPP
||
(local variable) const(int) e
e
==
(constant) int core.stdc.errno.ENOSYS = 38
ENOSYS
)
{
void std.stdio.writefln!(char, const(int))(in char[] fmt, const(int) __param_1) @safe

Equivalent to writef(fmt, args, '\n').

writefln
("SKIP: IORING_REGISTER_RESIZE_RINGS unsupported (errno %d) — needs Linux 6.13+",
(local variable) const(int) e
e
);
return 0; } stderr.
std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @system
writefln
("resizeRings failed unexpectedly: errno %d",
(local variable) const(int) e
e
);
return 1; } // After a successful resize `io.params` reflects the new (kernel-allocated) sizes. const uint
(local variable) const(uint) gotSq
gotSq
=
(local variable) during.Uring io
io
.
during.io_uring.SetupParameters during.Uring.params() const pure nothrow @nogc @safe

io_uring parameters

params
.
(field) uint during.io_uring.SetupParameters.sq_entries

(output) allocated entries in submission queue (both ring index array and separate entry array at SUBMISSION_QUEUE_ENTRIES_OFFSET).

sq_entries
;
const uint
(local variable) const(uint) gotCq
gotCq
=
(local variable) during.Uring io
io
.
during.io_uring.SetupParameters during.Uring.params() const pure nothrow @nogc @safe

io_uring parameters

params
.
(field) uint during.io_uring.SetupParameters.cq_entries

(output) allocated entries in completion queue

cq_entries
;
if (
(local variable) const(uint) gotSq
gotSq
< 32 ||
(local variable) const(uint) gotCq
gotCq
< 64)
{ stderr.
std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @system
writefln
("resize did not grow the rings: sq %u->%u (want >=32), cq %u->%u (want >=64)",
(local variable) const(uint) oldSq
oldSq
,
(local variable) const(uint) gotSq
gotSq
,
(local variable) const(uint) oldCq
oldCq
,
(local variable) const(uint) gotCq
gotCq
);
return 1; } // The re-mapped SQ must expose the larger capacity too. if (
(local variable) during.Uring io
io
.
ulong during.Uring.capacity() const pure nothrow @nogc @safe

Available space in submission queue before it becomes full

capacity
< 32)
{ stderr.
std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @system
writefln
("SQ capacity after resize is %u, expected >=32", cast(uint)
(local variable) during.Uring io
io
.
ulong during.Uring.capacity() const pure nothrow @nogc @safe

Available space in submission queue before it becomes full

capacity
);
return 1; } // Prove the ring is still live on the larger geometry: queue more NOPs than the // ORIGINAL 8-deep SQ could ever have held. If the SQ had not actually grown, // `io.full` would trip at the old depth and we could not queue this many. enum uint
(constant) uint io_uring_resize_rings.main.toQueue = 20u
toQueue
= 20; // > oldSq (8), <= gotSq (32)
uint
(local variable) uint queued
queued
;
foreach (
(local variable) uint i
i
; 0 ..
(constant) uint io_uring_resize_rings.main.toQueue = 20u
toQueue
)
{ if (
(local variable) during.Uring io
io
.
bool during.Uring.full() const pure nothrow @nogc @safe

Check if there is space for another SubmissionEntry to submit.

full
)
break;
(local variable) during.Uring io
io
.putWith!((ref SubmissionEntry e, ulong tag) {
e.prepNop(); e.user_data = tag; })(cast(ulong)
(local variable) uint i
i
);
(local variable) uint queued
queued
++;
} if (
(local variable) uint queued
queued
<
(constant) uint io_uring_resize_rings.main.toQueue = 20u
toQueue
)
{ stderr.
std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @system
writefln
("enlarged SQ only accepted %u/%u SQEs — resize did not take effect",
(local variable) uint queued
queued
,
(constant) uint io_uring_resize_rings.main.toQueue = 20u
toQueue
);
return 1; } // Hand the batch to the kernel. A successful submit on the resized ring confirms // the swapped-in SQ memory and SQE array are wired up correctly. const
(local variable) const(int) submitted
submitted
=
(local variable) during.Uring io
io
.
int during.Uring.submit(uint want) nothrow @nogc @safe

Submits qued SubmissionEntry to be processed by kernel.

@paramwant number of CompletionEntries to wait for. If 0, this just submits queued entries and returns. If > 0, it blocks until at least wanted number of entries were completed.@paramsig See io_uring_enter(2) man page@returnsNumber of submitted entries on success, -errno on error
submit
(
(local variable) uint queued
queued
);
if (
(local variable) const(int) submitted
submitted
< 0)
{ stderr.
std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @system
writefln
("submit on resized ring failed: errno %d", -
(local variable) const(int) submitted
submitted
);
return 1; } if (
(local variable) const(int) submitted
submitted
!=
(local variable) uint queued
queued
)
{ stderr.
std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @system
writefln
("submit accepted %d of %u queued SQEs on the resized ring",
(local variable) const(int) submitted
submitted
,
(local variable) uint queued
queued
);
return 1; }
void std.stdio.writefln!(char, const(uint), const(uint), const(uint), const(uint), const(int))(in char[] fmt, const(uint) __param_1, const(uint) __param_2, const(uint) __param_3, const(uint) __param_4, const(int) __param_5) @safe

Equivalent to writef(fmt, args, '\n').

writefln
("ok: resized live ring SQ %u->%u, CQ %u->%u; enlarged SQ accepted+submitted %u NOPs",
(local variable) const(uint) oldSq
oldSq
,
(local variable) const(uint) gotSq
gotSq
,
(local variable) const(uint) oldCq
oldCq
,
(local variable) const(uint) gotCq
gotCq
,
(local variable) const(int) submitted
submitted
);
return 0; }