This page documents Wine-NSPA’s io_uring surface for file and socket I/O.
io_uringThe io_uring surface is broader than the original file-I/O landing.
Current coverage includes:
CreateFile on the per-process server ringRECVMSG / SENDMSG, default on via
NSPA_URING_RECV=1 and NSPA_URING_SEND=1ntdll_io_uring_flush_deferred() and the inline
eventfd getterThe important boundary correction from 2026-05-02 is that io_uring does
not subsume named-pipe or named-event completion. Those remain
server-managed surfaces and compose with the local-event fix instead of
replacing it.
| Surface | Status | Default | Notes |
|---|---|---|---|
| Sync poll + async file I/O | Active | On | NtReadFile / NtWriteFile async bypass + sync poll replacement; pool allocator (TLS, 32 ops); CQE drain at server_select / server_wait |
Dispatcher-owned async CreateFile |
Active | On | routes CreateFile through the per-process ring and removes the open() lock-drop CS from the audio xrun path |
| Socket recv | Active | On | NSPA_URING_RECV=1; recv_socket submits IORING_OP_RECVMSG on the deferred path |
| Socket send | Active | On | NSPA_URING_SEND=1; send_socket submits IORING_OP_SENDMSG on the deferred path |
NtFlushBuffersFile FSYNC |
Dropped | – | disk path is already synchronous fsync(); no meaningful io_uring win |
| anonymous pipes / inotify | Dropped | – | both blocked by existing server-managed infrastructure shape |
RECVMSG / SENDMSG.io_uring work. Their remaining
completion story is handled by other server-managed mechanisms.This document is the io_uring design and implementation reference for
Wine-NSPA. It covers the integration boundary, per-thread ring model,
the file / socket paths, and the surfaces that remain outside the
io_uring boundary. For project-wide context, see the architecture overview.
This design addresses two bottlenecks:
io_uring_enter).global_lock hold time. Fewer server-monitored fds = shorter hold =
less contention for shmem dispatchers.| NSPA Component | io_uring Interaction |
|---|---|
| Shmem IPC (gamma channel + msg-ring v2) | Orthogonal. Shmem handles request/reply IPC. io_uring handles file/socket I/O. Different fd sets, no conflict. |
| PI global_lock | Indirect benefit. Fewer fds in server epoll = shorter main loop iterations = shorter global_lock hold. |
| ntsync (/dev/ntsync) | Integrated. ntsync uring_fd extension wakes threads blocked in ntsync waits when io_uring CQEs arrive. The pad field in ntsync_wait_args carries the io_uring eventfd; kernel returns NTSYNC_INDEX_URING_READY on CQE. Required for sync socket-style waits to drain CQEs inline. |
| CS-PI (FUTEX_LOCK_PI) | No conflict. io_uring operations happen client-side in ntdll, never acquiring server locks. |
| RT scheduling (SCHED_FIFO/RR) | Compatible. COOP_TASKRUN ensures completions run in the submitting thread’s context, preserving RT priority. |
archive/iouringRémi Bernon’s 2021-2022 archive/iouring branch attempted direct
replacement of wineserver’s epoll loop. That design touched core server
files (server/fd.c, request.c, thread.c) and depended on an
earlier io_uring feature set.
Wine-NSPA uses a different integration boundary. The shmem and gamma
paths already remove request/reply IPC from the hot path, so io_uring is
applied client-side to file and socket I/O. The bulk of the
implementation remains isolated in dlls/ntdll/unix/io_uring.c, with
thin call-site conditionals in existing files.
IORING_SETUP_SINGLE_ISSUER | IORING_SETUP_COOP_TASKRUN.
No cross-thread submission, no locking.COOP_TASKRUN ensures CQE processing happens
in the submitting thread’s context – preserving SCHED_FIFO priority.
No kernel worker threads at default priority.-ENOSYS
if the ring is unavailable. Callers fall back to the existing code
path. Wine apps see identical behavior whether io_uring is present
or not.dup() the unix fd before
submitting to the ring. The duplicate is owned by the in-flight SQE
and closed on CQE completion. Prevents use-after-close if the
server-side handle table changes during the operation.server_select() and server_wait() entry points – the natural
places where a thread is about to block. Completions are delivered
promptly without adding threads or signals.io_uring.c, ~794 lines). Existing files get thin conditionals.uring_async_op
structs. Freelist-based O(1) alloc/free – no malloc/free in the
submit path. Initialized once at ring setup.Client Thread Wineserver
------------- ----------
NtReadFile(async)
server_get_unix_fd() ----> get_handle_fd
register_async() ----> register_async
queue_async(&fd->read_q)
set_fd_events(POLLIN)
return STATUS_PENDING
... main_loop_epoll():
(thread does other work) global_lock.lock()
epoll_pwait2() -> fd ready
fd_poll_event -> async_wake_up
global_lock.unlock()
(thread enters alertable wait)
async_read_proc():
server_get_unix_fd() ----> get_handle_fd (again)
read(fd, buf, len)
set IOSB, signal event
Syscalls per async read: 2 server round-trips (register + get_fd) + epoll_wait + read = 4+ kernel transitions
Client Thread Wineserver
------------- ----------
NtReadFile(async)
server_get_unix_fd() ----> get_handle_fd (cached, usually no trip)
dup(unix_fd) -> ring_fd
io_uring_prep_read(ring_fd, buf, len)
io_uring_submit() (server never sees this I/O)
return STATUS_PENDING
...
(thread enters server_wait)
ntdll_io_uring_process_completions():
CQE ready -> bytes_read
file_complete_async()
close(ring_fd)
Syscalls per async read: 1 io_uring_enter (submit+wait batched) =
1 kernel transition
The server is bypassed for the I/O monitoring and data transfer. It still handles the initial fd lookup (usually cached) and completion port notifications if needed.
Before: After:
poll(fd, POLLIN, timeout) ntdll_io_uring_poll(fd, POLLIN, timeout)
read(fd, buf, len) read(fd, buf, len) <- unchanged
The read()/write() still goes through virtual_locked_read() for
write-watch safety. Only the poll wait is replaced.
Shipped, default-on. Single combined implementation covering sync poll
replacement and async NtReadFile / NtWriteFile bypass.
In NtReadFile and NtWriteFile:
poll(fd, events, timeout)
with ntdll_io_uring_poll().The async path tries io_uring before falling back to the server
register_async round-trip:
if (!ntdll_io_uring_submit_file_read(unix_fd, ...)) {
return STATUS_PENDING; /* CQE will complete later */
}
/* fallback: register_async with server */
dup() the unix fd before submission. The duplicate is owned by the
uring_async_op struct and closed on CQE completion or cancellation.
CQEs are drained cooperatively in server_select() and server_wait()
entry points:
unsigned int server_select(...) {
ntdll_io_uring_process_completions();
...
}
unsigned int server_wait(...) {
ntdll_io_uring_process_completions();
...
}
When a CQE arrives, complete_uring_op() translates the result to
NTSTATUS and calls file_complete_async() – the same function used
by Wine’s normal async completion path. This handles:
NtSetEvent)NtQueueApcThread)add_completion())If io_uring’s kernel read hits EFAULT (buffer in a write-watched page),
the CQE result is -EFAULT. The completion handler frees the operation;
the caller retries through the server async path, which uses
virtual_locked_read() with proper page fault handling. Graceful
fallback for an edge case that rarely occurs in practice.
| File | Change | LOC |
|---|---|---|
dlls/ntdll/unix/file.c |
NtReadFile sync wait + async bypass |
~25 |
dlls/ntdll/unix/file.c |
NtWriteFile sync wait + async bypass |
~25 |
dlls/ntdll/unix/server.c |
Completion drain in server_select/server_wait |
+2 |
dlls/ntdll/unix/thread.c |
Ring cleanup in pthread_exit_wrapper() |
+1 |
Shipped, default-on.
The socket path uses true socket SQEs on the deferred async path:
NSPA_URING_RECV=1 routes recv completion through IORING_OP_RECVMSGNSPA_URING_SEND=1 routes send completion through IORING_OP_SENDMSGThe integration boundary is still the same as the earlier ALERTED-state work: the server remains authoritative for the async lifecycle and socket state machine, while the client owns readiness monitoring and the data move itself.
The 2026-05-02 default-on flip was backed by:
socket-io deferred path: +6.5% throughputsocket-io deferred path: -6.8% p99 latencysocket-io: 0/2000 failures63 threads and zero
new errors versus the earlier socket baselineThe client is not bypassing the socket state machine. It is bypassing the monitoring and data-transfer part of the async path once the server has already established the async object and its completion contract.
That is why the current shape is viable:
set_async_direct_resultio_uringNot every async surface is an io_uring candidate.
| Surface | Current reason it stays outside |
|---|---|
| named pipes | server-owned pseudo-fd architecture; no real PE-side kernel fd to submit against |
| named events | server-side naming / object-directory semantics, not an fd-monitoring problem |
| anonymous-pipe follow-on | current Win32 CreatePipe route still sits on the named-pipe pseudo-fd infrastructure |
| directory notify via inotify | current inotify state is one server-process-wide facility, not a per-PE resource |
The 2026-05-02 correction is that these are no longer “next io_uring
candidates” in the same sense that sockets used to be. Named-pipe and named-event
completion compose with the local-event server-registration path, not
with io_uring.
| File | Lines | Purpose |
|---|---|---|
dlls/ntdll/unix/io_uring.c |
~794 | Per-thread ring management, all phase functions, pool allocator |
| File | Changed Lines | Purpose |
|---|---|---|
dlls/ntdll/unix/unix_private.h |
+30 | io_uring function declarations, bitmap helpers |
dlls/ntdll/unix/file.c |
~30 | sync poll + async read/write bypass |
dlls/ntdll/unix/socket.c |
~120 | socket path: ALERTED interception, RECVMSG / SENDMSG CQE handlers, bitmap set/clear |
dlls/ntdll/unix/sync.c |
~40 | ntsync uring_fd retry loop, deferred completion flush |
dlls/ntdll/unix/server.c |
+2 | Completion drain at server_select/server_wait |
dlls/ntdll/unix/thread.c |
+1 | Ring cleanup at thread exit |
server/sock.c |
~40 | E2 bitmap check in sock_get_poll_events |
dlls/ntdll/Makefile.in |
+2 | io_uring.c source + URING_LIBS |
configure.ac |
+8 | liburing detection |
liburing.so.2 (system package). Available as liburing in Arch Linux
[extra]. Detected at configure time via
AC_CHECK_LIB(uring, io_uring_queue_init).
| Component | Status | Notes |
|---|---|---|
| io_uring ring management | Shipped | Per-thread, lazy init |
| Pool allocator (TLS, 32 ops) | Shipped | RT-safe, zero malloc in submit path |
| sync poll + async file I/O | Shipped, default-on | NtReadFile / NtWriteFile |
async CreateFile via dispatcher ring |
Shipped | server-side consumer on the per-process ring |
| socket I/O (deferred path) | Shipped, default-on | NSPA_URING_RECV=1, NSPA_URING_SEND=1; validated on socket-io and Ableton |
| wait-path helper trimming | Shipped | removes tiny helper-call overhead on the steady-state wait path |
| dropped follow-ons | Dropped | not worthwhile or blocked by server-managed architecture |
E2 bitmap (server sock.c) |
Shipped | engaged when the client-owned socket poll path is active |
ntsync uring_fd extension |
Shipped (kernel patch) | Wakes ntsync waits on CQE |
| ntsync PI kernel + audit fixes | Shipped (kernel patch) | current Wine-NSPA ntsync overlay |
| Audit §4.1 retry-loop hardening | Shipped (wine) | NSPA_SHM_RETRY_GUARD is on the normal path |
socket-io harness.io_uring roadmap.