Spec layer 0 — Module io_uring, Stage 3d: multishot operations
Technical specification — Version 1.0 (target kernel: Linux 6.12 LTS)
Position. Stage 3d specifies multishot operations: a single submission produces multiple completions. Sub-module
air-sys-syscall::io_uring::multishot. Relies on the completion flagCQE_F_MORE(axis E), on the provided buffers from Stage 3b (for recv/read), and on the direct descriptors from Stage 3a (for accept). Reuses the core from Stage 1. No registered opcode, one dedicated opcode (READ_MULTISHOT, 49) and op flags (ACCEPT_MULTISHOT,RECV_MULTISHOT,POLL_ADD_MULTI,TIMEOUT_MULTISHOT).
1. Principle and lifecycle
A one-shot operation submits one SQE, receives one CQE, and releases its slot. A multishot operation submits one SQE and receives a stream of CQEs:
- each intermediate completion carries
CQE_F_MORE: “more completions will follow for this same SQE”; - the final (terminal) completion does not have
CQE_F_MORE: the multishot is done (error, buffer starvation, or cancellation).
Interaction with slab S1. The slot stays alive as long as completions
carry CQE_F_MORE; it is released only at the completion without F_MORE.
This is the second case (after the zero-copy NOTIF from Stage 2b) where a slot
outlives its first completion — the generation of the SubmissionToken
guards against late CQEs after cancellation (Stage 1 §4.2).
Token. A multishot operation returns a MultishotToken (distinct from the
one-shot SubmissionToken). All its completions carry it:
#![allow(unused)]
fn main() {
impl Completion {
/// Multishot token if this completion comes from a multishot.
pub fn multishot_token(&self) -> Option<MultishotToken>;
// has_more() (Stage 1) = CQE_F_MORE : true as long as the stream continues.
}
}
Re-arming. When a multishot ends (has_more() == false), the application
resubmits if it wants to continue. The facade makes termination explicit;
no hidden re-arming.
2. Accept multishot
#![allow(unused)]
fn main() {
impl IoUring {
pub fn submit_accept_multishot(&mut self, listener: BorrowedFd<'_>, flags: AcceptFlags)
-> Result<MultishotToken, Errno>;
/// Direct-descriptor variant: each connection lands in an auto-allocated slot
/// in the FixedFdTable (Stage 3a).
pub fn submit_accept_multishot_direct(&mut self, listener: BorrowedFd<'_>, flags: AcceptFlags)
-> Result<MultishotToken, Errno>;
}
}
- Op:
ACCEPT(13) +IORING_ACCEPT_MULTISHOT. A single SQE accepts continuously: each incoming connection produces a completion carrying the accepted FD (accepted_fd()),CQE_F_MOREmaintained. - Direct variant: each connection goes into a direct slot (auto-alloc,
FixedSlotTarget::Alloc) — ideal for a very high connection-rate server (compositor, AirCom): no ordinary FD, no userspace table to manage. Links to Stage 3a. - Termination: on error (e.g. listener closed) ⇒ completion without
F_MORE. Use case: a server makes one submission at startup and consumes connections as they arrive.
3. Recv / Read multishot (with provided buffers)
#![allow(unused)]
fn main() {
impl IoUring {
pub fn submit_receive_multishot(&mut self, sock: BorrowedFd<'_>, group: &ProvidedBufferRing, flags: MessageFlags)
-> Result<MultishotToken, Errno>;
pub fn submit_read_multishot(&mut self, fd: BorrowedFd<'_>, group: &ProvidedBufferRing, offset: Option<u64>)
-> Result<MultishotToken, Errno>;
}
}
- Ops:
RECV(27) +IORING_RECV_MULTISHOT;READ_MULTISHOT(49, dedicated opcode). Require provided buffers (Stage 3b,IOSQE_BUFFER_SELECT): each data arrival takes a buffer from the group and produces a completion. - Completion:
into_provided_buffer(group)returns the chosen buffer (id + bytes), a RAII guard that replenishes on drop (Stage 3b §4). Incremental consumption (CQE_F_BUF_MORE, Stage 3b §5) composes with multishot. - Buffer starvation: if the group is empty when data arrives, the completion
carries
-ENOBUFSand terminates the multishot (noF_MORE). The application replenishes the group then resubmits. The facade signals this case distinctly (termination due to starvation vs. network error). - Major benefit for AirCom / servers: a single SQE per connection serves its entire incoming stream, without a buffer pre-committed per idle connection.
4. Poll multishot
#![allow(unused)]
fn main() {
impl IoUring {
pub fn submit_poll_multishot(&mut self, fd: BorrowedFd<'_>, events: PollEvents)
-> Result<MultishotToken, Errno>;
}
}
- Op:
POLL_ADD(6) +IORING_POLL_ADD_MULTI. Each state transition of the FD toward the monitoredeventsproduces a completion (into_poll_result() -> PollEvents),F_MOREmaintained. - Level-triggered:
IORING_POLL_ADD_LEVELexposed via an option (PollEvents+ level). Otherwise, edge-triggered. - Cancellation:
submit_poll_remove/cancel_multishot.
5. Timeout multishot
#![allow(unused)]
fn main() {
impl IoUring {
pub fn submit_timeout_multishot(&mut self, interval: Duration, flags: TimeoutFlags)
-> Result<MultishotToken, Errno>;
}
}
- Op:
TIMEOUT(11) +IORING_TIMEOUT_MULTISHOT. Emits a completion at a regular interval (repeating timer) until cancelled — useful for a periodic heartbeat in the reactor without resubmitting on every tick. - Termination:
cancel_multishotor error.
6. Cancellation
#![allow(unused)]
fn main() {
impl IoUring {
pub fn cancel_multishot(&mut self, token: MultishotToken) -> Result<(), Errno>;
}
}
- Cancels an in-flight multishot (via
ASYNC_CANCEL, Stage 2c, targeting the token). The terminal completion (withoutF_MORE, often-ECANCELED) releases the slot; any late CQEs are filtered by generation (S1). submit_cancel(CancelTarget::…)(Stage 2c) remains usable for grouped cancellations (by FD, by op,Any).
7. Summary
| Op / flag | Facade | Completions |
|---|---|---|
ACCEPT + ACCEPT_MULTISHOT | submit_accept_multishot[_direct] | stream of accepted FDs |
RECV + RECV_MULTISHOT | submit_receive_multishot | stream of provided buffers |
READ_MULTISHOT (49) | submit_read_multishot | stream of provided buffers |
POLL_ADD + POLL_ADD_MULTI | submit_poll_multishot | stream of events |
TIMEOUT + TIMEOUT_MULTISHOT | submit_timeout_multishot | stream of ticks |
ASYNC_CANCEL | cancel_multishot | termination |
All share: MultishotToken, the CQE_F_MORE invariant, slot release at the
terminal completion.
8. Types added / shared
New: MultishotToken (already declared in Stage 1, semantics fixed here).
Reuses: AcceptFlags, MessageFlags, PollEvents, TimeoutFlags,
ProvidedBufferRing/ProvidedBuffer (Stage 3b), FixedSlotTarget (Stage 3a).
Completion method: multishot_token.
9. Test strategy
- Integration:
accept_multishoton a listener, N connections ⇒ N completions withF_MORE, listener closed ⇒ terminal completion; direct variant (FD in slots);recv_multishot+ buffer group, stream of datagrams, starvation ⇒-ENOBUFSterminating the multishot, replenish + resubmit;poll_multishoton a pipe written multiple times;timeout_multishot(N ticks then cancel);cancel_multishot(termination, late CQE filtered). - S1 lifecycle: slot remains occupied while
F_MORE, released at terminal completion; generation filters post-cancellation CQEs (property-based). - Safety: Miri on multishot slot restitution; no double-free; provided buffers correctly returned in multishot.
- Coverage: 100% lines + branches.
10. Key decisions that emerged at Stage 3d
- Distinct
MultishotToken— a stream of completions, not a one-shot op; the type prevents confusing the two lifecycles. - Slot alive until the terminal completion (
!F_MORE) — an assumed extension of S1 (like zero-copy NOTIF); generation guards against late CQEs. - Explicit termination, explicit re-arming — no hidden resubmission
(Principle 7);
-ENOBUFSsignalled distinctly from network error. accept_multishot_directpreferred for high connection-rate servers — connections as direct descriptors (links Stage 3a), relevant for AirCom/compositor.- recv/read multishot backed by provided buffers — no buffer pre-committed per connection (links Stage 3b).
11. Next steps
Next spec: io-uring-3e-shared-en.md (multi-thread usage: LockedIoUring,
RingPool thread-per-core, SqpollIoUring).
Document license: MPL 2.0
Status: Technical specification for Stage 3d (multishot) of the air-sys-syscall::io_uring module, target kernel 6.12 LTS.