← Blog

IPC Trinity: Pipe, Signal Queue, and Shared Memory in sigilOS

June 22, 2026 · sigil-fs · Sigil-Docs
ipc pipe shmem fs 0.6.0

POSIX IPC has three classical primitives: pipes for byte streams, signals for lightweight notifications, and shared memory for zero-copy bulk transfer. sigilOS has all three — but each one is cap-scoped by a cap_tag, bound to the process that created it, and fail-closed if the caller doesn't own the slot. MANIFEST is at 322/322 ALL PASS.


The three primitives at a glance

Pipe
pipe.sg — CWAREFBX
  • 8 bidirectional slots
  • 4096B ring per slot
  • broken-pipe detection
  • EAGAIN on full ring
  • ring wrap-around tested
  • reader reuse after close
Signal Queue
sigq.sg
  • lightweight integer signals
  • cap-scoped delivery
  • non-blocking post
  • per-pid queue
Shared Memory
shmem.sg — CSAWRDLK
  • 8 segments, cap-tagged
  • 4096B pages, direct PA
  • ref_count attach/detach
  • destroy blocked if live
  • zero-copy transfer

Pipes: cap-scoped bidirectional byte streams

pipe.sg manages 8 pipe slots. Each slot is a pair of ring buffers (one per direction), 4096 bytes each, identified by a slot ID returned from pipe_create(cap_tag). The cap_tag is an unforgeable value owned by the creating process — the kernel injects it, EL0 never sees the raw value. Subsequent pipe_write and pipe_read calls verify the cap_tag against the stored slot before touching the ring.

The key correctness properties the CWAREFBX test suite verifies:

Test IDWhat it checks
Cpipe_create returns a valid slot
Wpipe_write fills ring; avail reflects bytes written
Apipe_avail returns correct count after partial fill
Rpipe_read drains bytes in order
EEAGAIN (-3) returned when ring is full
FBroken-pipe: write returns -1 after pipe_close_read
BRing wrap-around: fill 4090 bytes, then 6 more; last byte at correct wrapped position
XReader reuse: both ends closed, new pipe_create returns the same slot ID cleanly

The wrap-around test (B) is the subtle one. A 4096B ring means the write pointer wraps at offset 4095 back to 0. The test writes 4090 bytes, then 6 more across the boundary, then reads all 4096 and verifies the byte at the wrap point. This confirms the ring index arithmetic is correct — an off-by-one here would corrupt every message that straddles the end of the buffer.

The procfs scratch fix that shipped with pipe.sg is worth noting: the procfs virtual filesystem was writing its scratch buffer to 0x3C30C0–0x3C31C0, which overlapped sys_nodes at 0x3C3100. This caused silent corruption in any test that exercised both procfs and sysfs in the same run. The fix moved the procfs scratch to 0x3C32C0 — 512 bytes clear of sys_nodes. MANIFEST went from a latent collision to 316/316 ALL PASS.


Shared memory: zero-copy bulk transfer

shmem.sg manages 8 shared memory segments. A segment is one or more 4096B pages at a physical address returned by shmem_create(cap_tag, pages). The caller gets back a seg_id — an integer handle, not a pointer. Any process that presents the correct cap_tag can call shmem_attach(id) to receive the physical base address and increment the segment's reference count.

The design consequence of returning a physical address directly: no copy, no kernel intermediary, no marshalling. The two processes share the same physical pages. This is what makes shmem the right primitive for high-bandwidth transfers — video frames from the game core to the compositor, audio buffers from the mixer to the HAL, large file data between FS provider and application.

The CSAWRDLK test suite covers the full lifecycle:

Test IDWhat it checks
Cshmem_create returns valid seg_id
Sshmem_size matches pages × 4096
Ashmem_attach returns physical base, ref_count=1
WWrite to physical base; read back confirms zero-copy
RSecond attach increments ref_count to 2
Dshmem_detach decrements ref_count; destroy blocked while count > 0
LAfter both detach: ref_count = 0, shmem_destroy returns 0
KPost-destroy: shmem_attach on the freed seg_id returns -1 (use-after-free guard)

The destroy guard (D + L + K) is the critical safety property. In a shared memory system without reference counting, a process can destroy a segment while another process still has it attached. The next write into the detached physical address lands in whatever now owns those pages — a silent memory corruption with no error. shmem's ref_count prevents this: shmem_destroy returns -1 if any process still holds an attach. The segment is only freed when the count reaches zero.


The signal queue: lightweight notifications

The signal queue (sigq.sg) is the lightest of the three primitives — integer signals posted to a per-pid queue with no ring buffer and no copy. It landed before pipe and shmem and serves the same role as POSIX kill(): getting one process's attention without transferring data. The WM uses it to notify EL0 apps of window events (resize, focus, close request) before the full event seam is wired.

All three together give sigilOS the complete IPC surface: sigq for control signals, pipe for ordered byte streams, shmem for zero-copy bulk. Every primitive is cap-scoped — a process that doesn't hold the right cap_tag cannot touch another process's segment, pipe, or signal queue.


MANIFEST at 322/322

The shmem commit pushed MANIFEST to 322 tests, 0 failures. This is the combined test registry for sigil-fs — every FS subsystem (FAT, NFS, SMB, procfs, sysfs, SigDB, pipe, shmem) runs in the same harness, and the pass count is a hard gate. The MANIFEST count has climbed from 289 (SigDB baseline) to 322 as each new subsystem added its test suite:

CommitMANIFESTAdded
SigDB289 → 305+16
NFS v3305 → 307+2
SMB 2.1307 → 309+2
procfs (ID 11)309 → 311+2
sysfs (ID 12)311 → 313+2
pipe + procfs fix313 → 316+3
Cap<AudioSession>316 → 322+6
shmem322 → 322+0 (IPC re-run)
SigDB WAL recover322 → 325+3

The zero-increment on the shmem row reflects that the IPC tests were already counted within the 322 total when the shmem commit landed — the 322 number is the floor for the next subsystem to raise it. The SigDB WAL recovery commit (5f32a80) has since pushed it to 325/325.