Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Layer 0 Spec — ipc Family

Technical specification — Version 1.0

Family Overview

The air-sys-syscall::ipc module exposes the modern kernel IPC primitives: eventfd, pipe, and the zero-copy operations between FDs (splice, tee, vmsplice). It is a small family that rounds out the inter-thread and inter-process communication toolkit.

Scope of the family.

Three distinct mechanisms:

  1. eventfd: a kernel counter exposed as an FD, readable when non-zero. Lightweight inter-thread or inter-process notifications.

  2. pipe: classic Unix pipe, unidirectional communication.

  3. Zero-copy operations: splice, tee, vmsplice. Moving data between FDs without going through userspace.

Position relative to the other families.

  • Unix sockets (the net family) are Air’s primary IPC, notably for AirCom.
  • Shared memory via memfd (the mem family) is used for the data plane.
  • This ipc family covers the simpler cases: notifications, pipes, zero-copy optimizations.

Cross-cutting characteristics.

  1. Universal CLOEXEC.
  2. Modern variants. eventfd2 and pipe2 preferred.
  3. No System V IPC. shmget, semget, msgget not wrapped. Listed in UNSUPPORTED.md.
  4. No POSIX message queues. mq_open etc. not wrapped.

Subsection 1: eventfd

eventfd2

#![allow(unused)]
fn main() {
pub fn eventfd2(
    initial: u64,
    flags: EventFdFlags,
) -> Result<EventFd, Errno>;

pub struct EventFd { /* owns an internal OwnedFd */ }

impl EventFd {
    pub fn as_fd(&self) -> BorrowedFd<'_>;
    pub fn into_fd(self) -> OwnedFd;
    
    pub fn read(&self) -> Result<u64, Errno>;
    pub fn write(&self, value: u64) -> Result<(), Errno>;
}

bitflags! {
    pub struct EventFdFlags: i32 {
        const CLOEXEC = 0x80000;
        const NONBLOCK = 0x800;
        const SEMAPHORE = 1;
    }
}
}

Underlying syscall. eventfd2 (x86_64 no. 290, ARM64 no. 19). Available since Linux 2.6.27.

Behavior.

Creates an FD that wraps a 64-bit kernel counter. Two modes depending on the SEMAPHORE flag:

  • Normal mode: read() returns the current value and resets it to zero. Blocks if the counter is zero.
  • Semaphore mode: read() returns 1 and decrements. Blocks if zero.

Typical use case: inter-thread notification.

#![allow(unused)]
fn main() {
let efd = eventfd2(0, EventFdFlags::empty())?;

// Thread A: wait for a notification
loop {
    let count = efd.read()?;
    process_events(count);
}

// Thread B: send a notification
efd.write(1)?;
}

Integration with io_uring.

#![allow(unused)]
fn main() {
let efd = eventfd2(0, EventFdFlags::empty())?;
let mut buf = [0u8; 8];
let token = ring.submit_read(efd.as_fd(), buf.to_vec(), 0)?;
ring.submit()?;

// From another thread:
efd.write(1)?;

// The ring receives the completion
let completion = ring.wait_completion()?;
}

This is the canonical pattern for letting an io_uring reactor thread interact with other threads.

Performance. Creation ~5-10 µs. Read/write ~500 ns to 1 µs.

Subsection 2: pipe

pipe2

#![allow(unused)]
fn main() {
pub fn pipe2(flags: PipeFlags) -> Result<(OwnedFd, OwnedFd), Errno>;

bitflags! {
    pub struct PipeFlags: i32 {
        const CLOEXEC = 0x80000;
        const DIRECT = 0x4000;
        const NONBLOCK = 0x800;
    }
}
}

Behavior.

Creates a unidirectional pipe. Returns (read_end, write_end).

Pipes have a kernel buffer size (typically 64 KB, adjustable via fcntl).

PipeFlags::DIRECT: “packet” mode where each write produces a distinct packet on the read side.

Typical use case.

  • Parent-child communication after fork.
  • Simple notification where eventfd would be overkill.
  • Pipeline implementation.

Subsection 3: Zero-copy operations

splice

#![allow(unused)]
fn main() {
pub fn splice(
    fd_in: BorrowedFd<'_>,
    off_in: Option<&mut u64>,
    fd_out: BorrowedFd<'_>,
    off_out: Option<&mut u64>,
    length: usize,
    flags: SpliceFlags,
) -> Result<usize, Errno>;

bitflags! {
    pub struct SpliceFlags: u32 {
        const MOVE = 1;
        const NONBLOCK = 2;
        const MORE = 4;
        const GIFT = 8;
    }
}
}

Preconditions.

At least one of the two FDs must be a pipe.

Behavior.

Transfers up to len bytes from fd_in to fd_out without going through user space.

Typical use cases.

  1. Serving a file over a socket: transfer from a file FD to a socket FD without copying.
  2. Pipeline between processes.
  3. High-performance logging.

Performance. Main gain: elimination of the kernel → user → kernel copies. Throughput 2-3 times higher than read/write for large transfers.

tee

#![allow(unused)]
fn main() {
pub fn tee(
    fd_in: BorrowedFd<'_>,
    fd_out: BorrowedFd<'_>,
    length: usize,
    flags: SpliceFlags,
) -> Result<usize, Errno>;
}

Both FDs must be pipes. Duplicates the contents of one pipe into another without consuming the source pipe.

vmsplice

#![allow(unused)]
fn main() {
pub fn vmsplice(
    fd: BorrowedFd<'_>,
    iov: &[IoSlice<'_>],
    flags: SpliceFlags,
) -> Result<usize, Errno>;
}

fd must be a pipe. Transfers data from userspace buffers into a pipe.

Gifting (the optimized mode) is dangerous: the donated pages must no longer be modified. Air documents the constraints heavily.

ipc Family Summary

Exposed functions:

CategoryMain functions
eventfdeventfd2, EventFd::read, EventFd::write
pipepipe2
zero-copysplice, tee, vmsplice

Total: ~6 main public functions.

Non-wrapped syscalls (listed in UNSUPPORTED.md):

  • eventfd (without the 2): replaced by eventfd2.
  • pipe (without the 2): replaced by pipe2.
  • shmget, shmat, shmdt, shmctl: System V shared memory, deprecated.
  • semget, semop, semctl: System V semaphores, deprecated.
  • msgget, msgsnd, msgrcv, msgctl: System V message queues, deprecated.
  • mq_open, mq_send, mq_receive, mq_close, mq_unlink, mq_notify, mq_setattr: POSIX message queues, marginal.

Types added to air-sys-types

  • EventFd, EventFdFlags
  • PipeFlags
  • SpliceFlags

That is ~4 additional types.

Underlying decisions that emerged in the ipc family

1. No System V IPC.

Clear decision: we do not expose the legacy mechanisms that have known semantic and security problems.

2. No POSIX message queues.

Marginal usage, supplanted by sockets and memfd.

3. EventFd::read returns u64, not Vec<u8>.

Air exposes the counter directly as u64. The conversion is internal to the wrapper.

4. vmsplice gifting documented but lightly tested.

A powerful but dangerous mode. An advanced case in phase 0.


Document license: MPL 2.0 Status: Technical specification of the air-sys-syscall::ipc module (layer 0).