xtc_aio(3)
---xtc_aio(3)
async file I/O for fibers
| XTC_AIO(3) | Library Functions Manual | XTC_AIO(3) |
NAME
xtc_aio_pread,
xtc_aio_pwrite,
xtc_aio_preadv,
xtc_aio_pwritev,
xtc_aio_fsync,
xtc_aio_fdatasync — async
file I/O for fibers
SYNOPSIS
#include <xtc.h>
#include <xtc_aio.h>
int
xtc_aio_pread(int
fd, void *buf,
uint32_t len,
int64_t off);
int
xtc_aio_pwrite(int
fd, const void
*buf, uint32_t len,
int64_t off);
int
xtc_aio_preadv(int
fd, const struct iovec
*iov, int iovcnt,
int64_t off);
int
xtc_aio_pwritev(int
fd, const struct iovec
*iov, int iovcnt,
int64_t off);
int
xtc_aio_fsync(int
fd);
int
xtc_aio_fdatasync(int
fd);
DESCRIPTION
These perform a file read, write, or data-sync that suspends the calling fiber and resumes it on completion, keeping the fiber's loop live for other work while the I/O is outstanding.
They are a
single, portable
async-file-I/O API. Write storage code once -- a buffer manager
paging the B-tree, a WAL writer -- calling these as though true asynchronous
I/O is always available, and it runs unchanged on every platform
libxtc supports. Where the host has a native
completion engine the call uses it; where it does not, the call uses a
transparent fallback that presents the
same API and the
same observable behavior (the fiber parks, the loop keeps running
other work, the call) returns bytes-transferred or a negative errno . A
consumer never has to branch on the platform. Branching with
#ifdef between a native-AIO and a non-AIO path is
permitted if a consumer wants it, but is never required: one API that always
appears to be AIO covers the common case.
The mechanism, chosen at run time, is:
- On a loop whose I/O backend is io_uring, the operation is submitted
natively (
IORING_OP_READ,IORING_OP_WRITE, orIORING_OP_FSYNC); the fiber parks and the kernel's completion re-enqueues it. No worker thread is used. - On Windows (IOCP),
xtc_aio_pread() andxtc_aio_pwrite() are native overlappedReadFile()/WriteFile() completions dequeued from the same completion port as socket events;xtc_aio_fsync() andxtc_aio_fdatasync() have no async form and take the offload path below. - On the kqueue backend (FreeBSD, macOS) the operation is
submitted as POSIX AIO
(
aio_read(),aio_write(),aio_fsync()) with completion delivered to the same kqueue viaSIGEV_KEVENTand reaped as anEVFILT_AIOevent -- no worker thread. A platform lacking either falls back to the offload path below. - On a readiness-only backend (epoll, poll, select, Solaris event ports, AIX) the operation is offloaded to the blocking thread pool (xtc_blocking(3)), because a regular file is not pollable -- a readiness poller cannot complete file I/O asynchronously, and a thread offload is the correct, behavior-preserving fallback elsewhere. The fiber still parks and the loop stays live, so the API behaves identically to the native path.
- Off a loop entirely (no current fiber) the operation runs inline on the calling thread.
This is the layer that lets storage code use the fastest async file path a host offers without the caller choosing or knowing which one. In every case the call returns only once the operation has completed (or failed), so the caller may reuse buf immediately afterward.
The one behavioral caveat to size for: where the offload path is used, concurrency is bounded by the blocking-pool thread count (see xtc_blocking(3)), whereas the native engines admit many more in-flight ops. Code that issues a very high number of concurrent file ops should size the pool accordingly; correctness is identical either way.
xtc_aio_pread()
reads up to len bytes from fd at
absolute offset off into buf;
xtc_aio_pwrite()
writes len bytes from buf at
off;
xtc_aio_fsync()
flushes the file's data and metadata durably;
xtc_aio_fdatasync()
flushes the data only (fdatasync semantics; the hot path for page and WAL
flushes, and what) xtc_io(3)'s storage callers use. On
macOS, which has no
fdatasync(),
the data-only call falls back to
fsync().
None of them moves the file's seek pointer.
Vectored (scatter/gather) I/O
xtc_aio_preadv()
and
xtc_aio_pwritev()
are the async-fiber analogs of preadv(2) and
pwritev(2): they read into, or write from, the
iovcnt buffers described by iov
at file offset off, as one positioned op, with the
same fiber-parks / loop-stays-live semantics as the scalar calls. They exist
for migration ergonomics: code that already assembles struct
iovec arrays -- a WAL writer, PostgreSQL's
smgr/md
layer, any readv(2)/writev(2) consumer
-- can move to libxtc without flattening its buffers
or looping. Where the host has
io_uring
the op is submitted natively
(IORING_OP_READV/WRITEV);
elsewhere it offloads to the blocking pool
(preadv(2)/pwritev(2) on a worker
thread), identical observable behavior to the scalar API.
iovcnt must be in [1,
IOV_MAX]; out of range returns
-EINVAL, so a caller cannot force an unbounded
submission. These two are POSIX only -- Windows lacks
preadv(2)/pwritev(2) and
struct iovec, so they are not declared there; the
scalar
xtc_aio_pread()/xtc_aio_pwrite()
remain available on every platform.
RETURN VALUES
xtc_aio_pread() and
xtc_aio_pwrite() return the number of bytes
transferred (>= 0); a short read at end of file
returns the partial count, not an error.
xtc_aio_fsync() returns 0. On failure each returns a
negative errno value (e.g.
-EIO).
CONTEXT
Call from a fiber running on a loop for the async path; off a loop the operation runs synchronously on the calling thread. The buffer must remain valid until the call returns, which is guaranteed simply by not returning from the call -- the fiber parks on this one operation's completion and arms no other waker, so the operation is never left in flight referencing a stack buffer that has gone away.
EXAMPLES
Page a 4 KiB block out and read it back, off the fiber's loop:
int n = xtc_aio_pwrite(fd, page, 4096, (int64_t)pid * 4096); if (n != 4096) return -1; if (xtc_aio_fsync(fd) != 0) return -1; n = xtc_aio_pread(fd, page, 4096, (int64_t)pid * 4096);
SEE ALSO
AUTHORS
The xtc project.
| June 4, 2026 | Debian |