#!/usr/bin/env dub
/+ dub.sdl:
name "io_uring_sync_cancel"
dependency "during" version="~>0.5.0"
platforms "linux"
targetPath "build"
+/
/**
* `io_uring` — synchronous cancellation from userspace
* (`IORING_REGISTER_SYNC_CANCEL`, Linux 6.0).
*
* Before 6.0 the only way to cancel an in-flight request was to submit an
* `IORING_OP_ASYNC_CANCEL` SQE and then reap *its* completion plus the
* cancelled op's completion — an asynchronous, two-CQE dance. 6.0 added a
* `register`-family opcode that cancels matching requests **synchronously**:
* the `io_uring_register(REGISTER_SYNC_CANCEL, …)` call blocks until the
* matching request(s) are torn down and returns the count, with no cancel SQE
* and no extra CQE.
*
* This example:
* 1. Opens a pipe and arms a `POLL_ADD` for `POLLIN` on the read end. Nothing
* is ever written, so the poll would block forever — a perfect stand-in
* for a genuinely in-flight request. The SQE carries `user_data = 1`.
* 2. Fills an `io_uring_sync_cancel_reg` with `addr = 1` (match by the same
* `user_data`; `flags = 0` selects user_data matching) and calls
* `io.registerSyncCancel(reg)`.
* 3. Reaps the poll's CQE and asserts it came back with `-ECANCELED`.
*
* Companion to the io_uring chronology:
* see docs/research/async-io/io-uring/timeline.md
* § "6.0 — Zero-copy send, single-issuer, sync cancel (October 2022)".
*
* Run with: `dub run --single sync-cancel.d`
*
* Portability: prints a `SKIP:` line and exits 0 when io_uring is unavailable
* (old kernel / sandbox) or when `REGISTER_SYNC_CANCEL` is missing (kernel
* < 6.0, reported as `-EINVAL` / `-ENOSYS`). It returns nonzero only if a call
* that should have worked fails. This host runs kernel 6.18, where the feature
* is present and is exercised for real.
*/
module (module) io_uring_sync_cancelio_uring — synchronous cancellation from userspace
(IORING_REGISTER_SYNC_CANCEL, Linux 6.0).
Before 6.0 the only way to cancel an in-flight request was to submit an
IORING_OP_ASYNC_CANCEL SQE and then reap its completion plus the
cancelled op's completion — an asynchronous, two-CQE dance. 6.0 added a
register-family opcode that cancels matching requests synchronously:
the io_uring_register(REGISTER_SYNC_CANCEL, …) call blocks until the
matching request(s) are torn down and returns the count, with no cancel SQE
and no extra CQE.
This example:
Opens a pipe and arms a POLL_ADD for POLLIN on the read end. Nothing
is ever written, so the poll would block forever — a perfect stand-in
for a genuinely in-flight request. The SQE carries user_data = 1.
Fills an io_uring_sync_cancel_reg with addr = 1 (match by the same
user_data; flags = 0 selects user_data matching) and calls
io.registerSyncCancel(reg).
Reaps the poll's CQE and asserts it came back with -ECANCELED.
Companion to the io_uring chronology:
see docs/research/async-io/io-uring/timeline.md
§ "6.0 — Zero-copy send, single-issuer, sync cancel (October 2022)".
Run with: dub run --single sync-cancel.d
Portability
prints a SKIP: line and exits 0 when io_uring is unavailable
(old kernel / sandbox) or when REGISTER_SYNC_CANCEL is missing (kernel
< 6.0, reported as -EINVAL / -ENOSYS). It returns nonzero only if a call
that should have worked fails. This host runs kernel 6.18, where the feature
is present and is exercised for real.
io_uring_sync_cancel;
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.syssys.(package) core.sys.linuxlinux.(module) core.sys.linux.errnoD header file for GNU/Linux
errno : (alias constant) io_uring_sync_cancel.ECANCELED = int core.stdc.errno.ECANCELED = 125ECANCELED, (alias constant) io_uring_sync_cancel.EINTR = int core.stdc.errno.EINTR = 4EINTR, (alias constant) io_uring_sync_cancel.EINVAL = int core.stdc.errno.EINVAL = 22EINVAL, (alias constant) io_uring_sync_cancel.ENOSYS = int core.stdc.errno.ENOSYS = 38ENOSYS;
import (package) corecore.(package) core.syssys.(package) core.sys.posixposix.(module) core.sys.posix.unistdD header file for POSIX.
unistd : (alias) io_uring_sync_cancel.close = int core.sys.posix.unistd.close(int) nothrow @nogc @trustedclose, (alias) io_uring_sync_cancel.pipe = int core.sys.posix.unistd.pipe(ref int[2]) nothrow @nogc @trustedpipe;
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_sync_cancel.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()
{
// The cookie we will both tag the poll with and match against on cancel.
enum ulong (constant) ulong io_uring_sync_cancel.main.cookie = 1LUcookie = 1;
(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;
}
// A pipe with no writer: POLLIN on the read end can never become ready, so
// the poll request stays genuinely in-flight until we cancel it.
int[2] (local variable) int[2] fdsfds;
if (int core.sys.posix.unistd.pipe(ref int[2]) nothrow @nogc @trustedpipe((local variable) int[2] fdsfds) != 0)
{
stderr.std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @systemwritefln("pipe() failed");
return 1;
}
scope (exit) { int core.sys.posix.unistd.close(int) nothrow @nogc @trustedclose((local variable) int[2] fdsfds[0]); int core.sys.posix.unistd.close(int) nothrow @nogc @trustedclose((local variable) int[2] fdsfds[1]); }
// Arm a single POLL_ADD for readability on the read end, tagged with `cookie`.
(local variable) during.Uring ioio.putWith!((ref SubmissionEntry e, int fd) {
e.prepPollAdd(fd, PollEvents.IN);
e.user_data = cookie;
})(during.Uring during.Uring.putWith!(function (ref during.io_uring.SubmissionEntry e, int fd) nothrow @nogc @safe
{
prepPollAdd(e, fd, PollEvents.IN, PollFlags.NONE);
e.user_data = 1LU;
}
, int)(ref int __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().
fds[0]);
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(0); // submit without waiting — nothing will complete yet
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;
}
// Synchronous cancel: match by user_data (flags = 0, the default selector).
// timeout {0,0} means "don't wait for the cancel itself to settle"; a poll
// is cancellable immediately so this returns the match count right away.
(struct) during.io_uring.io_uring_sync_cancel_regArgument to IORING_REGISTER_SYNC_CANCEL. Synchronously cancels matching in-flight
requests; addr, fd, flags, and opcode act as match keys (combined the same way as
IORING_OP_ASYNC_CANCEL). timeout bounds the cancel wait — {-1, -1} means "no timeout".
Note
Available from Linux 6.0
io_uring_sync_cancel_reg (local variable) during.io_uring.io_uring_sync_cancel_reg regreg;
(local variable) during.io_uring.io_uring_sync_cancel_reg regreg.(field) ulong during.io_uring.io_uring_sync_cancel_reg.addraddr = (constant) ulong io_uring_sync_cancel.main.cookie = 1LUcookie; // match key: the poll's user_data
(local variable) during.io_uring.io_uring_sync_cancel_reg regreg.(field) int during.io_uring.io_uring_sync_cancel_reg.fdfd = -1; // unused when matching by user_data
(local variable) during.io_uring.io_uring_sync_cancel_reg regreg.(field) uint during.io_uring.io_uring_sync_cancel_reg.flagsflags = 0; // 0 => match by user_data
(local variable) during.io_uring.io_uring_sync_cancel_reg regreg.(field) ubyte during.io_uring.io_uring_sync_cancel_reg.opcodeopcode = 0;
(local variable) during.io_uring.io_uring_sync_cancel_reg regreg.(field) during.io_uring.KernelTimespec during.io_uring.io_uring_sync_cancel_reg.timeouttimeout.(field) long during.io_uring.KernelTimespec.tv_secseconds
tv_sec = 0;
(local variable) during.io_uring.io_uring_sync_cancel_reg regreg.(field) during.io_uring.KernelTimespec during.io_uring.io_uring_sync_cancel_reg.timeouttimeout.(field) long during.io_uring.KernelTimespec.tv_nsecnanoseconds
tv_nsec = 0;
const (local variable) const(int) cretcret = (local variable) during.Uring ioio.int during.Uring.registerSyncCancel(ref scope during.io_uring.io_uring_sync_cancel_reg reg) nothrow @nogc @trustedSynchronously cancel one or more in-flight requests matching the keys in reg. Returns
the number of cancelled requests on success, -errno on failure. Use
KernelTimespec(-1, -1) in ``reg.timeout to wait indefinitely.
Note
Available from Linux 6.0
registerSyncCancel((local variable) during.io_uring.io_uring_sync_cancel_reg regreg);
if ((local variable) const(int) cretcret == -(constant) int core.stdc.errno.EINVAL = 22EINVAL || (local variable) const(int) cretcret == -(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_SYNC_CANCEL unsupported (errno %d) — needs Linux 6.0+",
-(local variable) const(int) cretcret);
return 0;
}
if ((local variable) const(int) cretcret < 0)
{
stderr.std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @systemwritefln("registerSyncCancel failed: errno %d", -(local variable) const(int) cretcret);
return 1;
}
// A successful synchronous cancel guarantees the cancelled request's CQE is
// already enqueued, so the completion is here now. Bound the wait anyway so a
// misbehaving kernel can't hang us: `submitAndWaitMinTimeout` blocks at most
// `ts` (one second) before giving up. (EXT_ARG-style waits need Linux 5.11+,
// which is implied by the 6.0 feature we are already on.)
const (local variable) const(during.io_uring.KernelTimespec) tsts = (struct) during.io_uring.KernelTimespecTime specification as defined in kernel headers (used by TIMEOUT operations)
KernelTimespec(1, 0); // {1s, 0ns}
const (local variable) const(int) waitedwaited = (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) const(during.io_uring.KernelTimespec) tsts, 0);
if ((local variable) const(int) waitedwaited < 0 || (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 after sync cancel (wait returned %d)", (local variable) const(int) waitedwaited);
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();
if ((local variable) const(ulong) echoedechoed != (constant) ulong io_uring_sync_cancel.main.cookie = 1LUcookie)
{
stderr.std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @systemwritefln("user_data mismatch: expected %d, got %d", (constant) ulong io_uring_sync_cancel.main.cookie = 1LUcookie, (local variable) const(ulong) echoedechoed);
return 1;
}
// A cancelled request reports -ECANCELED (some kernels surface -EINTR for
// interrupted ops); either confirms the synchronous cancel took effect.
if ((local variable) const(int) resres != -(constant) int core.stdc.errno.ECANCELED = 125ECANCELED && (local variable) const(int) resres != -(constant) int core.stdc.errno.EINTR = 4EINTR)
{
stderr.std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @systemwritefln("expected -ECANCELED, got res=%d", (local variable) const(int) resres);
return 1;
}
// `cret` is the kernel's reported match count (often 0 for a poll the kernel
// tears down inline); the authoritative proof is the poll's -ECANCELED CQE.
void std.stdio.writefln!(char, string, const(int), const(int))(in char[] fmt, string __param_1, const(int) __param_2, const(int) __param_3) @safeEquivalent to writef(fmt, args, '\n').
writefln("ok: REGISTER_SYNC_CANCEL torn down the in-flight poll; CQE returned %s (res=%d, matches=%d)",
(local variable) const(int) resres == -(constant) int core.stdc.errno.ECANCELED = 125ECANCELED ? "-ECANCELED" : "-EINTR", (local variable) const(int) resres, (local variable) const(int) cretcret);
return 0;
}