#!/usr/bin/env dub
/+ dub.sdl:
name "io_uring_clock_min_timeout"
dependency "during" version="~>0.5.0"
platforms "linux"
targetPath "build"
+/
/**
* `io_uring` — registered wait clock + min-timeout batched wait (Linux 6.12).
*
* 6.12 added two cooperating knobs for the batched-wait path:
*
* * `IORING_REGISTER_CLOCK` selects which kernel clock source backs ring-side
* timeouts. The default is `CLOCK_MONOTONIC`; re-registering it here is a
* deliberate no-op that simply exercises the register wire-up. A program
* that wants suspend-aware deadlines would instead register `CLOCK_BOOTTIME`.
*
* * The *min-timeout* batched wait lets a single `io_uring_enter` block for an
* overall deadline while guaranteeing a minimum dwell time, so the kernel can
* accumulate a batch of completions without an extra wakeup/context-switch per
* event. `during` exposes it as `submitAndWaitMinTimeout(want, ts, minWaitUsec)`,
* which drives the `io_uring_getevents_arg` (EXT_ARG) enter path.
*
* This example registers `CLOCK_MONOTONIC`, queues a short relative `TIMEOUT`
* SQE, submits it, then performs a min-timeout wait. The TIMEOUT op fires with
* `-ETIME`, proving the registered clock + min-timeout wait drove a real timed
* completion.
*
* Two `during` 0.5.0 quirks are worked around here (both noted inline):
* 1. `Uring.registerClock` passes `nr_args = 1`, but the kernel's
* `IORING_REGISTER_CLOCK` handler requires `nr_args == 0` and otherwise
* returns `-EINVAL`. We issue the `io_uring_register(2)` syscall directly.
* 2. `Uring.submitAndWaitMinTimeout` only *waits* (`to_submit = 0`); it does not
* flush pending SQEs. We `io.submit(1)` the TIMEOUT first, then wait.
*
* Companion to the io_uring chronology:
* see docs/research/async-io/io-uring/timeline.md
* § "6.12 — Clock source, buffer cloning, min-timeout (November 2024)".
*
* Run with: `dub run --single clock-min-timeout.d`
*
* Portability: if the running kernel has no `io_uring`, or is older than 6.12
* (so `IORING_REGISTER_CLOCK` / the min-timeout enter return
* `-EINVAL`/`-EOPNOTSUPP`/`-ENOSYS`), the program prints a `SKIP:` line and exits
* 0 so it stays green in CI.
*/
module (module) io_uring_clock_min_timeoutio_uring — registered wait clock + min-timeout batched wait (Linux 6.12).
6.12 added two cooperating knobs for the batched-wait path:
IORING_REGISTER_CLOCK selects which kernel clock source backs ring-side
timeouts. The default is CLOCK_MONOTONIC; re-registering it here is a
deliberate no-op that simply exercises the register wire-up. A program
that wants suspend-aware deadlines would instead register CLOCK_BOOTTIME.
The min-timeout batched wait lets a single io_uring_enter block for an
overall deadline while guaranteeing a minimum dwell time, so the kernel can
accumulate a batch of completions without an extra wakeup/context-switch per
event. during exposes it as submitAndWaitMinTimeout(want, ts, minWaitUsec),
which drives the io_uring_getevents_arg (EXT_ARG) enter path.
This example registers CLOCK_MONOTONIC, queues a short relative TIMEOUT
SQE, submits it, then performs a min-timeout wait. The TIMEOUT op fires with
-ETIME, proving the registered clock + min-timeout wait drove a real timed
completion.
Two during 0.5.0 quirks are worked around here (both noted inline):
Uring.registerClock passes nr_args = 1, but the kernel's
IORING_REGISTER_CLOCK handler requires nr_args == 0 and otherwise
returns -EINVAL. We issue the io_uring_register(2) syscall directly.
Uring.submitAndWaitMinTimeout only waits (to_submit = 0); it does not
flush pending SQEs. We io.submit(1) the TIMEOUT first, then wait.
Companion to the io_uring chronology:
see docs/research/async-io/io-uring/timeline.md
§ "6.12 — Clock source, buffer cloning, min-timeout (November 2024)".
Run with: dub run --single clock-min-timeout.d
Portability
if the running kernel has no io_uring, or is older than 6.12
(so IORING_REGISTER_CLOCK / the min-timeout enter return
-EINVAL/-EOPNOTSUPP/-ENOSYS), the program prints a SKIP: line and exits
0 so it stays green in CI.
io_uring_clock_min_timeout;
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) io_uring_clock_min_timeout.errno = int core.stdc.errno.__errno_location() nothrow @nogc ref @trustederrno, (alias constant) io_uring_clock_min_timeout.EINVAL = int core.stdc.errno.EINVAL = 22EINVAL, (alias constant) io_uring_clock_min_timeout.EOPNOTSUPP = int core.stdc.errno.EOPNOTSUPP = 95EOPNOTSUPP, (alias constant) io_uring_clock_min_timeout.ETIME = int core.stdc.errno.ETIME = 62ETIME, (alias constant) io_uring_clock_min_timeout.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_clock_min_timeout.writefln = std.stdio.writefln(alias fmt, A...)(A args) if (isSomeString!(typeof(fmt)))Equivalent to writef(fmt, args, '\n').
writefln, stderr;
// Raw `io_uring_register(2)` access — needed to work around during 0.5.0 passing
// the wrong `nr_args` for `IORING_REGISTER_CLOCK` (see header note #1).
extern (C) long long io_uring_clock_min_timeout.syscall(long number, ...) nothrow @nogc @systemsyscall(long (parameter) long numbernumber, ...) @system nothrow @nogc;
private enum (constant) int io_uring_clock_min_timeout.__NR_io_uring_register = 427__NR_io_uring_register = 427; // x86_64
private enum (constant) int io_uring_clock_min_timeout.IORING_REGISTER_CLOCK = 29IORING_REGISTER_CLOCK = 29; // RegisterOpCode.REGISTER_CLOCK
// Posix clock id. `CLOCK_MONOTONIC` is the io_uring default; druntime's posix
// bindings don't surface it portably here, so define the constant locally.
private enum (constant) int io_uring_clock_min_timeout.CLOCK_MONOTONIC = 1CLOCK_MONOTONIC = 1;
// Mirror of `struct io_uring_clock_register` from <linux/io_uring.h>.
private struct (struct) io_uring_clock_min_timeout.ClockRegisterClockRegister
{
uint (field) uint io_uring_clock_min_timeout.ClockRegister.clockidclockid;
uint[3] (field) uint[3] io_uring_clock_min_timeout.ClockRegister.__resv__resv;
}
int int D main()main()
{
enum ulong (constant) ulong io_uring_clock_min_timeout.main.cookie = 101892364LUcookie = 0x6_12_C10C; // marks the TIMEOUT completion
(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);
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;
}
// (1) IORING_REGISTER_CLOCK (6.12): choose the clock source backing ring-side
// timeouts. Re-registering the default CLOCK_MONOTONIC is a no-op but proves
// the register path works on this kernel; pre-6.12 kernels reject it.
//
// We bypass during's `registerClock` because it passes nr_args=1, which the
// kernel's REGISTER_CLOCK handler rejects with -EINVAL on every kernel; the
// handler requires nr_args == 0.
(struct) io_uring_clock_min_timeout.ClockRegisterClockRegister (local variable) io_uring_clock_min_timeout.ClockRegister clkclk;
(local variable) io_uring_clock_min_timeout.ClockRegister clkclk.(field) uint io_uring_clock_min_timeout.ClockRegister.clockidclockid = (constant) int io_uring_clock_min_timeout.CLOCK_MONOTONIC = 1CLOCK_MONOTONIC;
const (local variable) const(int) clockRetclockRet = () @trusted {
const (local variable) const(long) rr = long io_uring_clock_min_timeout.syscall(long number, ...) nothrow @nogc @systemsyscall((constant) int io_uring_clock_min_timeout.__NR_io_uring_register = 427__NR_io_uring_register, (local variable) during.Uring ioio.int during.Uring.fd() const pure nothrow @nogc @safeNative io_uring file descriptor
fd, (constant) int io_uring_clock_min_timeout.IORING_REGISTER_CLOCK = 29IORING_REGISTER_CLOCK, cast(void*)&(local variable) io_uring_clock_min_timeout.ClockRegister clkclk, 0);
return (local variable) const(long) rr < 0 ? -int core.stdc.errno.__errno_location() nothrow @nogc ref @trustederrno : cast(int)(local variable) const(long) rr;
}();
if ((local variable) const(int) clockRetclockRet == -(constant) int core.stdc.errno.EINVAL = 22EINVAL || (local variable) const(int) clockRetclockRet == -(constant) int core.stdc.errno.EOPNOTSUPP = 95EOPNOTSUPP || (local variable) const(int) clockRetclockRet == -(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_CLOCK unsupported (errno %d) — needs Linux 6.12+", -(local variable) const(int) clockRetclockRet);
return 0;
}
if ((local variable) const(int) clockRetclockRet < 0)
{
stderr.std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @systemwritefln("IORING_REGISTER_CLOCK(CLOCK_MONOTONIC) failed: errno %d", -(local variable) const(int) clockRetclockRet);
return 1;
}
// (2) Queue a relative TIMEOUT that fires after 20ms. `count = 0` means the
// timeout completes purely on elapsed time (no completion-count trigger), so
// the resulting CQE is guaranteed to be -ETIME.
auto (local variable) during.io_uring.KernelTimespec firefire = (struct) during.io_uring.KernelTimespecTime specification as defined in kernel headers (used by TIMEOUT operations)
KernelTimespec(0, 20_000_000); // 20ms
(local variable) during.Uring ioio.putWith!((ref SubmissionEntry e, ref KernelTimespec t) {
e.prepTimeout(t, /*count*/ 0, TimeoutFlags.REL);
e.user_data = cookie;
})(during.Uring during.Uring.putWith!(function (ref during.io_uring.SubmissionEntry e, ref during.io_uring.KernelTimespec t) nothrow @nogc @safe
{
prepTimeout(e, t, 0LU, TimeoutFlags.REL);
e.user_data = 101892364LU;
}
, during.io_uring.KernelTimespec)(ref during.io_uring.KernelTimespec __param_0) nothrow @nogc return ref @safeAdds new entry to the SubmissionQueue.
Note that this just adds entry to the queue and doesn't advance the tail
marker kernel sees. For that finishSq() is needed to be called next.
Also note that to actually enter new entries to kernel,
it's needed to call submit().
fire);
// submitAndWaitMinTimeout only *waits* (to_submit=0) in during 0.5.0, so flush
// the queued TIMEOUT SQE ourselves first.
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(1);
if ((local variable) const(int) submittedsubmitted < 0)
{
stderr.std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @systemwritefln("submit failed: errno %d", -(local variable) const(int) submittedsubmitted);
return 1;
}
// (3) min-timeout batched wait (6.12): one io_uring_enter that blocks for at
// most `overall` (500ms), but is asked to dwell at least `minWaitUsec` (5ms)
// before returning so the kernel can batch completions without an extra
// wakeup. The 20ms TIMEOUT comfortably fires inside the 500ms ceiling. This
// drives the EXT_ARG `io_uring_getevents_arg` enter path.
auto (local variable) during.io_uring.KernelTimespec overalloverall = (struct) during.io_uring.KernelTimespecTime specification as defined in kernel headers (used by TIMEOUT operations)
KernelTimespec(0, 500_000_000); // 500ms ceiling — bounds the wait
enum uint (constant) uint io_uring_clock_min_timeout.main.minWaitUsec = 5000uminWaitUsec = 5_000; // 5ms minimum dwell
const (local variable) const(int) waitRetwaitRet = (local variable) during.Uring ioio.int during.Uring.submitAndWaitMinTimeout(uint want, ref const(during.io_uring.KernelTimespec) ts, uint minWaitUsec, const(core.sys.posix.signal.sigset_t)* sigmask = null) nothrow @nogc @trustedSubmit pending SQEs and wait for at least want CQEs with an absolute timeout ts
and a minimum wait minWaitUsec (the kernel will let through completions arriving
sooner than ts once it has waited at least minWaitUsec microseconds).
Note
Available from Linux 6.13
submitAndWaitMinTimeout(1, (local variable) during.io_uring.KernelTimespec overalloverall, (constant) uint io_uring_clock_min_timeout.main.minWaitUsec = 5000uminWaitUsec);
if ((local variable) const(int) waitRetwaitRet == -(constant) int core.stdc.errno.EINVAL = 22EINVAL || (local variable) const(int) waitRetwaitRet == -(constant) int core.stdc.errno.EOPNOTSUPP = 95EOPNOTSUPP || (local variable) const(int) waitRetwaitRet == -(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: min-timeout wait unsupported (errno %d) — needs Linux 6.12+", -(local variable) const(int) waitRetwaitRet);
return 0;
}
// A valid wait returns 0 (the requested completion is ready) or -ETIME if the
// overall ceiling elapsed first. Either way we expect our 20ms TIMEOUT CQE.
if ((local variable) const(int) waitRetwaitRet < 0 && (local variable) const(int) waitRetwaitRet != -(constant) int core.stdc.errno.ETIME = 62ETIME)
{
stderr.std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @systemwritefln("submitAndWaitMinTimeout failed unexpectedly: errno %d", -(local variable) const(int) waitRetwaitRet);
return 1;
}
if ((local variable) during.Uring ioio.bool during.Uring.empty() const pure nothrow @nogc @safeCheck if there is some CompletionEntry to process.
empty)
{
stderr.std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @systemwritefln("no completion ready after min-timeout wait (waitRet=%d)", (local variable) const(int) waitRetwaitRet);
return 1;
}
const (local variable) const(int) resres = (local variable) during.Uring ioio.during.io_uring.CompletionEntry during.Uring.front() pure nothrow @nogc return ref @safeGet first CompletionEntry from cq ring
front.(field) int during.io_uring.CompletionEntry.resresult code for this event
res;
const (local variable) const(ulong) echoedechoed = (local variable) during.Uring ioio.during.io_uring.CompletionEntry during.Uring.front() pure nothrow @nogc return ref @safeGet first CompletionEntry from cq ring
front.(field) ulong during.io_uring.CompletionEntry.user_datasqe->data submission passed back
user_data;
(local variable) during.Uring ioio.void during.Uring.popFront() pure nothrow @nogc @safeMove to next CompletionEntry
popFront();
// The TIMEOUT op reports -ETIME when it fires by elapsed time. Anything else
// means the op didn't behave as a relative timer.
if ((local variable) const(int) resres != -(constant) int core.stdc.errno.ETIME = 62ETIME)
{
stderr.std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @systemwritefln("TIMEOUT completed with unexpected res=%d (expected -ETIME=%d)", (local variable) const(int) resres, -(constant) int core.stdc.errno.ETIME = 62ETIME);
return 1;
}
if ((local variable) const(ulong) echoedechoed != (constant) ulong io_uring_clock_min_timeout.main.cookie = 101892364LUcookie)
{
stderr.std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @systemwritefln("user_data mismatch: expected 0x%X, got 0x%X", (constant) ulong io_uring_clock_min_timeout.main.cookie = 101892364LUcookie, (local variable) const(ulong) echoedechoed);
return 1;
}
void std.stdio.writefln!(char, const(ulong))(in char[] fmt, const(ulong) __param_1) @safeEquivalent to writef(fmt, args, '\n').
writefln("ok: registered CLOCK_MONOTONIC and the min-timeout batched wait "
~ "fired the relative TIMEOUT (res=-ETIME, user_data 0x%X)", (local variable) const(ulong) echoedechoed);
return 0;
}