#!/usr/bin/env dub
/+ dub.sdl:
name "io_uring_poll_add"
dependency "during" version="~>0.5.0"
platforms "linux"
targetPath "build"
+/
/**
* `io_uring` — readiness notification with `IORING_OP_POLL_ADD` (Linux 5.1).
*
* `POLL_ADD` is io_uring's answer to `poll(2)`/`epoll`: arm a one-shot
* readiness watch on a file descriptor and get a CQE when it becomes ready,
* without ever calling `poll` from userspace. It is the building block for
* event-loop style I/O — the kernel does the waiting and notifies you through
* the same completion ring every other op uses.
*
* This example creates a libc `pipe()`, arms a `POLL_ADD` for `POLLIN` on the
* read end, then writes one byte into the write end. Once the read end is
* readable the CQE fires, and (per during's `tests/poll.d`) `res` carries the
* set of ready poll events — a positive value with the `POLLIN` bit set.
*
* Companion to the io_uring chronology:
* see docs/research/async-io/io-uring/timeline.md § "5.1 — The introduction (May 2019)".
*
* Run with: `dub run --single poll-add.d`
*
* Portability: `POLL_ADD` has existed since the 5.1 introduction, so it works on
* any kernel that has io_uring at all. If io_uring is unavailable (too old, or
* blocked by a seccomp/container policy) the program prints a `SKIP:` line and
* exits 0 so it stays green in CI regardless of the host kernel.
*/
module (module) io_uring_poll_addio_uring — readiness notification with IORING_OP_POLL_ADD (Linux 5.1).
POLL_ADD is io_uring's answer to poll(2)/epoll: arm a one-shot
readiness watch on a file descriptor and get a CQE when it becomes ready,
without ever calling poll from userspace. It is the building block for
event-loop style I/O — the kernel does the waiting and notifies you through
the same completion ring every other op uses.
This example creates a libc pipe(), arms a POLL_ADD for POLLIN on the
read end, then writes one byte into the write end. Once the read end is
readable the CQE fires, and (per during's tests/poll.d) res carries the
set of ready poll events — a positive value with the POLLIN bit set.
Companion to the io_uring chronology:
see docs/research/async-io/io-uring/timeline.md § "5.1 — The introduction (May 2019)".
Run with: dub run --single poll-add.d
Portability
POLL_ADD has existed since the 5.1 introduction, so it works on
any kernel that has io_uring at all. If io_uring is unavailable (too old, or
blocked by a seccomp/container policy) the program prints a SKIP: line and
exits 0 so it stays green in CI regardless of the host kernel.
io_uring_poll_add;
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.posixposix.(module) core.sys.posix.pollD header file for POSIX.
poll : (alias enum value) io_uring_poll_add.POLLIN = core.sys.posix.poll.POLLIN = 1POLLIN;
import (package) corecore.(package) core.syssys.(package) core.sys.posixposix.(module) core.sys.posix.unistdD header file for POSIX.
unistd : (alias) io_uring_poll_add.close = int core.sys.posix.unistd.close(int) nothrow @nogc @trustedclose, (alias) io_uring_poll_add.pipe = int core.sys.posix.unistd.pipe(ref int[2]) nothrow @nogc @trustedpipe, (alias) io_uring_poll_add.write = long core.sys.posix.unistd.write(int, scope const(void*), ulong) nothrow @nogcwrite;
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 : stderr, (alias template) io_uring_poll_add.writefln = std.stdio.writefln(alias fmt, A...)(A args) if (isSomeString!(typeof(fmt)))Equivalent to writef(fmt, args, '\n').
writefln, (alias template) io_uring_poll_add.writeln = std.stdio.writeln(T...)(T args)Equivalent to write(args, '\n'). Calling writeln without
arguments is valid and just prints a newline to the standard
output.
Example
Reads stdin and writes it to stdout with an argument
counter.
import std.stdio;
void main()
{
string line;
for (size_t count = 0; (line = readln) !is null; count++)
{
writeln("Input ", count, ": ", line);
}
}
writeln;
int int D main()main()
{
// user_data cookie identifying this poll op when its CQE comes back.
enum ulong (constant) ulong io_uring_poll_add.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 classic anonymous pipe: fds[0] is the read end, fds[1] the write end.
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 @systemwriteln("SKIP: pipe() failed");
return 0;
}
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 one-shot poll on the read end for readability (POLLIN). The kernel
// will park this until the fd is readable, then post a CQE.
(local variable) during.Uring ioio.putWith!((ref SubmissionEntry e, int readFd) {
e.prepPollAdd(readFd, PollEvents.IN);
e.user_data = cookie;
})(during.Uring during.Uring.putWith!(function (ref during.io_uring.SubmissionEntry e, int readFd) nothrow @nogc @safe
{
prepPollAdd(e, readFd, 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]);
// submit(0) flushes the SQ without blocking on a completion count — the poll
// is now armed in the kernel but the fd is not yet readable.
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);
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;
}
// Make the read end readable: one byte into the write end is enough to
// satisfy POLLIN and trip the armed poll.
immutable ubyte (local variable) immutable(ubyte) oneone = 0x2A;
const (local variable) const(long) wrotewrote = long core.sys.posix.unistd.write(int, scope const(void*), ulong) nothrow @nogcwrite((local variable) int[2] fdsfds[1], &(local variable) immutable(ubyte) oneone, 1);
if ((local variable) const(long) wrotewrote != 1)
{
stderr.std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @systemwritefln("write to pipe failed (ret %d)", (local variable) const(long) wrotewrote);
return 1;
}
// Block for the poll completion.
(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;
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(int) resres < 0)
{
stderr.std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @systemwritefln("POLL_ADD completed with error: errno %d", -(local variable) const(int) resres);
return 1;
}
if ((local variable) const(ulong) echoedechoed != (constant) ulong io_uring_poll_add.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_poll_add.main.cookie = 1LUcookie, (local variable) const(ulong) echoedechoed);
return 1;
}
// On success `res` is the bitmask of ready events; POLLIN must be set since
// the pipe's read end now has data.
if (!((local variable) const(int) resres & (enum value) core.sys.posix.poll.POLLIN = 1POLLIN))
{
stderr.std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @systemwritefln("expected POLLIN in ready mask, got 0x%X", (local variable) const(int) resres);
return 1;
}
void std.stdio.writefln!(char, const(int))(in char[] fmt, const(int) __param_1) @safeEquivalent to writef(fmt, args, '\n').
writefln("ok: POLL_ADD reported readiness (res=0x%X, POLLIN set) on the pipe read end", (local variable) const(int) resres);
return 0;
}