Reading — step 1 of 5
Read
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 -lknows 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 | headcleanly stops: whenheadexits and closes its read end,yesgets 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…