#!/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_waitidio_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) duringSimple idiomatic dlang wrapper around linux io_uring
(see: https://kernel.dk/io_uring.pdf) asynchronous API.
during;
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_waitid.writefln = std.stdio.writefln(alias fmt, A...)(A args) if (isSomeString!(typeof(fmt)))Equivalent to writef(fmt, args, '\n').
writefln, stderr;
import (package) corecore.(package) core.syssys.(package) core.sys.linuxlinux.(module) core.sys.linux.errnoD header file for GNU/Linux
errno : (alias constant) io_uring_waitid.EINVAL = int core.stdc.errno.EINVAL = 22EINVAL, (alias constant) io_uring_waitid.EOPNOTSUPP = int core.stdc.errno.EOPNOTSUPP = 95EOPNOTSUPP, (alias constant) io_uring_waitid.ENOSYS = int core.stdc.errno.ENOSYS = 38ENOSYS;
import (package) corecore.(package) core.syssys.(package) core.sys.posixposix.(module) core.sys.posix.signalD header file for POSIX.
Source
core/sys/posix/signal.d
signal : (struct) core.sys.posix.signal.siginfo_tsiginfo_t;
import (package) corecore.(package) core.syssys.(package) core.sys.posixposix.(package) core.sys.posix.syssys.(module) core.sys.posix.sys.waitD header file for POSIX.
wait : (alias) io_uring_waitid.waitpid = int core.sys.posix.sys.wait.waitpid(int, int*, int) nothrow @nogcwaitpid;
import (package) corecore.(package) core.syssys.(package) core.sys.posixposix.(module) core.sys.posix.unistdD header file for POSIX.
unistd : (alias) io_uring_waitid.fork = int core.sys.posix.unistd.fork() nothrow @nogc @trustedfork, (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 = 1P_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 = 4WEXITED = 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 = 1CLD_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 = 42childExitCode = 42;
int int D main()main()
{
(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;
}
// 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) probeprobe = (local variable) during.Uring ioio.during.Probe during.Uring.probe() nothrow @nogc @safeProbes supported operations
probe();
if (cast(bool) (local variable) const(during.Probe) probeprobe && !(local variable) const(during.Probe) probeprobe.bool during.Probe.isSupported(during.io_uring.Operation op) const pure nothrow @nogc @safeIs operation supported?
isSupported((enum) during.io_uring.OperationDescribes the operation to be performed
Operation.(enum value) during.io_uring.Operation.WAITID = cast(ubyte)50uIORING_OP_WAITID - async waitid(2)
WAITID))
{
void std.stdio.writefln!char(in char[] fmt) @safeEquivalent 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) pidpid = int core.sys.posix.unistd.fork() nothrow @nogc @trustedfork();
if ((local variable) const(int) pidpid < 0)
{
stderr.std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @systemwritefln("fork failed: errno %d", -(local variable) const(int) pidpid);
return 1;
}
if ((local variable) const(int) pidpid == 0)
noreturn core.sys.posix.unistd._exit(int) nothrow @nogc @trusted_exit((constant) int io_uring_waitid.childExitCode = 42childExitCode); // 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_tsiginfo_t (local variable) core.sys.posix.signal.siginfo_t infoinfo;
// 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 ioio.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 @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().
pid, &(local variable) core.sys.posix.signal.siginfo_t infoinfo);
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);
// Avoid leaving a zombie behind on the error path.
int (local variable) int statusstatus;
int core.sys.posix.sys.wait.waitpid(int, int*, int) nothrow @nogcwaitpid((local variable) const(int) pidpid, &(local variable) int statusstatus, 0);
return 1;
}
// Block for the single completion. Bounded: exactly one CQE is expected.
(local variable) during.Uring ioio.int during.Uring.wait(uint want = 1u) nothrow @nogcSimmilar to submit but with this method we just wait for required number
of CompletionEntries.
wait(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;
(local variable) during.Uring ioio.void during.Uring.popFront() pure nothrow @nogc @safeMove 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) resres == -(constant) int core.stdc.errno.EINVAL = 22EINVAL || (local variable) const(int) resres == -(constant) int core.stdc.errno.EOPNOTSUPP = 95EOPNOTSUPP || (local variable) const(int) resres == -(constant) int core.stdc.errno.ENOSYS = 38ENOSYS)
{
int (local variable) int statusstatus;
int core.sys.posix.sys.wait.waitpid(int, int*, int) nothrow @nogcwaitpid((local variable) const(int) pidpid, &(local variable) int statusstatus, 0);
void std.stdio.writefln!(char, const(int))(in char[] fmt, const(int) __param_1) @safeEquivalent to writef(fmt, args, '\n').
writefln("SKIP: IORING_OP_WAITID rejected with errno %d (kernel < 6.7?)", -(local variable) const(int) resres);
return 0;
}
if ((local variable) const(int) resres < 0)
{
stderr.std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @systemwritefln("WAITID completed with error: errno %d", -(local variable) const(int) resres);
int (local variable) int statusstatus;
int core.sys.posix.sys.wait.waitpid(int, int*, int) nothrow @nogcwaitpid((local variable) const(int) pidpid, &(local variable) int statusstatus, 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 infoinfo.(field) int core.sys.posix.signal.siginfo_t.si_codesi_code != (constant) int io_uring_waitid.CLD_EXITED = 1CLD_EXITED || (local variable) core.sys.posix.signal.siginfo_t infoinfo.int core.sys.posix.signal.siginfo_t.si_status!()() pure nothrow @nogc @property ref @safesi_status != (constant) int io_uring_waitid.childExitCode = 42childExitCode)
{
stderr.std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @systemwritefln("siginfo mismatch: si_code=%d (want %d), si_status=%d (want %d)",
(local variable) core.sys.posix.signal.siginfo_t infoinfo.(field) int core.sys.posix.signal.siginfo_t.si_codesi_code, (constant) int io_uring_waitid.CLD_EXITED = 1CLD_EXITED, (local variable) core.sys.posix.signal.siginfo_t infoinfo.int core.sys.posix.signal.siginfo_t.si_status!()() pure nothrow @nogc @property ref @safesi_status, (constant) int io_uring_waitid.childExitCode = 42childExitCode);
return 1;
}
// Confirm the child really is gone — a second waitpid should find no such child.
int (local variable) int statusstatus;
const (local variable) const(int) wpwp = int core.sys.posix.sys.wait.waitpid(int, int*, int) nothrow @nogcwaitpid((local variable) const(int) pidpid, &(local variable) int statusstatus, 0);
if ((local variable) const(int) wpwp > 0)
{
stderr.std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @systemwritefln("child %d was NOT reaped by IORING_OP_WAITID (waitpid returned it)", (local variable) const(int) pidpid);
return 1;
}
void std.stdio.writefln!(char, const(int), int)(in char[] fmt, const(int) __param_1, int __param_2) @safeEquivalent 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) pidpid, (local variable) core.sys.posix.signal.siginfo_t infoinfo.int core.sys.posix.signal.siginfo_t.si_status!()() pure nothrow @nogc @property ref @safesi_status);
return 0;
}