← Back to Software Architecture

Inter-Process Communication on Linux

Linux inter-process communication through pipes, queues, sockets, and shared memory.

Linux processes are isolated on purpose. Each process gets its own virtual address space, file descriptor table, and scheduler state. Inter-process communication, or IPC, is the set of kernel mechanisms that let those isolated processes exchange data or coordinate safely. The right mechanism depends on whether you need a byte stream, discrete messages, a wake-up signal, or shared state with very low overhead.

A pipe is the simplest option. One process writes bytes, another reads them in order. Anonymous pipes are usually created between related processes, such as a shell connecting grep to sort. Named pipes, or FIFOs, extend the same idea to unrelated processes through the filesystem namespace. Pipes are easy to use and work well for streaming output, but they have no message boundaries. If one side writes 100 bytes twice, the reader sees a stream, not two logical records. Pipes also apply backpressure through a bounded kernel buffer, so a slow reader can stall a writer.

Message queues add message boundaries back into the system. Instead of a stream, the kernel stores distinct records that readers fetch one at a time. This is useful when requests must not be split or merged accidentally. The cost is higher overhead than a raw stream and stricter size limits. Message queues are good for control-plane traffic and small commands, but they are a poor fit for large bulk payloads.

Signals are even smaller. A signal is an asynchronous notification such as SIGTERM, SIGCHLD, or SIGINT. It tells a process that something happened, but it carries very little data. Signals are useful for lifecycle events, interruption, and simple wake-ups. They are dangerous as a general messaging system because signal handlers run under tight constraints. Many library calls are unsafe inside a handler, signals can coalesce, and poorly designed code can create races between normal execution and interrupt paths.

Semaphores are not mainly for moving data. They are for coordinating access to shared state. A semaphore lets one process announce "a resource is available" or "wait until this count changes" without spinning in a loop. In practice, semaphores are often paired with shared memory. Shared memory maps the same physical pages into multiple processes so they can read and write common data structures directly. This is the fastest IPC option for high-volume exchange because bytes do not need to be copied through the kernel for every operation. The catch is that you lose the safety of message boundaries. Once several processes touch the same memory, you must handle locking, visibility, corruption after crashes, and versioning of the in-memory layout.

That tradeoff explains a useful rule of thumb. If correctness is more important than raw speed, start with a kernel-managed primitive such as a pipe, queue, or Unix domain socket. If throughput is the real bottleneck, move data through shared memory and use semaphores or eventfds only for coordination. Many high-performance systems do exactly that: a ring buffer in shared memory for payloads, plus a small signalling mechanism to tell the peer where the new data ends.

Linux also offers Unix domain sockets, which many production services prefer even though they are often taught separately from the classic five IPC mechanisms. They preserve message boundaries when used as datagram sockets, support streaming when used as stream sockets, and allow descriptor passing between processes. That makes them a practical default for local RPC between daemons.

The operational failure modes are mostly about blocking and cleanup. A writer can block forever on a full pipe. A process can die while holding a semaphore. Shared memory can leak if nothing removes the segment. A signal sent too early can arrive before the target is ready to handle it. Good IPC design therefore needs timeouts, ownership rules, and a recovery story after one side crashes.

IPC on Linux is not one feature. It is a toolkit for choosing where you want the kernel to help and where you are willing to manage complexity yourself.