Skip to content

Async observations

io_uring and Linux AIO do their work after the submitting call has returned. Antinomie records these requests as async observations attached to the submitting call, so you can see what was asked, what the kernel accepted and what completed.

Stage Meaning
Submission The request descriptor was observed. This is not proof it was accepted.
Accepted The kernel accepted it for processing.
Rejected Submission failed; the result explains why.
Completion The operation finished. Multishot and partial completions are not final.
Canceled The request ended with -ECANCELED.
Observation unavailable The observer retired the request without seeing a completion. The error describes the observation, not the operation.
Reaped For an AIO read, the data was read when the program collected the result
  • Request descriptors and typed operands for many opcodes, such as a socket address for BIND, descriptors for FILES_UPDATE, a futex word or a siginfo for WAITID.
  • Read and write payloads, when they can be read safely. A request run by an io_uring worker thread or the SQPOLL thread in the same process is read. Buffers registered with the ring are read within their lifetime.
  • For O_DIRECT AIO reads that complete in an interrupt, the data is read when the program reaps the result (stage Reaped), including multi-segment vectored reads.

A submission by the SQPOLL thread, or by a thread outside the ring creator’s group, is attributed to the ring creator and flagged as having an unknown producer, because the shared ring does not say which thread wrote the entry.

A call row previews its first 256 observations. The Async section of the decoded call says how many were saved and how many are not shown, for example Async (1-256 of 4,000 saved). Browse all saved observations pages through the rest from the archive without reading the whole call, with Previous page, Next page and Back to preview. Triage reports the calls it only previewed.

This keeps heavy io_uring runs usable: thousands of completions on one syscall no longer lose their entry, exit or task-end facts.

Triage reads each io_uring or AIO request that works on a file as the system call it stands for, from the submission entry, the paths and data the guest recorded and the request’s result. The same Files tabs, file lifecycles and detection events then cover programs that do their file work this way:

Requests Read as
io_uring OPENAT, OPENAT2 openat, openat2
io_uring READ*, WRITE* reads and writes, including the vectored and fixed-buffer forms
io_uring FTRUNCATE, FALLOCATE, CLOSE, STATX the matching calls
io_uring RENAMEAT, LINKAT, SYMLINKAT, UNLINKAT, MKDIRAT the matching calls
io_uring FSYNC, SYNC_FILE_RANGE, SPLICE, FSETXATTR, SETXATTR, FGETXATTR, GETXATTR the matching calls (since 0.7.1)
AIO pread, pwrite, preadv, pwritev the matching calls

The Operation column says where a row came from, such as openat (io_uring), and the file.via filter field is syscall, io_uring or AIO. A vectored read shows all the data it read, segment after segment.

A request on a registered descriptor carries no descriptor number. On a base built for Antinomie 0.7.0 or later the guest records the identity of the file each request used, and Triage ties the request to its file by that identity. An open straight into the ring’s registered table shows its descriptor as registered. With an older base, such requests are known by path only.

Triage reads up to 100,000 file requests per scan, including those past the call’s preview, and its coverage line says what was not read. Since 0.7.1 the file requests an SQPOLL ring’s kernel thread submitted are read too, attributed to the ring’s creator, which has no call of its own for them. Not mapped: requests that touch no file (sockets, timeouts, polls) and TEE. A request that never completed has no result and is shown as a call with no exit. A file written through an io_uring worker is not hashed.

io_uring worker threads belong to the program. On a base built for Antinomie 0.7.0 or later they count as part of the run, and whichever thread leaves last records how the program ended, so the run gets its own exit status, with or without Core Dump on Exit.

A run is Finished (or Stopped) when its process tree has ended with no request outstanding. An io_uring ring that a forked child or another process also maps, or an SQPOLL ring, does not prove which task wrote an entry, so such a run also reports incomplete observation. Since 0.7.1, a ring that only one process maps is attributed to that process and the run is not flagged (with a base built for 0.7.1 or later). That is a note beside the run state, not a different state; see Recording incomplete. With an older base an io_uring run can end with its Root exit shown as Unknown, never as exit 0.

  • At most 256 requests are tracked at once, and 1,024 records can wait to be published. Beyond that, new requests or records are refused, counted as async observation loss for the run, and the run is marked as having incomplete observation.
  • A pending AIO read is dropped, and counted as loss, if the reap table (64 entries per boot) is full or the process exits first.
  • Async records are published by one worker that waits for room in the CPU’s buffer, up to 10 ms per record, then publishes anyway.
  • Completion is not guaranteed to be observed. Accepted is not completion, and Observation unavailable is not failure.

Async loss appears in Summary under Runs with async observation loss (this boot). See Recording incomplete. The decoded call’s fields and stages are described in Syscall Trace.