Inter-Process Communication: Pipes & Message Queues

Building an Operating System Kernel: Concurrency, I/O & Synchronization

Chapter 5 · Inter-Process Communication: Pipes & Message Queues

Chapter 1's own shared page let two processes touch the same raw memory. A pipe is the safer, structured alternative — processes exchange data through a real, ordered stream, no shared address space required. This chapter builds a real pipe directly on top of Chapter 3's own bounded buffer, finds a real gap in what a pipe can express, and builds a real message queue to close it.

Finding 1: A Real Pipe, Built Directly on Chapter 3's Own BoundedBuffer

class Pipe: # an unstructured, ordered stream of raw bytes -- no concept # of 'messages' at all, only bytes in the order they were written def __init__(self, bounded_buffer): self.buf = bounded_buffer def try_write_byte(self, byte_value): return self.buf.try_produce(byte_value) def try_read_byte(self): return self.buf.try_consume()
Verified directly — every byte delivered, in order, under real preemption
b'HELLO KERNEL', written one byte at a time by one process and read one byte at a time by another, arrives back exactly as sent. A pipe is a message queue's simpler cousin — reusing Chapter 3's own producer-consumer correctness directly, with zero new synchronization code required.

Finding 2: A Real Pipe Has No Message Boundaries

Verified directly — two distinct messages silently merge into one stream
Writer A sends b'HI' (intended as one message). Writer B sends b'BYE' (also intended as one message). Every real byte from both arrives, correctly ordered, with zero loss — but the raw stream the reader actually sees is b'HBIYE': the two writers' bytes genuinely interleaved at the byte level. Reading it back in any fixed chunk size not chosen to match the original writes produces genuinely wrong groupings[b'HB', b'IY', b'E']. A pipe preserves byte order. It has no concept of where one message ends and the next begins.

A Deeper Finding: Length-Prefixing Doesn't Save the Multi-Writer Case

The classic real-world fix for a single writer is length-prefixing — write how many bytes are coming, then the bytes themselves.

Verified directly — the length bytes themselves can interleave too
With one writer sending both length-prefixed messages back to back, decoding works perfectly. But with two independent writers, each length-prefixing their own message onto the same pipe, the scheduler can interleave their length bytes with each other just as freely as any other byte — decoding the raw stream b'\x02\x03HBIYE' produces garbage, not the original two messages. Length-prefixing only works when one writer's entire framed burst is guaranteed to land as one uninterrupted unit — true for exactly one writer, false the moment a second one can interleave with it.

Finding 3: A Real Message Queue Preserves Discrete Message Boundaries

class MessageQueue: # the SAME empty/full semaphore pattern as BoundedBuffer, but counting # MESSAGES, not bytes -- each send/receive is one atomic unit def try_send(self, message): if not self.empty.try_acquire(): return False if not self.queue_mutex.try_acquire(): self.empty.release() return False self.messages.append(message) # the WHOLE message, one real step self.queue_mutex.release() self.full.release() return True
Verified directly — every message arrives whole, never merged, never split
Writer A sends b'HI', writer B sends b'BYE' — the exact same setup as Finding 2. Result: [b'HI', b'BYE'], exactly the two original whole messages, regardless of how the scheduler interleaved the two real send() calls. Because each message is queued and dequeued as one real, atomic Python object, there is no byte-level interleaving possible at all — not something to avoid carefully, something the data structure itself makes structurally impossible.

Where This Connects

This chapter's findingWhat it connects to
The pipe itselfChapter 3's own BoundedBuffer — reused completely unchanged; a pipe is that same real fix, applied to real IPC instead of a shared counter
The message queue's own empty/full semaphoresChapter 3's own two-semaphore pattern — reused directly, counting whole messages instead of bytes
Length-prefixing failing under multiple writersChapter 1's own shared-page race — the same underlying lesson: a technique that assumes uncontested access silently breaks the moment a second real competitor shows up
Structural correctness vs. careful disciplineCourse 1 Chapter 6's own syscall boundary — some protections work by making the bad outcome impossible by construction, rather than by everyone remembering to be careful

Hands-On Exercises

Exercise 1

Length-prefix two messages sent by a single writer, back to back, and confirm decoding works correctly. Then repeat with two independent writers each length-prefixing their own message onto the same pipe, and confirm decoding fails. Explain precisely why.

📄 View solution
Exercise 2

Run a real MessageQueue with a capacity of exactly 1, sending several messages of genuinely different lengths. Confirm every message still arrives whole and in order, and explain why capacity doesn't affect message-boundary correctness.

📄 View solution
Exercise 3

Send an empty message (b'') followed by a real one through a MessageQueue. Confirm both are received as two genuinely distinct messages, and explain why a raw pipe fundamentally cannot express this same scenario cleanly.

📄 View solution

Chapter 5 Quick Reference

  • Pipe: an ordered byte stream, built directly on Chapter 3's own BoundedBuffer — correct byte order, zero concept of message boundaries
  • Verified Finding 2: two writers on the same pipe genuinely interleave at the byte level, silently merging distinct messages
  • Verified (deeper finding): length-prefixing fixes a single writer but not multiple — the length bytes themselves are just as interleavable as any other byte
  • Verified Finding 3: a real MessageQueue — the same empty/full pattern, counting whole messages — makes byte-level interleaving structurally impossible
  • Golden rule: a pipe answers "in what order did the bytes arrive"; a message queue answers "what were the actual messages" — genuinely different questions, not the same primitive with a different name
  • Next chapter: Device Drivers & the I/O Abstraction Layer