waitid.dhover×126all
#!/usr/bin/env dub
/+ dub.sdl:
    name "io_uring_waitid"
    dependency "during" version="~>0.5.0"
    platforms "linux"
    targetPath "build"
+/
/**
 * `io_uring` — asynchronously reap a child process (`IORING_OP_WAITID`, Linux 6.7).
 *
 * Before 6.7, the only way to reap a child without blocking a thread was to
 * juggle `SIGCHLD` handlers or poll `waitid(WNOHANG)` in your event loop.
 * `IORING_OP_WAITID` folds the reap into the ring like any other op: you submit
 * a `WAITID` SQE naming the child, and the completion fires when the child
 * changes state. No signal plumbing, no busy-polling.
 *
 * This example forks a child that immediately exits with a known code, submits a
 * `prepWaitid(P_PID, childpid, &siginfo, WEXITED, 0)`, waits for the CQE, and
 * verifies `res == 0` plus the `siginfo_t` the kernel filled in (`si_code ==
 * CLD_EXITED`, `si_status ==` the child's exit code). Modeled on the `during`
 * library's own `tests/waitid.d`.
 *
 * Companion to the io_uring chronology:
 * see docs/research/async-io/io-uring/timeline.md
 * § "6.7 — Futex, waitid, read-multishot (January 2024)".
 *
 * Run with: `dub run --single waitid.d`
 *
 * Portability: if the running kernel has no `io_uring`, or lacks `IORING_OP_WAITID`
 * (kernel < 6.7), the program prints a `SKIP:` line and exits 0 so it stays green
 * in CI regardless of the host kernel.
 */
module 
(module) io_uring_waitid

io_uring — asynchronously reap a child process (IORING_OP_WAITID, Linux 6.7).

Before 6.7, the only way to reap a child without blocking a thread was to juggle SIGCHLD handlers or poll waitid(WNOHANG) in your event loop. IORING_OP_WAITID folds the reap into the ring like any other op: you submit a WAITID SQE naming the child, and the completion fires when the child changes state. No signal plumbing, no busy-polling.

This example forks a child that immediately exits with a known code, submits a prepWaitid(P_PID, childpid, &siginfo, WEXITED, 0), waits for the CQE, and verifies res == 0 plus the siginfo_t the kernel filled in (si_code == CLD_EXITED, si_status == the child's exit code). Modeled on the during library's own tests/waitid.d.

Companion to the io_uring chronology: see docs/research/async-io/io-uring/timeline.md § "6.7 — Futex, waitid, read-multishot (January 2024)".

Run with: dub run --single waitid.d

Portability

if the running kernel has no io_uring, or lacks IORING_OP_WAITID (kernel < 6.7), the program prints a SKIP: line and exits 0 so it stays green in CI regardless of the host kernel.

io_uring_waitid
;
import
(module) during

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

during
;
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_waitid.writefln = std.stdio.writefln(alias fmt, A...)(A args) if (isSomeString!(typeof(fmt)))

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

writefln
, stderr;
import
(package) core
core
.
(package) core.sys
sys
.
(package) core.sys.linux
linux
.
(module) core.sys.linux.errno

D header file for GNU/Linux

glibc stdlib/errno.h

errno
:
(alias constant) io_uring_waitid.EINVAL = int core.stdc.errno.EINVAL = 22
EINVAL
,
(alias constant) io_uring_waitid.EOPNOTSUPP = int core.stdc.errno.EOPNOTSUPP = 95
EOPNOTSUPP
,
(alias constant) io_uring_waitid.ENOSYS = int core.stdc.errno.ENOSYS = 38
ENOSYS
;
import
(package) core
core
.
(package) core.sys
sys
.
(package) core.sys.posix
posix
.
(module) core.sys.posix.signal

D header file for POSIX.

Source

core/sys/posix/signal.d

@copyrightCopyright Sean Kelly 2005 - 2009.@licenseBoost License 1.0@authorsSean Kelly, Alex Rønne Petersen@standardsThe Open Group Base Specifications Issue 6, IEEE Std 1003.1, 2004 Edition
signal
:
(struct) core.sys.posix.signal.siginfo_t
siginfo_t
;
import
(package) core
core
.
(package) core.sys
sys
.
(package) core.sys.posix
posix
.
(package) core.sys.posix.sys
sys
.
(module) core.sys.posix.sys.wait

D header file for POSIX.

@copyrightCopyright Sean Kelly 2005 - 2009.@licenseBoost License 1.0.@authorsSean Kelly, Alex Rønne Petersen@standardsThe Open Group Base Specifications Issue 6, IEEE Std 1003.1, 2004 Edition
wait
:
(alias) io_uring_waitid.waitpid = int core.sys.posix.sys.wait.waitpid(int, int*, int) nothrow @nogc
waitpid
;
import
(package) core
core
.
(package) core.sys
sys
.
(package) core.sys.posix
posix
.
(module) core.sys.posix.unistd

D header file for POSIX.

@copyrightCopyright Sean Kelly 2005 - 2009.@licenseBoost License 1.0.@authorsSean Kelly@standardsThe Open Group Base Specifications Issue 8, IEEE Std 1003.1, 2024 Edition
unistd
:
(alias) io_uring_waitid.fork = int core.sys.posix.unistd.fork() nothrow @nogc @trusted
fork
,
(alias) io_uring_waitid._exit = noreturn core.sys.posix.unistd._exit(int) nothrow @nogc @trusted
_exit
;
// idtype_t values from <sys/wait.h>. druntime doesn't expose these portably, so // we hardcode the well-known constants. private enum
(constant) int io_uring_waitid.P_PID = 1
P_PID
= 1;
// `options` bits from <sys/wait.h>. `WEXITED` is mandatory for waitid(2): it asks // to wait for children that have terminated. private enum
(constant) int io_uring_waitid.WEXITED = 4
WEXITED
= 0x00000004;
// si_code value the kernel sets for a normally-exited child (from <bits/siginfo-consts.h>). private enum
(constant) int io_uring_waitid.CLD_EXITED = 1
CLD_EXITED
= 1;
// The exit code our child reports; we verify it round-trips through siginfo_t. private enum int
(constant) int io_uring_waitid.childExitCode = 42
childExitCode
= 42;
int
int D main()
main
()
{
(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; } // Probe up front: on a kernel older than 6.7 `IORING_OP_WAITID` is unknown and // we should skip cleanly rather than fork a child we'd have to reap by hand. const
(local variable) const(during.Probe) probe
probe
=
(local variable) during.Uring io
io
.
during.Probe during.Uring.probe() nothrow @nogc @safe

Probes supported operations

probe
();
if (cast(bool)
(local variable) const(during.Probe) probe
probe
&& !
(local variable) const(during.Probe) probe
probe
.
bool during.Probe.isSupported(during.io_uring.Operation op) const pure nothrow @nogc @safe

Is operation supported?

isSupported
(
(enum) during.io_uring.Operation

Describes the operation to be performed

@seeio_uring_enter(2)
Operation
.
(enum value) during.io_uring.Operation.WAITID = cast(ubyte)50u

IORING_OP_WAITID - async waitid(2)

WAITID
))
{
void std.stdio.writefln!char(in char[] fmt) @safe

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

writefln
("SKIP: IORING_OP_WAITID unsupported on this kernel (needs Linux >= 6.7)");
return 0; } // Fork the child *after* the ring is up. The child exits immediately with a // known code; the parent reaps it asynchronously through the ring. const
(local variable) const(int) pid
pid
=
int core.sys.posix.unistd.fork() nothrow @nogc @trusted
fork
();
if (
(local variable) const(int) pid
pid
< 0)
{ stderr.
std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @system
writefln
("fork failed: errno %d", -
(local variable) const(int) pid
pid
);
return 1; } if (
(local variable) const(int) pid
pid
== 0)
noreturn core.sys.posix.unistd._exit(int) nothrow @nogc @trusted
_exit
(
(constant) int io_uring_waitid.childExitCode = 42
childExitCode
); // child path — never returns.
// The kernel writes the reaped child's status into this struct. It must stay // alive (and addressable) until the completion arrives, hence a plain stack local.
(struct) core.sys.posix.signal.siginfo_t
siginfo_t
(local variable) core.sys.posix.signal.siginfo_t info
info
;
// Place the WAITID SQE: wait on this specific pid (P_PID), accept terminated // children (WEXITED), and have the kernel fill `&info` with the child status.
(local variable) during.Uring io
io
.putWith!(
(ref SubmissionEntry e, int p, siginfo_t* infop) { e.prepWaitid(P_PID, cast(uint) p, infop, WEXITED, 0); e.user_data = 1; })(
during.Uring during.Uring.putWith!(function (ref during.io_uring.SubmissionEntry e, int p, core.sys.posix.signal.siginfo_t* infop) nothrow @nogc @safe { prepWaitid(e, 1, cast(uint)p, infop, 4, 0u); e.user_data = 1LU; } , const(int), core.sys.posix.signal.siginfo_t*)(ref const(int) __param_0, core.sys.posix.signal.siginfo_t* __param_1) 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.
pid
, &
(local variable) core.sys.posix.signal.siginfo_t info
info
);
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
);
// Avoid leaving a zombie behind on the error path. int
(local variable) int status
status
;
int core.sys.posix.sys.wait.waitpid(int, int*, int) nothrow @nogc
waitpid
(
(local variable) const(int) pid
pid
, &
(local variable) int status
status
, 0);
return 1; } // Block for the single completion. Bounded: exactly one CQE is expected.
(local variable) during.Uring io
io
.
int during.Uring.wait(uint want = 1u) nothrow @nogc

Simmilar to submit but with this method we just wait for required number of CompletionEntries.

@returns0 on success, -errno on error
wait
(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
;
(local variable) during.Uring io
io
.
void during.Uring.popFront() pure nothrow @nogc @safe

Move to next CompletionEntry

popFront
();
// Some kernels surface "op unknown" only at completion time. Treat the // canonical unsupported errnos as a SKIP, reaping the child synchronously so // we don't leak a zombie. if (
(local variable) const(int) res
res
== -
(constant) int core.stdc.errno.EINVAL = 22
EINVAL
||
(local variable) const(int) res
res
== -
(constant) int core.stdc.errno.EOPNOTSUPP = 95
EOPNOTSUPP
||
(local variable) const(int) res
res
== -
(constant) int core.stdc.errno.ENOSYS = 38
ENOSYS
)
{ int
(local variable) int status
status
;
int core.sys.posix.sys.wait.waitpid(int, int*, int) nothrow @nogc
waitpid
(
(local variable) const(int) pid
pid
, &
(local variable) int status
status
, 0);
void std.stdio.writefln!(char, const(int))(in char[] fmt, const(int) __param_1) @safe

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

writefln
("SKIP: IORING_OP_WAITID rejected with errno %d (kernel < 6.7?)", -
(local variable) const(int) res
res
);
return 0; } if (
(local variable) const(int) res
res
< 0)
{ stderr.
std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @system
writefln
("WAITID completed with error: errno %d", -
(local variable) const(int) res
res
);
int
(local variable) int status
status
;
int core.sys.posix.sys.wait.waitpid(int, int*, int) nothrow @nogc
waitpid
(
(local variable) const(int) pid
pid
, &
(local variable) int status
status
, 0);
return 1; } // The kernel reaped the child for us and populated `info`. // For a WEXITED reap, si_code is CLD_EXITED and si_status is the raw exit code. if (
(local variable) core.sys.posix.signal.siginfo_t info
info
.
(field) int core.sys.posix.signal.siginfo_t.si_code
si_code
!=
(constant) int io_uring_waitid.CLD_EXITED = 1
CLD_EXITED
||
(local variable) core.sys.posix.signal.siginfo_t info
info
.
int core.sys.posix.signal.siginfo_t.si_status!()() pure nothrow @nogc @property ref @safe
si_status
!=
(constant) int io_uring_waitid.childExitCode = 42
childExitCode
)
{ stderr.
std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @system
writefln
("siginfo mismatch: si_code=%d (want %d), si_status=%d (want %d)",
(local variable) core.sys.posix.signal.siginfo_t info
info
.
(field) int core.sys.posix.signal.siginfo_t.si_code
si_code
,
(constant) int io_uring_waitid.CLD_EXITED = 1
CLD_EXITED
,
(local variable) core.sys.posix.signal.siginfo_t info
info
.
int core.sys.posix.signal.siginfo_t.si_status!()() pure nothrow @nogc @property ref @safe
si_status
,
(constant) int io_uring_waitid.childExitCode = 42
childExitCode
);
return 1; } // Confirm the child really is gone — a second waitpid should find no such child. int
(local variable) int status
status
;
const
(local variable) const(int) wp
wp
=
int core.sys.posix.sys.wait.waitpid(int, int*, int) nothrow @nogc
waitpid
(
(local variable) const(int) pid
pid
, &
(local variable) int status
status
, 0);
if (
(local variable) const(int) wp
wp
> 0)
{ stderr.
std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @system
writefln
("child %d was NOT reaped by IORING_OP_WAITID (waitpid returned it)",
(local variable) const(int) pid
pid
);
return 1; }
void std.stdio.writefln!(char, const(int), int)(in char[] fmt, const(int) __param_1, int __param_2) @safe

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

writefln
("ok: IORING_OP_WAITID reaped child %d asynchronously (si_code=CLD_EXITED, exit code %d)",
(local variable) const(int) pid
pid
,
(local variable) core.sys.posix.signal.siginfo_t info
info
.
int core.sys.posix.signal.siginfo_t.si_status!()() pure nothrow @nogc @property ref @safe
si_status
);
return 0; }