Two processes cannot share memory by accident. The kernel ensures that each process operates in its own isolated address space — a write by one process is invisible to another unless both processes explicitly arrange to share that memory. This isolation is the point: it is what keeps a crash in one process from corrupting another.
But programs need to cooperate. A shell pipeline connects the output of one program to the input of another. A database server receives queries from client processes. A browser communicates with a GPU process to render a page. None of these can rely on shared memory happening automatically — they need explicit mechanisms.
That is what IPC—Inter-Process Communication—is.
Pipes
A pipe is the simplest IPC mechanism: a unidirectional byte stream with a write end and a read end, buffered in the kernel. Data written to the write end comes out the read end in order. No structure, no message boundaries — just bytes.
When you run a shell pipeline:
ls | grep txt
The shell creates a pipe before forking. The pipe() system call returns two file descriptors: one for writing, one for reading. The shell forks twice — once for ls, once for grep. In the ls process, the write end of the pipe is connected to standard output (file descriptor 1). In the grep process, the read end is connected to standard input (file descriptor 0). Each process closes the end it does not use.
ls writes its output to file descriptor 1 — which is the pipe's write end. grep reads from file descriptor 0 — the pipe's read end. Neither process knows it is talking through a pipe. As far as ls is concerned, it is writing to standard output. As far as grep is concerned, it is reading from standard input. The connection is arranged entirely by the shell before either process runs.
You can observe this directly. Run the pipeline in the background and inspect its file descriptors:
sleep 100 | sleep 100 &
ls -la /proc/$(pgrep -n sleep)/fd
Look for file descriptors pointing to pipe:[N]. The number in brackets is the pipe's inode in the kernel's pipefs filesystem. Both processes in the pipeline will show the same pipe inode number — one with the write end, one with the read end.
The pipe has a kernel buffer — 64 kilobytes by default on Linux. If the writer fills the buffer before the reader drains it, the writer blocks until space is available. If the reader reads faster than the writer produces, the reader blocks. The kernel handles the synchronisation automatically.
A pipe exists only as long as a process holds one of its file descriptors. When the last write-end descriptor is closed, the reader gets EOF. When the last read-end descriptor is closed, the writer gets SIGPIPE.
Named Pipes (FIFOs)
An anonymous pipe exists only between related processes — processes that share a common ancestor who created the pipe and passed the file descriptors through fork(). Two unrelated processes cannot use an anonymous pipe.
A named pipe — or FIFO — solves this. A FIFO is a pipe with a name in the filesystem. It is created with mkfifo:
mkfifo /tmp/mypipe
This creates a filesystem entry. Any process that knows the path can open it. One process opens it for writing, another opens it for reading, and they communicate exactly as through an anonymous pipe.
The filesystem entry has permissions like any file — you can restrict who can open it. But it stores no data. The data lives in the kernel's pipe buffer, not on disk. A FIFO that no process has open holds nothing. Opening one for reading blocks until a writer opens the other end.
FIFOs are simple and portable but limited. They are unidirectional. They provide no message boundaries — a reader that calls read() might get part of what a writer wrote in a single write(), or multiple writes combined. For structured communication between unrelated processes, Unix domain sockets are more appropriate.
Unix Domain Sockets
A Unix domain socket is a socket that lives in the filesystem instead of the network. It uses the same socket(), bind(), connect(), send(), and recv() system calls as a TCP socket, but all communication stays within the kernel — no network stack, no IP routing, no checksums, no MTU constraints.
This distinction matters for performance. A TCP connection to 127.0.0.1 still passes through the full network stack: the data is framed into TCP segments, handed to the IP layer, routed (trivially, since it is loopback), then processed by the receiving socket's TCP implementation. This adds overhead — in latency, in CPU cycles, and in the number of kernel data copies. A Unix domain socket bypasses all of it. The kernel copies data directly from the sending process's buffer to the receiving process's buffer.
Unix domain sockets also support credential passing — the receiver can ask the kernel for the PID, UID, and GID of the process on the other end of the connection. This is not possible with TCP sockets without additional application-layer authentication. systemd, D-Bus, Docker, and the X11 display server all use Unix domain sockets as their primary communication channel precisely for this combination of performance and authentication.
See all active Unix domain sockets on the system:
ss -xl
The volume is usually striking. Every running application that communicates with systemd, the display server, the D-Bus message broker, or the audio daemon has one or more Unix domain socket connections. The paths in the Peer column show where each socket's endpoint lives in the filesystem — typically under /run, /tmp, or /var/run.
Shared Memory
Shared memory is the fastest IPC mechanism because it involves no kernel copying at all. Two processes map the same physical memory into their respective address spaces. A write by one process is immediately visible to the other — the data never leaves RAM.
The mmap() system call with the MAP_SHARED flag and a file descriptor maps a file-backed region into the process's address space. If two processes map the same file with MAP_SHARED, they share the underlying pages. A write to any address in the mapped region goes directly to the shared physical page — visible immediately to any other process that has mapped the same region.
For IPC purposes, the backing file is often not a real file but a POSIX shared memory object — created with shm_open(). POSIX shared memory objects appear as files under /dev/shm, which is a tmpfs filesystem mounted entirely in RAM:
ls -la /dev/shm/
On a typical desktop system you will see entries for applications that use shared memory for inter-process communication — browsers, media players, sandboxed renderer processes. Each entry is a shared memory segment with a name, a size, and permissions.
Creating and using a shared memory segment:
# Producer process creates it:
# shm_open("/myshm", O_CREAT | O_RDWR, 0600) then ftruncate() to set size
# Consumer process opens it:
# shm_open("/myshm", O_RDONLY, 0) then mmap()
# When done:
rm /dev/shm/myshm # removes the shared memory object
The critical constraint with shared memory is that the kernel provides no synchronisation. Two processes writing to shared memory simultaneously produce a race condition — any shared memory IPC requires an explicit synchronisation mechanism — a mutex in another shared memory region, a semaphore, or a separate coordination channel — to be safe.
This is the tradeoff: shared memory is fast precisely because the kernel does nothing except map the pages. The application is responsible for ensuring coherent access.
Message Queues
Pipes and Unix domain sockets are stream-oriented — they carry a sequence of bytes with no inherent message boundaries. A sender that writes 100 bytes and then 50 bytes may have them read back as 150 bytes in a single read, or as 30 bytes and 120 bytes, or in any other combination the kernel's buffering produces.
POSIX message queues provide a message-oriented interface. Each mq_send() call writes one message. Each mq_receive() call reads one complete message — never a partial message, never two messages combined. Messages have a priority field; higher-priority messages are delivered before lower-priority ones regardless of arrival order.
POSIX message queues appear under /dev/mqueue:
ls -la /dev/mqueue/
Each queue is a file. The file's content is not the queue data — it is metadata showing the queue's capacity and current message count. The queue itself lives in kernel memory.
Message queues are bounded — each queue has a maximum number of messages and a maximum message size, set at creation. A sender that writes to a full queue blocks until a receiver removes a message, or receives EAGAIN if the queue was opened with O_NONBLOCK. This backpressure is built in, unlike pipes where the application must handle it explicitly.
For most modern applications, Unix domain sockets have largely displaced message queues. Sockets are more flexible, support streaming and datagram modes, and work between hosts as well as locally. Message queues retain an advantage in scenarios requiring strict message ordering with priority, bounded queues, and no persistent connection between sender and receiver.
Signals as Lightweight IPC
Signals are the lightest IPC mechanism and the oldest. A signal is a small integer sent from one process to another — or from the kernel to a process — that causes the recipient to execute a registered handler function or take a default action.
Signals carry almost no data. The signal number identifies the event; there is no payload beyond a small siginfo_t structure for some signal types. They are not reliable: multiple instances of the same signal delivered before the handler runs may be collapsed into one. They are not ordered relative to other signals. They interrupt whatever the receiving process was doing at an arbitrary point.
Despite these limitations, signals are indispensable. They are the mechanism for process control (SIGTERM, SIGKILL, SIGSTOP, SIGCONT), for notification of asynchronous events (SIGIO, SIGCHLD), and for error conditions (SIGSEGV, SIGFPE, SIGBUS).
Choosing the Right Mechanism
| Mechanism | Direction | Boundaries | Synchronisation | Best for |
|---|---|---|---|---|
| Anonymous pipe | Unidirectional | None (stream) | Kernel-managed | Shell pipelines, parent-child |
| Named pipe (FIFO) | Unidirectional | None (stream) | Kernel-managed | Unrelated processes, simple |
| Unix domain socket | Bidirectional | Stream or datagram | Application | Local services, credential passing |
| Shared memory | Both | None | Application | High-throughput, low-latency |
| Message queue | Both | Message | Kernel-managed | Ordered, prioritised, bounded |
| Signal | One-way notification | None | N/A | Process control, async events |
References
- Linux man page: pipe(2)
- Linux man page: unix(7) — Unix domain sockets
- Linux man page: mq_overview(7) — POSIX message queues
- Linux man page: shm_open(3) — POSIX shared memory
- Linux man page: mmap(2)