Skip to main content
Processes, IPC (including semaphores), scheduling, memory, I/O, file systems, virtualization, concurrency models, performance profiling, and the hardware-software interface.

Operating Systems

Processes, IPC (including semaphores), scheduling, memory, I/O, file systems, virtualization, concurrency models, performance profiling, and the hardware-software interface.

Inter-Process Communication (IPC)

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 connects ls and less. 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: mmap a file (or the Windows MapViewOfFile) 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:

  1. The disk controller reads the file into a kernel page cache page.
  2. read() copies that page into the application buffer (user space).
  3. write() copies the application buffer back into kernel socket buffers.
  4. 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; tee duplicates a pipe’s data without consuming it. These are the building blocks a network stack uses to join disk → pipe → socket without user copies.
  • mmap shared 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 same send/recv model.

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

NeedMechanismWhy
One-shot pipeline, related processes (a | b)PipeCheapest, kernel-built
Two unrelated processes, same host, simple streamNamed pipe / FIFOLow overhead, file-like API
Structured records, decoupled senders/receiversMessage queuePreserves boundaries, kernel buffering
Maximum throughput, shared hot dataShared memoryNo per-access kernel copy
File → socket / file → file with no app copysendfile / spliceKernel-to-kernel transfer, zero user-space round-trip
Bidirectional, local or remote, general-purposeSocketMost general, same API both ways
Control/notification, no payloadSignalAsynchronous, cheap
Passing a file descriptor between processessendmsg/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?

  1. The shell creates a pipe (pipe()) and forks grep with its write end, and sort with its read end.
  2. grep writes matches into the pipe; the kernel buffers them; sort reads the byte stream from the other end.
  3. Each write/read is a syscall (see the System Calls topic) — the kernel copies data between the two processes’ buffers.
  4. When grep exits, its write end closes; sort sees EOF and stops reading; head closes early, which sends SIGPIPE to sort, 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

  1. 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.
  2. Write two small programs that exchange a message through a named pipe (POSIX FIFO or Windows named pipe); run them from separate terminals.
  3. Rewrite the same exchange using shared memory (shm_open/mmap or MapViewOfFile) plus a semaphore; measure the throughput difference against the pipe version.
  4. Reproduce a classic race: two processes both increment a value in shared memory without synchronization, then add a mutex and compare results.
  5. Compare a local Unix-domain socket vs a loopback TCP socket for a request/response benchmark and note the latency gap.
  6. Write a tiny static-file server and benchmark read+write against sendfile on the same payload; observe the throughput and CPU difference.

When It’s the Right Tool

SituationTakeaway
Shell pipelines and tool compositionPipes — cheap and implicit
Unrelated daemons on one hostNamed pipes or local sockets
Event/job decoupling with boundariesMessage queues
Latency- or bandwidth-critical hot dataShared memory + explicit sync
Client-server, local or remoteSockets
Process control (stop/reload)Signals
Performance debuggingThe copy is the cost — measure before optimizing