#!/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_ringsio_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:
sets up a deliberately tiny ring (8 SQ entries),
resizes it up to 32 SQ / 64 CQ entries via io.resizeRings(newParams),
asserts the kernel now reports the larger SQ/CQ capacity (io.params), and
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) duringSimple idiomatic dlang wrapper around linux io_uring
(see: https://kernel.dk/io_uring.pdf) asynchronous API.
during;
import (package) corecore.(package) core.stdcstdc.(module) core.stdc.errnoD header file for C99.
pubs.opengroup.org/onlinepubs/009695399/basedefs/errno.h.html, errno.h
Source
core/stdc/errno.d
errno : (alias constant) io_uring_resize_rings.EINVAL = int core.stdc.errno.EINVAL = 22EINVAL, (alias constant) io_uring_resize_rings.EOPNOTSUPP = int core.stdc.errno.EOPNOTSUPP = 95EOPNOTSUPP, (alias constant) io_uring_resize_rings.ENOSYS = int core.stdc.errno.ENOSYS = 38ENOSYS;
import (package) stdstd.(module) std.stdioCategory 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:
The lowest layer is the operating system layer. The two main schemes are Windows and Posix.
C's stdio.h which unifies the two operating system schemes.
std.stdio, this module, unifies the various stdio.h implementations into
a high level package for D programs.
Source
std/stdio.d
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.UringMain 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 ioio;
const (local variable) const(int) setupRetsetupRet = (local variable) during.Uring ioio.int during.setup(ref during.Uring uring, uint entries = 128u, during.io_uring.SetupFlags flags = SetupFlags.NONE) nothrow @nogc @safeSetup new instance of io_uring into provided Uring structure.
setup(8, (enum) during.io_uring.SetupFlagsio_uring_setup() flags
SetupFlags.(enum value) during.io_uring.SetupFlags.SINGLE_ISSUER = 4096uIORING_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.SetupFlagsio_uring_setup() flags
SetupFlags.(enum value) during.io_uring.SetupFlags.DEFER_TASKRUN = 8192uIORING_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) setupRetsetupRet < 0)
{
void std.stdio.writefln!(char, const(int))(in char[] fmt, const(int) __param_1) @safeEquivalent to writef(fmt, args, '\n').
writefln("SKIP: io_uring_setup failed (errno %d) — io_uring unavailable on this host",
-(local variable) const(int) setupRetsetupRet);
return 0;
}
const uint (local variable) const(uint) oldSqoldSq = (local variable) during.Uring ioio.during.io_uring.SetupParameters during.Uring.params() const pure nothrow @nogc @safeio_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) oldCqoldCq = (local variable) during.Uring ioio.during.io_uring.SetupParameters during.Uring.params() const pure nothrow @nogc @safeio_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.SetupParametersPassed 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 newParamsnewParams;
(local variable) during.io_uring.SetupParameters newParamsnewParams.(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 newParamsnewParams.(field) uint during.io_uring.SetupParameters.cq_entries(output) allocated entries in completion queue
cq_entries = 64;
const (local variable) const(int) resizeRetresizeRet = (local variable) during.Uring ioio.int during.Uring.resizeRings(ref during.io_uring.SetupParameters newParams) nothrow @nogc @trustedResize 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 newParamsnewParams);
if ((local variable) const(int) resizeRetresizeRet < 0)
{
const (local variable) const(int) ee = -(local variable) const(int) resizeRetresizeRet;
// -EINVAL: pre-6.13 kernels don't know REGISTER_RESIZE_RINGS (opcode 33).
// -EOPNOTSUPP/-ENOSYS: register path disabled/unimplemented.
if ((local variable) const(int) ee == (constant) int core.stdc.errno.EINVAL = 22EINVAL || (local variable) const(int) ee == (constant) int core.stdc.errno.EOPNOTSUPP = 95EOPNOTSUPP || (local variable) const(int) ee == (constant) int core.stdc.errno.ENOSYS = 38ENOSYS)
{
void std.stdio.writefln!(char, const(int))(in char[] fmt, const(int) __param_1) @safeEquivalent to writef(fmt, args, '\n').
writefln("SKIP: IORING_REGISTER_RESIZE_RINGS unsupported (errno %d) — needs Linux 6.13+",
(local variable) const(int) ee);
return 0;
}
stderr.std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @systemwritefln("resizeRings failed unexpectedly: errno %d", (local variable) const(int) ee);
return 1;
}
// After a successful resize `io.params` reflects the new (kernel-allocated) sizes.
const uint (local variable) const(uint) gotSqgotSq = (local variable) during.Uring ioio.during.io_uring.SetupParameters during.Uring.params() const pure nothrow @nogc @safeio_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) gotCqgotCq = (local variable) during.Uring ioio.during.io_uring.SetupParameters during.Uring.params() const pure nothrow @nogc @safeio_uring parameters
params.(field) uint during.io_uring.SetupParameters.cq_entries(output) allocated entries in completion queue
cq_entries;
if ((local variable) const(uint) gotSqgotSq < 32 || (local variable) const(uint) gotCqgotCq < 64)
{
stderr.std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @systemwritefln("resize did not grow the rings: sq %u->%u (want >=32), cq %u->%u (want >=64)",
(local variable) const(uint) oldSqoldSq, (local variable) const(uint) gotSqgotSq, (local variable) const(uint) oldCqoldCq, (local variable) const(uint) gotCqgotCq);
return 1;
}
// The re-mapped SQ must expose the larger capacity too.
if ((local variable) during.Uring ioio.ulong during.Uring.capacity() const pure nothrow @nogc @safeAvailable 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 @systemwritefln("SQ capacity after resize is %u, expected >=32", cast(uint) (local variable) during.Uring ioio.ulong during.Uring.capacity() const pure nothrow @nogc @safeAvailable 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 = 20utoQueue = 20; // > oldSq (8), <= gotSq (32)
uint (local variable) uint queuedqueued;
foreach ((local variable) uint ii; 0 .. (constant) uint io_uring_resize_rings.main.toQueue = 20utoQueue)
{
if ((local variable) during.Uring ioio.bool during.Uring.full() const pure nothrow @nogc @safeCheck if there is space for another SubmissionEntry to submit.
full)
break;
(local variable) during.Uring ioio.putWith!((ref SubmissionEntry e, ulong tag) {
e.prepNop();
e.user_data = tag;
})(cast(ulong) (local variable) uint ii);
(local variable) uint queuedqueued++;
}
if ((local variable) uint queuedqueued < (constant) uint io_uring_resize_rings.main.toQueue = 20utoQueue)
{
stderr.std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @systemwritefln("enlarged SQ only accepted %u/%u SQEs — resize did not take effect",
(local variable) uint queuedqueued, (constant) uint io_uring_resize_rings.main.toQueue = 20utoQueue);
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) submittedsubmitted = (local variable) during.Uring ioio.int during.Uring.submit(uint want) nothrow @nogc @safeSubmits qued SubmissionEntry to be processed by kernel.
submit((local variable) uint queuedqueued);
if ((local variable) const(int) submittedsubmitted < 0)
{
stderr.std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @systemwritefln("submit on resized ring failed: errno %d", -(local variable) const(int) submittedsubmitted);
return 1;
}
if ((local variable) const(int) submittedsubmitted != (local variable) uint queuedqueued)
{
stderr.std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @systemwritefln("submit accepted %d of %u queued SQEs on the resized ring",
(local variable) const(int) submittedsubmitted, (local variable) uint queuedqueued);
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) @safeEquivalent 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) oldSqoldSq, (local variable) const(uint) gotSqgotSq, (local variable) const(uint) oldCqoldCq, (local variable) const(uint) gotCqgotCq, (local variable) const(int) submittedsubmitted);
return 0;
}