multishot-accept.dhover×188all
#!/usr/bin/env dub
/+ dub.sdl:
    name "io_uring_multishot_accept"
    dependency "during" version="~>0.5.0"
    platforms "linux"
    targetPath "build"
+/
/**
 * `io_uring` — multishot accept (`IORING_ACCEPT_MULTISHOT`, Linux 5.19).
 *
 * Classic `IORING_OP_ACCEPT` (5.5) consumes one SQE per connection: you must
 * re-arm an accept after every client. Multishot accept inverts that — a
 * *single* accept SQE stays armed and posts one CQE per incoming connection,
 * each carrying a fresh accepted fd in `res` and `CQEFlags.MORE` set to signal
 * "this completion is not the last; the request lives on". This removes the
 * per-connection submit round-trip from the accept loop of a server.
 *
 * This program builds a libc TCP listener on `127.0.0.1:<ephemeral>`, arms one
 * `prepMultishotAccept` SQE, then opens and connects two loopback clients. It
 * waits for the two accept CQEs and verifies each yields a valid fd (`res >= 0`)
 * with `CQEFlags.MORE` set, proving the one SQE served multiple connections.
 *
 * Companion to the io_uring chronology: see
 * docs/research/async-io/io-uring/timeline.md
 * § "5.19 — Buffer rings, zero-copy groundwork, big SQE/CQE (July 2022)".
 *
 * Run with: `dub run --single multishot-accept.d`
 *
 * Portability: if `io_uring` is unavailable (too old / sandboxed) the program
 * prints `SKIP:` and exits 0. If multishot accept itself is unsupported
 * (kernel < 5.19 — the accept CQE comes back `-EINVAL`), it likewise prints
 * `SKIP:` and exits 0, so it stays green on the older kernels CI runs on.
 */
module 
(module) io_uring_multishot_accept

io_uring — multishot accept (IORING_ACCEPT_MULTISHOT, Linux 5.19).

Classic IORING_OP_ACCEPT (5.5) consumes one SQE per connection: you must re-arm an accept after every client. Multishot accept inverts that — a single accept SQE stays armed and posts one CQE per incoming connection, each carrying a fresh accepted fd in res and CQEFlags.MORE set to signal "this completion is not the last; the request lives on". This removes the per-connection submit round-trip from the accept loop of a server.

This program builds a libc TCP listener on 127.0.0.1:<ephemeral>, arms one prepMultishotAccept SQE, then opens and connects two loopback clients. It waits for the two accept CQEs and verifies each yields a valid fd (res >= 0) with CQEFlags.MORE set, proving the one SQE served multiple connections.

Companion to the io_uring chronology: see docs/research/async-io/io-uring/timeline.md § "5.19 — Buffer rings, zero-copy groundwork, big SQE/CQE (July 2022)".

Run with: dub run --single multishot-accept.d

Portability

if io_uring is unavailable (too old / sandboxed) the program prints SKIP: and exits 0. If multishot accept itself is unsupported (kernel < 5.19 — the accept CQE comes back -EINVAL), it likewise prints SKIP: and exits 0, so it stays green on the older kernels CI runs on.

io_uring_multishot_accept
;
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.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_multishot_accept.EINVAL = int core.stdc.errno.EINVAL = 22
EINVAL
,
(alias constant) io_uring_multishot_accept.EOPNOTSUPP = int core.stdc.errno.EOPNOTSUPP = 95
EOPNOTSUPP
,
(alias constant) io_uring_multishot_accept.ENOSYS = int core.stdc.errno.ENOSYS = 38
ENOSYS
;
import
(package) core
core
.
(package) core.sys
sys
.
(package) core.sys.posix
posix
.
(package) core.sys.posix.arpa
arpa
.
(module) core.sys.posix.arpa.inet

D header file for POSIX.

@copyrightCopyright Sean Kelly 2005 - 2009.@licenseBoost License 1.0.@authorsSean Kelly@standardsThe Open Group Base Specifications Issue 6, IEEE Std 1003.1, 2004 Edition
inet
:
(alias) io_uring_multishot_accept.htonl = uint core.sys.posix.arpa.inet.htonl(uint) pure nothrow @nogc @trusted
htonl
;
import
(package) core
core
.
(package) core.sys
sys
.
(package) core.sys.posix
posix
.
(package) core.sys.posix.netinet
netinet
.
(module) core.sys.posix.netinet.in_

D header file for POSIX.

@copyrightCopyright Sean Kelly 2005 - 2009.@licenseBoost License 1.0.@authorsSean Kelly@standardsThe Open Group Base Specifications Issue 6, IEEE Std 1003.1, 2004 Edition
in_
:
(struct) core.sys.posix.netinet.in_.sockaddr_in
sockaddr_in
,
(alias enum value) io_uring_multishot_accept.AF_INET = core.sys.posix.sys.socket.AF_INET = 2
AF_INET
;
import
(package) core
core
.
(package) core.sys
sys
.
(package) core.sys.posix
posix
.
(package) core.sys.posix.sys
sys
.
(module) core.sys.posix.sys.socket

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
socket
:
(alias) io_uring_multishot_accept.socket = int core.sys.posix.sys.socket.socket(int, int, int) nothrow @nogc @safe
socket
,
(alias) io_uring_multishot_accept.setsockopt = int core.sys.posix.sys.socket.setsockopt(int, int, int, scope const(void*), uint) nothrow @nogc
setsockopt
,
(alias) io_uring_multishot_accept.bind = int core.sys.posix.sys.socket.bind(int, scope const(core.sys.posix.sys.socket.sockaddr*), uint) nothrow @nogc
bind
,
(alias) io_uring_multishot_accept.listen = int core.sys.posix.sys.socket.listen(int, int) nothrow @nogc @safe
listen
,
(alias) io_uring_multishot_accept.connect = int core.sys.posix.sys.socket.connect(int, scope const(core.sys.posix.sys.socket.sockaddr*), uint) nothrow @nogc
connect
,
(alias) io_uring_multishot_accept.getsockname = int core.sys.posix.sys.socket.getsockname(int, scope core.sys.posix.sys.socket.sockaddr*, scope uint*) nothrow @nogc
getsockname
,
(struct) core.sys.posix.sys.socket.sockaddr
sockaddr
, socklen_t,
(alias enum value) io_uring_multishot_accept.SOCK_STREAM = core.sys.posix.sys.socket.SOCK_STREAM = 1
SOCK_STREAM
,
(alias enum value) io_uring_multishot_accept.SOL_SOCKET = core.sys.posix.sys.socket.SOL_SOCKET = 1
SOL_SOCKET
,
(alias enum value) io_uring_multishot_accept.SO_REUSEADDR = core.sys.posix.sys.socket.SO_REUSEADDR = 2
SO_REUSEADDR
;
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_multishot_accept.close = int core.sys.posix.unistd.close(int) nothrow @nogc @trusted
close
;
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_multishot_accept.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
()
{ enum
(constant) int io_uring_multishot_accept.main.ACCEPT_TAG = 1
ACCEPT_TAG
= 1; // user_data cookie for the multishot accept SQE
enum
(constant) int io_uring_multishot_accept.main.CLIENTS = 2
CLIENTS
= 2; // how many loopback connections we drive through
(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; } // --- libc TCP listener on 127.0.0.1:<ephemeral> ------------------------- const int
(local variable) const(int) srv
srv
=
int core.sys.posix.sys.socket.socket(int, int, int) nothrow @nogc @safe
socket
(
(enum value) core.sys.posix.sys.socket.AF_INET = 2
AF_INET
,
(enum value) core.sys.posix.sys.socket.SOCK_STREAM = 1
SOCK_STREAM
, 0);
if (
(local variable) const(int) srv
srv
< 0)
{ stderr.
std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @system
writefln
("socket(srv) failed");
return 1; } scope (exit)
int core.sys.posix.unistd.close(int) nothrow @nogc @trusted
close
(
(local variable) const(int) srv
srv
);
int
(local variable) int one
one
= 1;
int core.sys.posix.sys.socket.setsockopt(int, int, int, scope const(void*), uint) nothrow @nogc
setsockopt
(
(local variable) const(int) srv
srv
,
(enum value) core.sys.posix.sys.socket.SOL_SOCKET = 1
SOL_SOCKET
,
(enum value) core.sys.posix.sys.socket.SO_REUSEADDR = 2
SO_REUSEADDR
, &
(local variable) int one
one
,
(local variable) int one
one
.
(constant) ulong int.sizeof = 4LU
sizeof
);
(struct) core.sys.posix.netinet.in_.sockaddr_in
sockaddr_in
(local variable) core.sys.posix.netinet.in_.sockaddr_in saddr
saddr
;
(local variable) core.sys.posix.netinet.in_.sockaddr_in saddr
saddr
.
(field) ushort core.sys.posix.netinet.in_.sockaddr_in.sin_family
sin_family
=
(enum value) core.sys.posix.sys.socket.AF_INET = 2
AF_INET
;
(local variable) core.sys.posix.netinet.in_.sockaddr_in saddr
saddr
.
(field) ushort core.sys.posix.netinet.in_.sockaddr_in.sin_port
sin_port
= 0; // ask the kernel for an ephemeral port
(local variable) core.sys.posix.netinet.in_.sockaddr_in saddr
saddr
.
(field) core.sys.posix.arpa.inet.in_addr core.sys.posix.netinet.in_.sockaddr_in.sin_addr
sin_addr
.
(field) uint core.sys.posix.arpa.inet.in_addr.s_addr
s_addr
=
uint core.sys.posix.arpa.inet.htonl(uint) pure nothrow @nogc @trusted
htonl
(0x7f00_0001); // 127.0.0.1
if (
int core.sys.posix.sys.socket.bind(int, scope const(core.sys.posix.sys.socket.sockaddr*), uint) nothrow @nogc
bind
(
(local variable) const(int) srv
srv
, cast(
(struct) core.sys.posix.sys.socket.sockaddr
sockaddr
*)&
(local variable) core.sys.posix.netinet.in_.sockaddr_in saddr
saddr
,
(local variable) core.sys.posix.netinet.in_.sockaddr_in saddr
saddr
.
(constant) ulong core.sys.posix.netinet.in_.sockaddr_in.sizeof = 16LU
sizeof
) != 0)
{ stderr.
std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @system
writefln
("bind() failed");
return 1; } if (
int core.sys.posix.sys.socket.listen(int, int) nothrow @nogc @safe
listen
(
(local variable) const(int) srv
srv
,
(constant) int io_uring_multishot_accept.main.CLIENTS = 2
CLIENTS
) != 0)
{ stderr.
std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @system
writefln
("listen() failed");
return 1; } // Read back the kernel-assigned port so the clients can connect to it.
(struct) core.sys.posix.netinet.in_.sockaddr_in
sockaddr_in
(local variable) core.sys.posix.netinet.in_.sockaddr_in actual
actual
;
socklen_t
(local variable) uint alen
alen
=
(local variable) core.sys.posix.netinet.in_.sockaddr_in actual
actual
.
(constant) ulong core.sys.posix.netinet.in_.sockaddr_in.sizeof = 16LU
sizeof
;
if (
int core.sys.posix.sys.socket.getsockname(int, scope core.sys.posix.sys.socket.sockaddr*, scope uint*) nothrow @nogc
getsockname
(
(local variable) const(int) srv
srv
, cast(
(struct) core.sys.posix.sys.socket.sockaddr
sockaddr
*)&
(local variable) core.sys.posix.netinet.in_.sockaddr_in actual
actual
, &
(local variable) uint alen
alen
) != 0)
{ stderr.
std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @system
writefln
("getsockname() failed");
return 1; } // --- arm ONE multishot accept SQE --------------------------------------- // The kernel fills `peer`/`peerLen` for each accepted connection; one SQE // keeps posting CQEs (each with CQEFlags.MORE) until cancelled or it errors. static
(struct) core.sys.posix.netinet.in_.sockaddr_in
sockaddr_in
(thread local global) core.sys.posix.netinet.in_.sockaddr_in io_uring_multishot_accept.main.peer
peer
;
static socklen_t
(thread local global) uint io_uring_multishot_accept.main.peerLen
peerLen
=
(thread local global) core.sys.posix.netinet.in_.sockaddr_in io_uring_multishot_accept.main.peer
peer
.
(constant) ulong core.sys.posix.netinet.in_.sockaddr_in.sizeof = 16LU
sizeof
;
(local variable) during.Uring io
io
.putWith!(
(ref SubmissionEntry e, int fd) { e.prepMultishotAccept(fd, peer, peerLen); e.user_data = ACCEPT_TAG; })(
during.Uring during.Uring.putWith!(function (ref during.io_uring.SubmissionEntry e, int fd) nothrow @nogc @safe { prepMultishotAccept(e, fd, peer, peerLen, AcceptFlags.NONE); e.user_data = 1LU; } , const(int))(ref const(int) __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.
srv
);
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
(0); // just flush the SQ; we wait explicitly below
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; } // --- drive CLIENTS loopback connections, collecting one CQE each -------- int[
(constant) int io_uring_multishot_accept.main.CLIENTS = 2
CLIENTS
]
(local variable) int[2] clientFds
clientFds
= -1;
scope (exit) foreach (
(parameter) int cfd
cfd
;
(local variable) int[2] clientFds
clientFds
)
if (
(local variable) int cfd
cfd
>= 0)
int core.sys.posix.unistd.close(int) nothrow @nogc @trusted
close
(
(local variable) int cfd
cfd
);
int
(local variable) int accepted
accepted
; // count of successfully accepted server-side fds
bool
(local variable) bool sawMore
sawMore
; // did at least one accept CQE carry CQEFlags.MORE?
foreach (
(local variable) int i
i
; 0 ..
(constant) int io_uring_multishot_accept.main.CLIENTS = 2
CLIENTS
)
{ const int
(local variable) const(int) cli
cli
=
int core.sys.posix.sys.socket.socket(int, int, int) nothrow @nogc @safe
socket
(
(enum value) core.sys.posix.sys.socket.AF_INET = 2
AF_INET
,
(enum value) core.sys.posix.sys.socket.SOCK_STREAM = 1
SOCK_STREAM
, 0);
if (
(local variable) const(int) cli
cli
< 0)
{ stderr.
std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @system
writefln
("socket(cli %d) failed",
(local variable) int i
i
);
return 1; }
(local variable) int[2] clientFds
clientFds
[
(local variable) int i
i
] =
(local variable) const(int) cli
cli
;
// Loopback connect completes synchronously enough that the matching // accept CQE is ready by the time we wait for it. if (
int core.sys.posix.sys.socket.connect(int, scope const(core.sys.posix.sys.socket.sockaddr*), uint) nothrow @nogc
connect
(
(local variable) const(int) cli
cli
, cast(
(struct) core.sys.posix.sys.socket.sockaddr
sockaddr
*)&
(local variable) core.sys.posix.netinet.in_.sockaddr_in actual
actual
,
(local variable) uint alen
alen
) != 0)
{ stderr.
std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @system
writefln
("connect(cli %d) failed",
(local variable) int i
i
);
return 1; }
(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); // bounded: exactly one CQE per connection we just made
const
(local variable) const(during.io_uring.CompletionEntry) c
c
=
(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
;
const
(local variable) const(int) res
res
=
(local variable) const(during.io_uring.CompletionEntry) c
c
.
(field) int during.io_uring.CompletionEntry.res

result code for this event

res
;
const
(local variable) const(during.io_uring.CQEFlags) flags
flags
=
(local variable) const(during.io_uring.CompletionEntry) c
c
.
(field) during.io_uring.CQEFlags during.io_uring.CompletionEntry.flags
flags
;
(local variable) during.Uring io
io
.
void during.Uring.popFront() pure nothrow @nogc @safe

Move to next CompletionEntry

popFront
();
if (
(local variable) const(int) res
res
< 0)
{ // -EINVAL/-EOPNOTSUPP/-ENOSYS on the very first accept => the kernel // does not implement multishot accept (predates 5.19). Skip cleanly. if (
(local variable) int accepted
accepted
== 0 && (
(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
))
{
void std.stdio.writefln!(char, const(int))(in char[] fmt, const(int) __param_1) @safe

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

writefln
("SKIP: multishot accept unsupported (accept CQE res=%d) — needs Linux >= 5.19",
(local variable) const(int) res
res
);
return 0; } stderr.
std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @system
writefln
("accept CQE %d failed: errno %d",
(local variable) int i
i
, -
(local variable) const(int) res
res
);
return 1; } // `res` is a freshly accepted server-side fd — close it once observed.
int core.sys.posix.unistd.close(int) nothrow @nogc @trusted
close
(
(local variable) const(int) res
res
);
(local variable) int accepted
accepted
++;
if (
(local variable) const(during.io_uring.CQEFlags) flags
flags
&
(enum) during.io_uring.CQEFlags

Flags used with CompletionEntry

CQEFlags
.
(enum value) during.io_uring.CQEFlags.MORE = 2u

IORING_CQE_F_MORE (from Linux 5.13) If set, parent SQE will generate more CQE entries

MORE
)
(local variable) bool sawMore
sawMore
= true;
} if (
(local variable) int accepted
accepted
!=
(constant) int io_uring_multishot_accept.main.CLIENTS = 2
CLIENTS
)
{ stderr.
std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @system
writefln
("expected %d accepted connections, got %d",
(constant) int io_uring_multishot_accept.main.CLIENTS = 2
CLIENTS
,
(local variable) int accepted
accepted
);
return 1; } // CQEFlags.MORE on each completion is the defining signal of a multishot // request: the one accept SQE remains armed for further connections. if (!
(local variable) bool sawMore
sawMore
)
{ stderr.
std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @system
writefln
("no accept CQE carried CQEFlags.MORE — request was not multishot");
return 1; }
void std.stdio.writefln!(char, int)(in char[] fmt, int __param_1) @safe

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

writefln
("ok: one multishot accept SQE served %d loopback connections (CQEFlags.MORE set, request stays armed)",
(local variable) int accepted
accepted
);
return 0; }