Processes Are Islands
From the process-management topic: every process gets its own private address space. That isolation is the OS’s safety guarantee — but it comes at a price: processes cannot simply reach into each other’s memory. Any time two processes need to cooperate (a shell piping output into a pager, a web server talking to a cache daemon, a browser talking to its renderer), they must use an explicit inter-process communication (IPC) mechanism provided by the OS.
IPC is the connective tissue of the system. The same primitives you see here power the client-server model, microservices, pipelines, and almost every daemon architecture. Choosing the wrong one costs throughput, latency, or correctness — so the goal of this topic is a mental model of what each mechanism is good at.
Two Fundamental Strategies
Every IPC mechanism reduces to one of two strategies:
- Channels — data flows from one process to another through a kernel-mediated stream or queue (pipes, message queues, sockets). The kernel copies data between the sender and the receiver, and both sides can block on the transfer.
- Shared memory — the kernel maps a region of memory into two (or more) process address spaces; processes then read and write the same physical pages directly, with no kernel copy on each access. Synchronization becomes the caller’s job.
The core trade-off: channels are safe and simple but pay a copy cost; shared memory is fast but requires explicit synchronization (and brings back the race conditions from the Threads & Concurrency topic, now across processes).
Pipes
A pipe is a unidirectional byte-stream channel. One process writes to one end, another reads from the other, FIFO order. On Unix, cmd1 | cmd2 creates one; on Windows, the C runtime’s _pipe does the same job. Pipes are anonymous — they have no name on the file system and exist only as long as a process holds an end open.
Key properties:
- Unidirectional — one writer, one reader. Two-way communication needs two pipes (or a socketpair).
- Byte stream, not records — the reader sees a stream of bytes; there is no message boundary unless the protocol adds one.
- Blocking semantics — a read blocks until data arrives or the writer closes; a write blocks when the pipe buffer is full. On Unix the pipe buffer is typically 64 KB; on Windows the size is set at creation.
- Inherited through
fork— the child inherits the open ends, which is how a shell connectslsandless. This makes pipes the natural glue for pipelines.
Pipes are cheap, kernel-built, and perfect for serial producer → consumer data flow. The grep -r foo | head pattern you type every day is a chain of pipes.
Named Pipes (FIFOs)
An anonymous pipe has no identity outside the fork tree. A named pipe (FIFO on Unix, Windows named pipe) gets a name in the file system (/tmp/myfifo, \\.\pipe\myname) so unrelated processes can connect to it. On Unix it’s a file-like object you open and read/write; on Windows, named pipes support bidirectional byte-stream or message-mode communication and can span the network.
Use a named pipe when two unrelated processes on the same machine need a simple channel and you don’t want to build a socket. They are a classic low-overhead way to connect a producer and consumer without a full network stack.
Message Queues
Where a pipe is a stream of bytes, a message queue is a list of records. A sender enqueues a message of a given type and size; a receiver dequeues it (optionally by type). The kernel preserves message boundaries, so the sender does not have to invent a framing protocol.
- POSIX message queues (
mq_open,mq_send) have a kernel lifetime and bounded depth/size. - System V message queues (
msgget,msgsnd) are the older Unix form. - On Windows, mailslots provide one-way broadcast-style message queues; named pipes in message mode are the closer analog.
Message queues shine when the data is naturally structured (events, jobs, notifications) and when you want bounded, persistent-in-kernel storage between senders and receivers that may live and die independently. The queues you’ll see in the Distributed Systems category (Kafka, RabbitMQ) are the networked, fault-tolerant descendants of this same idea.
Shared Memory & Memory Mapping
Shared memory is the fastest IPC because, after the initial setup, data moves with no kernel involvement. The OS maps the same physical pages into multiple address spaces; process A writes, process B reads — with zero copies between them.
- POSIX shared memory:
shm_open+mmap. - Memory-mapped files:
mmapa file (or the WindowsMapViewOfFile) so reads/writes go straight to the page cache — this is also how executables and shared libraries load. - Windows: file mappings (
CreateFileMapping+MapViewOfFile).
The catch: since both processes share memory, you must re-introduce synchronization — a mutex, a semaphore, or an atomic flag — to prevent one process from reading a half-written value. This is precisely the race-condition territory from Threads & Concurrency, now across process boundaries. Shared memory is the right call when the throughput matters (media pipelines, game loops, high-frequency data) and you can afford the synchronization discipline.
Zero-Copy
“Zero-copy” is the practice of moving data between a device (disk, NIC) and a process — or between two processes — without staging it through intermediate kernel buffers. It is not a single API; it is a family of techniques that share one goal: remove redundant copies from the hot path.
To see why copies hurt, trace what a naive file → socket transfer does:
- The disk controller reads the file into a kernel page cache page.
read()copies that page into the application buffer (user space).write()copies the application buffer back into kernel socket buffers.- The NIC driver copies the socket buffer into the network card’s DMA ring.
That is at least two user↔kernel copies (steps 2–3) for data that never needed to touch the application at all — a web server serving a static file is just relaying bytes.
Zero-copy techniques eliminate those user-space round-trips:
sendfile()/copy_file_range()— the kernel copies data directly from a file descriptor to another descriptor (or file to file) inside the kernel. The application never sees the bytes. A static-file web server becomes:sendfile(sockfd, fd, &offset, count).splice()/tee()— splice moves data between two file descriptors (either of which may be a pipe) with no user-space buffer;teeduplicates a pipe’s data without consuming it. These are the building blocks a network stack uses to join disk → pipe → socket without user copies.mmapshared regions — discussed above: once mapped, both processes touch the same physical pages, so there is no per-access copy at all.- Direct I/O (
O_DIRECT) — bypasses the page cache entirely so the device transfers straight to an application-aligned buffer. Trade-off: you lose kernel caching and must align buffers — usually only worth it when you know exactly what you need. - DMA (hardware): NICs with RDMA (remote direct memory access) or bus-mastering can transfer to/from user memory without CPU involvement at all — the deep end of zero-copy used by high-performance storage and HPC.
The point of zero-copy is not “never copy anything.” It is “don’t copy bytes the receiver won’t modify and the sender doesn’t need back.” Copying is cheap per byte and expensive per transfer because of syscall overhead, cache pollution, and CPU stalls; eliminating round-trips through user space is where the real win lives.
When to reach for it: high-throughput servers (proxies, static-file web servers, load balancers), log forwarding, media streaming, and database WAL shipping. When not to: when the data must be transformed, parsed, or reformatted in the application — you’re going to touch the bytes anyway, so a zero-copy path just makes your code harder to write.
Sockets (Network & Local)
A socket is a bidirectional channel identified by an endpoint (address + port). Sockets come in two flavors:
- Network sockets — TCP/UDP over IP (see the Networking topic). Work across machines.
- Local (Unix domain) sockets — same socket API but addressed by a file-system path on Unix (
AF_UNIX) or a named pipe abstraction on Windows. They live entirely on one machine and skip the IP/TCP overhead while keeping the samesend/recvmodel.
Sockets are the most general IPC: they carry arbitrary bidirectional byte streams, work locally and across the network, and are the foundation of almost every server. The price is the heaviest setup (bind, listen, accept) and a protocol you must define.
Signals
Signals are the odd one out: they don’t carry data — they carry a notification. A signal interrupts the receiving process, runs a handler, and can terminate, pause, or prompt it to reload. SIGTERM (graceful shutdown), SIGKILL (force), SIGINT (Ctrl+C), SIGHUP (reload) are the Unix classics; Windows has an analogous but more restricted set (CTRL_C_EVENT, WM_QUERYENDSESSION). Signals are ideal for control (“stop”, “restart”, “config changed”), never for data transfer — their delivery is asynchronous, and only a handful of signals carry a tiny payload.
Choosing an IPC Mechanism
| Need | Mechanism | Why |
|---|---|---|
One-shot pipeline, related processes (a | b) | Pipe | Cheapest, kernel-built |
| Two unrelated processes, same host, simple stream | Named pipe / FIFO | Low overhead, file-like API |
| Structured records, decoupled senders/receivers | Message queue | Preserves boundaries, kernel buffering |
| Maximum throughput, shared hot data | Shared memory | No per-access kernel copy |
| File → socket / file → file with no app copy | sendfile / splice | Kernel-to-kernel transfer, zero user-space round-trip |
| Bidirectional, local or remote, general-purpose | Socket | Most general, same API both ways |
| Control/notification, no payload | Signal | Asynchronous, cheap |
| Passing a file descriptor between processes | sendmsg/SCM_RIGHTS (Unix) | Moves the open file, not just data |
Worked Example: A Pipeline
A shell runs grep -r "error" logs/ | sort | head -20. What IPC is happening?
- The shell creates a pipe (
pipe()) andforksgrepwith its write end, andsortwith its read end. grepwrites matches into the pipe; the kernel buffers them;sortreads the byte stream from the other end.- Each
write/readis a syscall (see the System Calls topic) — the kernel copies data between the two processes’ buffers. - When
grepexits, its write end closes;sortsees EOF and stops reading;headcloses early, which sendsSIGPIPEtosort, terminating the chain cleanly.
Three IPC concepts in one command: a pipe for the data, inherited descriptors through fork, and a signal for the early-exit notification.
Practice Trajectory
- On the command line, build a two-stage pipeline and confirm it with
strace -f -e trace=pipe,read,write bash -c 'ls | wc -l'on Unix (Process Monitor on Windows) — identify the pipe and the inherited descriptors. - Write two small programs that exchange a message through a named pipe (POSIX FIFO or Windows named pipe); run them from separate terminals.
- Rewrite the same exchange using shared memory (
shm_open/mmaporMapViewOfFile) plus a semaphore; measure the throughput difference against the pipe version. - Reproduce a classic race: two processes both increment a value in shared memory without synchronization, then add a mutex and compare results.
- Compare a local Unix-domain socket vs a loopback TCP socket for a request/response benchmark and note the latency gap.
- Write a tiny static-file server and benchmark
read+writeagainstsendfileon the same payload; observe the throughput and CPU difference.
When It’s the Right Tool
| Situation | Takeaway |
|---|---|
| Shell pipelines and tool composition | Pipes — cheap and implicit |
| Unrelated daemons on one host | Named pipes or local sockets |
| Event/job decoupling with boundaries | Message queues |
| Latency- or bandwidth-critical hot data | Shared memory + explicit sync |
| Client-server, local or remote | Sockets |
| Process control (stop/reload) | Signals |
| Performance debugging | The copy is the cost — measure before optimizing |