clock-min-timeout.dhover×115all
#!/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_timeout

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.

io_uring_clock_min_timeout
;
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) io_uring_clock_min_timeout.errno = int core.stdc.errno.__errno_location() nothrow @nogc ref @trusted
errno
,
(alias constant) io_uring_clock_min_timeout.EINVAL = int core.stdc.errno.EINVAL = 22
EINVAL
,
(alias constant) io_uring_clock_min_timeout.EOPNOTSUPP = int core.stdc.errno.EOPNOTSUPP = 95
EOPNOTSUPP
,
(alias constant) io_uring_clock_min_timeout.ETIME = int core.stdc.errno.ETIME = 62
ETIME
,
(alias constant) io_uring_clock_min_timeout.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_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 @system
syscall
(long
(parameter) long number
number
, ...) @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 = 29
IORING_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 = 1
CLOCK_MONOTONIC
= 1;
// Mirror of `struct io_uring_clock_register` from <linux/io_uring.h>. private struct
(struct) io_uring_clock_min_timeout.ClockRegister
ClockRegister
{ uint
(field) uint io_uring_clock_min_timeout.ClockRegister.clockid
clockid
;
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 = 101892364LU
cookie
= 0x6_12_C10C; // marks the TIMEOUT completion
(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);
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; } // (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.ClockRegister
ClockRegister
(local variable) io_uring_clock_min_timeout.ClockRegister clk
clk
;
(local variable) io_uring_clock_min_timeout.ClockRegister clk
clk
.
(field) uint io_uring_clock_min_timeout.ClockRegister.clockid
clockid
=
(constant) int io_uring_clock_min_timeout.CLOCK_MONOTONIC = 1
CLOCK_MONOTONIC
;
const
(local variable) const(int) clockRet
clockRet
= () @trusted {
const
(local variable) const(long) r
r
=
long io_uring_clock_min_timeout.syscall(long number, ...) nothrow @nogc @system
syscall
(
(constant) int io_uring_clock_min_timeout.__NR_io_uring_register = 427
__NR_io_uring_register
,
(local variable) during.Uring io
io
.
int during.Uring.fd() const pure nothrow @nogc @safe

Native io_uring file descriptor

fd
,
(constant) int io_uring_clock_min_timeout.IORING_REGISTER_CLOCK = 29
IORING_REGISTER_CLOCK
, cast(void*)&
(local variable) io_uring_clock_min_timeout.ClockRegister clk
clk
, 0);
return
(local variable) const(long) r
r
< 0 ? -
int core.stdc.errno.__errno_location() nothrow @nogc ref @trusted
errno
: cast(int)
(local variable) const(long) r
r
;
}(); if (
(local variable) const(int) clockRet
clockRet
== -
(constant) int core.stdc.errno.EINVAL = 22
EINVAL
||
(local variable) const(int) clockRet
clockRet
== -
(constant) int core.stdc.errno.EOPNOTSUPP = 95
EOPNOTSUPP
||
(local variable) const(int) clockRet
clockRet
== -
(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_CLOCK unsupported (errno %d) — needs Linux 6.12+", -
(local variable) const(int) clockRet
clockRet
);
return 0; } if (
(local variable) const(int) clockRet
clockRet
< 0)
{ stderr.
std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @system
writefln
("IORING_REGISTER_CLOCK(CLOCK_MONOTONIC) failed: errno %d", -
(local variable) const(int) clockRet
clockRet
);
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 fire
fire
=
(struct) during.io_uring.KernelTimespec

Time specification as defined in kernel headers (used by TIMEOUT operations)

KernelTimespec
(0, 20_000_000); // 20ms
(local variable) during.Uring io
io
.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 @safe

Adds 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().

@paramFN Function to fill next entry in queue by ref (should be faster). It is expected to be in a form of void function(ARGS)(ref SubmissionEntry, auto ref ARGS). Note that in this case queue entry is cleaned first before function is called.@paramentry Custom built SubmissionEntry to be posted as is. Note that in this case it is copied whole over one in the SubmissionQueue.@paramargs Optional arguments passed to the function@returnsreference to Uring structure so it's possible to chain multiple commands.
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) 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
(1);
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 failed: errno %d", -
(local variable) const(int) submitted
submitted
);
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 overall
overall
=
(struct) during.io_uring.KernelTimespec

Time 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 = 5000u
minWaitUsec
= 5_000; // 5ms minimum dwell
const
(local variable) const(int) waitRet
waitRet
=
(local variable) during.Uring io
io
.
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 @trusted

Submit 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 overall
overall
,
(constant) uint io_uring_clock_min_timeout.main.minWaitUsec = 5000u
minWaitUsec
);
if (
(local variable) const(int) waitRet
waitRet
== -
(constant) int core.stdc.errno.EINVAL = 22
EINVAL
||
(local variable) const(int) waitRet
waitRet
== -
(constant) int core.stdc.errno.EOPNOTSUPP = 95
EOPNOTSUPP
||
(local variable) const(int) waitRet
waitRet
== -
(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: min-timeout wait unsupported (errno %d) — needs Linux 6.12+", -
(local variable) const(int) waitRet
waitRet
);
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) waitRet
waitRet
< 0 &&
(local variable) const(int) waitRet
waitRet
!= -
(constant) int core.stdc.errno.ETIME = 62
ETIME
)
{ stderr.
std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @system
writefln
("submitAndWaitMinTimeout failed unexpectedly: errno %d", -
(local variable) const(int) waitRet
waitRet
);
return 1; } if (
(local variable) during.Uring io
io
.
bool during.Uring.empty() const pure nothrow @nogc @safe

Check if there is some CompletionEntry to process.

empty
)
{ stderr.
std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @system
writefln
("no completion ready after min-timeout wait (waitRet=%d)",
(local variable) const(int) waitRet
waitRet
);
return 1; } const
(local variable) const(int) res
res
=
(local variable) during.Uring io
io
.
during.io_uring.CompletionEntry during.Uring.front() pure nothrow @nogc return ref @safe

Get first CompletionEntry from cq ring

front
.
(field) int during.io_uring.CompletionEntry.res

result code for this event

res
;
const
(local variable) const(ulong) echoed
echoed
=
(local variable) during.Uring io
io
.
during.io_uring.CompletionEntry during.Uring.front() pure nothrow @nogc return ref @safe

Get first CompletionEntry from cq ring

front
.
(field) ulong during.io_uring.CompletionEntry.user_data

sqe->data submission passed back

user_data
;
(local variable) during.Uring io
io
.
void during.Uring.popFront() pure nothrow @nogc @safe

Move 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) res
res
!= -
(constant) int core.stdc.errno.ETIME = 62
ETIME
)
{ stderr.
std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @system
writefln
("TIMEOUT completed with unexpected res=%d (expected -ETIME=%d)",
(local variable) const(int) res
res
, -
(constant) int core.stdc.errno.ETIME = 62
ETIME
);
return 1; } if (
(local variable) const(ulong) echoed
echoed
!=
(constant) ulong io_uring_clock_min_timeout.main.cookie = 101892364LU
cookie
)
{ stderr.
std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @system
writefln
("user_data mismatch: expected 0x%X, got 0x%X",
(constant) ulong io_uring_clock_min_timeout.main.cookie = 101892364LU
cookie
,
(local variable) const(ulong) echoed
echoed
);
return 1; }
void std.stdio.writefln!(char, const(ulong))(in char[] fmt, const(ulong) __param_1) @safe

Equivalent 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) echoed
echoed
);
return 0; }