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:
-
eventfd: a kernel counter exposed as an FD, readable when non-zero. Lightweight inter-thread or inter-process notifications. -
pipe: classic Unix pipe, unidirectional communication. -
Zero-copy operations:
splice,tee,vmsplice. Moving data between FDs without going through userspace.
Position relative to the other families.
- Unix sockets (the
netfamily) are Air’s primary IPC, notably for AirCom. - Shared memory via
memfd(thememfamily) is used for the data plane. - This
ipcfamily covers the simpler cases: notifications, pipes, zero-copy optimizations.
Cross-cutting characteristics.
- Universal CLOEXEC.
- Modern variants.
eventfd2andpipe2preferred. - No System V IPC.
shmget,semget,msggetnot wrapped. Listed inUNSUPPORTED.md. - No POSIX message queues.
mq_openetc. 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.
- Serving a file over a socket: transfer from a file FD to a socket FD without copying.
- Pipeline between processes.
- 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:
| Category | Main functions |
|---|---|
| eventfd | eventfd2, EventFd::read, EventFd::write |
| pipe | pipe2 |
| zero-copy | splice, 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,EventFdFlagsPipeFlagsSpliceFlags
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).