Skip to content
Pipes & Inter-Process Communication
step 1/5

Reading — step 1 of 5

Read

~4 min readProcesses & Scheduling

Pipes & Inter-Process Communication

You've built isolated processes — separate address spaces, walled off by the paging and privilege machinery. Isolation is the point, but total isolation is useless: processes need to cooperate. A pipe is the simplest way they talk — a one-way byte stream between two processes — and it's the primitive the entire Unix philosophy (small tools, composed with |) is built on. If you took the shell course, you wired pipes between commands; now you build the pipe itself.

A pipe is a kernel buffer with two ends

pipe() asks the kernel for a bounded in-kernel buffer and hands back two file descriptors: a read end and a write end. Bytes written to one appear at the other, in order. The buffer lives in the kernel — neither process can see the other's memory (isolation intact!), so all data crosses through code the kernel controls, which is exactly where a cooperation channel should live.

Two properties make it more than a shared array, and they're your exercise:

  • It's bounded (classically 64 KiB). A finite buffer can fill up.
  • It blocks. That's the genius part.

Blocking is flow control for free

What happens when a writer outruns a reader? The buffer fills. A naive design would drop bytes or error; a pipe blocks the writer — puts it to sleep (the scheduler machinery!) until the reader drains some space. Symmetrically, a reader on an empty pipe blocks until a writer supplies bytes. This is back-pressure: cat huge.txt | grep x | head -1 doesn't read the whole huge file, because once head exits and stops reading, the buffer fills, grep blocks on write, and cat blocks behind it — the pipeline self-throttles to the speed of its slowest stage, using no code, just the blocking rule. A producer can never overwhelm a consumer; the buffer's finiteness is the coordination.

(Your simulator reports BLOCK instead of actually sleeping — same decision point, made visible. In a real kernel, "block" means "deschedule this process and wake it when the condition clears," implemented with the wait-queue-plus-SIGCHLD-style machinery from the scheduling and signals lessons.)

Close, EOF, and EPIPE — the end-of-stream contract

Streams need to signal "no more data," and pipes encode it in the two directions:

  • Reader's view — EOF. When all write ends are closed and the buffer is drained, reads return end-of-file (0 bytes). This is how wc -l knows the input ended. Note "all write ends": a pipe can have several writers, and EOF waits for the last (the reason the shell course insisted every process close every pipe fd it isn't using — a stray open write end means the reader hangs forever waiting for an EOF that never comes).
  • Writer's view — EPIPE / SIGPIPE. Write to a pipe whose read end is all closed and there's no one to receive it — the kernel delivers SIGPIPE (default action: terminate — the signals lesson) or returns EPIPE. This is why yes | head cleanly stops: when head exits and closes its read end, yes gets SIGPIPE on its next write and dies. A feature, not an error.

Where pipes sit in the IPC family

Pipes are the simplest IPC; the family is worth a map, because each trades simplicity for power:

  • Pipes — one-way, byte stream, related processes (share the fd via fork). Anonymous.
  • Named pipes (FIFOs) — same, but with a filesystem name, so unrelated processes can meet.
  • Shared memory — fastest: two processes map the same physical frames (the frame allocator + page tables!), zero-copy — but now you handle synchronization (the spinlocks lesson's races are yours to prevent).
  • Message queues, sockets — structured messages, and sockets extend the whole model across machines (the networking courses).

Pipes are where you learn the shape — bounded buffer, blocking, EOF — that all of them refine.

Your exercise: Bounded Pipe Buffer

PIPE cap, then WRITE (full or partial-with-BLOCK), READ (data, BLOCK on empty-open, EOF on empty-closed), and CLOSE (drain-then-EOF; writes after become EPIPE). The graded edges are exactly the contract's corners: partial write when the buffer fills mid-string, reading less than requested when less is available, the drain-after-close window (buffered bytes still readable!), and EOF only once truly empty. It's a circular buffer with three state rules — and it's the beating heart of every | you've ever typed.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…