
6 Linux IPC Mechanisms: When to Use Each
There's no single best mechanism among the Linux IPC mechanisms, so stop looking for one. Pick pipes for parent-child streaming, Unix domain sockets for general client-server work, POSIX shared memory for high-throughput data, and signals only for lightweight async notification. Then verify every choice with strace and /proc instead of trusting your assumptions. Interprocess communication (IPC) is just the kernel moving data between processes that live in separate address spaces, and the wrong tool shows up at runtime as a hang or corrupted data, well after the compiler waved it through.
Last updated: 2026-07-21
I write this from years of losing evenings to IPC bugs that turned out to be one blocked read or one missing cleanup. So the guide is practical, direct, and focused on results. You will compile small C programs, run them across two terminals, and prove behavior with standard tools instead of trusting the man page.
What is IPC in Linux and why does the choice matter?
IPC is kernel-mediated data exchange between processes. A forked child gets its own copy of memory, so writes never flow back to the parent unless you open a shared path. Every one of those paths crosses into the kernel through a system call: read(), write(), sendmsg(), sigaction(). The kernel checks permissions, moves the bytes, and handles who sleeps and who wakes.
The choice affects correctness, not just speed. Pick a file when you need persistence or offline inspection. Pick a direct channel when you need low latency and tight coordination. Get it wrong and you inherit the failure modes: partial writes and locking bugs with files, blocking reads or lost signals with channels.
Here is the mental model I keep for every mechanism. Each one has a data path, a wake/sleep path, and a cleanup path. Miss any of the three and the bug hides until production. If you want the deeper story on how these calls enter the kernel, my Linux system calls walkthrough covers the boundary in detail.
The six IPC families you will actually use
Six families cover almost everything you will build, from an ls | grep pipeline to two daemons hammering one shared memory region. Here is the map the rest of the article fills in.
| Mechanism | Direction | Carries | Best for |
|---|---|---|---|
| Pipes / FIFOs | One way | Byte stream | Parent-child streaming, shell composition |
| Signals | One way | Intent, no payload | Async notification, reload, shutdown |
| Message queues | Two way | Typed records | Structured messages with buffering |
| Shared memory | Shared region | Raw bytes | Highest throughput, zero copies |
| Sockets (Unix domain) | Two way | Byte or datagram | General client-server, passing descriptors |
| Semaphores | N/A | Counter | Coordinating access to a shared region |
Semaphores are not a data channel. They coordinate the others, which is why shared memory is useless without one. Keep that split straight and half the confusion goes away.
Pipes and FIFOs for streaming between processes
A pipe is a one-way byte stream between related processes. pipe(fd) hands you two file descriptors: fd[0] to read, fd[1] to write. The standard pattern is fork(), then each side closes the end it does not use. If the child reads, close fd[1] in the child and fd[0] in the parent.
Closing the unused ends is not tidiness, it is correctness. A reader only sees end-of-file when every write descriptor is closed. Leave one open and your read blocks forever. That is the classic pipe hang, and strace on the reader shows it plainly: a read() that never returns.
int fd[2];
pipe(fd);
if (fork() == 0) { /* child reads */
close(fd[1]);
char buf[64];
ssize_t n = read(fd[0], buf, sizeof buf);
/* n == 0 means EOF: all writers closed */
}
Write to a pipe with no reader and the kernel sends you SIGPIPE, which kills the process by default. That surprises people. Handle or ignore the signal and write() returns -1 with errno set to EPIPE instead.
Named pipes (FIFOs) do the same job for unrelated processes. Create one with mkfifo /tmp/myfifo, then any two programs can open it by path. Opening blocks until the other side shows up, which is deliberate and worth relying on. Run ls -l /tmp/myfifo and the leading p confirms it is a FIFO. Delete it with unlink when you are done, because a stale FIFO with wrong permissions is a denied-open you will chase later.
When a pipe read hangs, do not guess. Run ls -l /proc/PID/fd and count the open ends. That tells you whether a writer you thought was closed is still holding the descriptor.
Signals for asynchronous event notification
Signals are the smallest IPC there is: they carry intent and nothing else. Use them for stop, continue, reload config, or a bare wakeup. When the event matters but the data does not, a signal fits. When you need to carry a payload, reach for something else.
The rule that saves you: never do real work inside a signal handler. A handler can interrupt your program at any instruction, so only async-signal-safe functions are legal there, and that list is short. What I do is set one volatile sig_atomic_t flag in the handler and check it in the main loop. Install it with sigaction, not the old signal, because signal behaves differently across systems and hands you phantom bugs.
Two signals cannot be caught, blocked, or ignored: SIGKILL and SIGSTOP. So if your cleanup must run, wire it to SIGTERM, which is what a normal shutdown sends. A signal interrupting a slow system call makes that call return -1 with errno set to EINTR. Handle it by retrying the call, or set SA_RESTART in sigaction and let the kernel restart it for you.
When signals go missing, trace them. Run strace -e trace=signal -p PID and watch delivery in real time. Standard signals do not queue, so two of the same kind arriving while one is pending collapse into one. That coalescing is by design, and it is why you should never count signals as if they were messages. For the full handler setup, see my step-by-step signal handling guide.
Are System V IPC mechanisms still worth using?
Usually not for new code. System V gives you three objects: message queues, semaphores, and shared memory, all keyed by a number instead of a filesystem path. They predate the POSIX equivalents and carry the scars. A System V semaphore set requires at least 250 semaphores on Red Hat Enterprise Linux, and the API forces you through semop structs that read like assembly.
The real problem with System V is lifecycle. These objects outlive the process that made them. Crash before cleanup and the segment lingers in the kernel until reboot or manual removal. Run ipcs to list what is hanging around and ipcrm to delete it. You will run both more than you expect while debugging.
ipcs -m # list shared memory segments
ipcs -q # list message queues
ipcs -s # list semaphore sets
ipcrm -m <id> # remove a leaked segment
Access control lives in a struct ipc_perm on every object, with an owner, creator, and mode bits like a file. A key can be public or private; IPC_PRIVATE makes an object only the creator's descendants can find. Reach for System V when you are maintaining code that already uses it. Otherwise the POSIX interfaces (mq_open, sem_open, shm_open) are cleaner, use file descriptors, and clean up on close.
Message queues versus socket-based IPC
Message queues win when you need typed records and selective reads. A receiver can pull the next message of a given type and leave the rest, while the kernel buffers everything until it wakes. That is genuinely useful for a work dispatcher where consumers care about categories.
Sockets win almost everywhere else. Unix domain sockets give you bidirectional streams or datagrams over a filesystem path, they scale from one machine to the network with a one-line change, and they can pass open file descriptors between processes over SCM_RIGHTS. No message-queue mechanism does that last trick. When your protocol is going to grow, start with a socket and skip the migration later.
The one honest downside of sockets is that you frame your own messages. A stream socket hands you bytes, so you decide where one record ends and the next begins. Message queues do that framing for you. If your data is naturally record-shaped and stays on one host, a queue saves you code. If you expect flexibility, take the socket. The Linux manual page for Unix domain sockets documents the descriptor-passing details worth reading before you commit.
Shared memory versus message passing
Shared memory is the fastest option and the most dangerous. Both processes map the same physical pages, so a write by one is instantly visible to the other with zero copies. On Linux the default cap on a single segment is enormous, 2^64 bytes, so size is never your constraint. Correctness is.
Message passing copies data through the kernel, which costs cycles but buys you safety. Each message arrives whole, in order, with no torn reads. Shared memory hands you none of that. Two processes writing the same region without a lock produce a race, and races show up as data that is almost right, which is the worst kind of bug to reproduce.
So the tradeoff is blunt. Choose message passing when correctness is fragile and throughput is fine. Choose shared memory when you have measured a real bottleneck and you are ready to carry the synchronization burden yourself. Do not reach for it because it sounds fast.
One more gotcha: the same segment can map to a different virtual address in each process. So never store a raw pointer in shared memory. Store an offset from the base of the region and compute the address on each side. A pointer that is valid in the writer is garbage in the reader.
Synchronizing processes that share memory
Pair every shared region with a lock, and keep the locked section tiny. A semaphore is the usual tool: one process decrements it to enter the critical section and increments it to leave. The operation is atomic, and a waiter sleeps instead of spinning, so you do not burn a core waiting your turn.
You have three practical choices for the lock itself:
- POSIX semaphores (
sem_open,sem_wait,sem_post) for named coordination between unrelated processes. - Process-shared mutexes placed inside the shared region, initialized with
PTHREAD_PROCESS_SHARED, when you want mutex semantics across processes. - Futexes, the kernel primitive underneath both, which you rarely call directly but will see in traces.
System V semaphores add SEM_UNDO, which rolls back your operation if the process dies while holding it. That saves you from a permanent deadlock when a crash leaves the counter wrong. It is one of the few System V features I actually miss in the POSIX world.
When two processes deadlock on a shared lock, attach gdb -p PID to each and dump the stack. If both are parked in futex, you have found your deadlock, and the backtrace names the lock. Confirm it under load with strace -f and watch which process holds while the other waits. Guessing at lock order wastes the night; the trace tells you in a minute.
Which IPC mechanism should you choose?
Answer four questions and the choice falls out. Do you need an event or data? Structured records or a raw byte firehose? One machine or possibly the network? Related processes or strangers?
- Event, no payload: signals. Reload, stop, wake. Nothing more.
- Parent-child byte stream: an anonymous pipe. Simplest thing that works.
- Unrelated processes, filesystem rendezvous: a FIFO or a Unix domain socket.
- General client-server, may grow to network: Unix domain sockets. My default.
- Typed records buffered for a sleeping reader: a message queue.
- Measured high throughput, ready to lock: POSIX shared memory plus a semaphore.
Pick the least complex mechanism that meets your size, ordering, and throughput needs. I have watched people reach for shared memory to move a few kilobytes a second, then spend a week debugging a race that a pipe would never have had. Complexity is a cost you pay in debugging time, so spend it only when a benchmark says you must.
Remember the default limit of 1024 open file descriptors per process. Socket-heavy and pipe-heavy designs both consume descriptors fast, so a server fanning out to thousands of clients will hit that ceiling. Raise it deliberately with ulimit -n and know why, rather than crashing into it. My guide on managing file descriptors covers the limits and how to watch them.
Debugging IPC failures with strace and /proc

Reproduce the failure, then trace it. Do not read the code and theorize. Run strace -f -T ./myprog and watch the exact system call where it stalls. The -f follows forked children, and -T prints how long each call took, so a read() that hangs stands out immediately.
Three failures cover most of what you will hit. A hang is a process blocked in read, write, or semop waiting on something that never comes. Partial data means your framing is wrong, or a shared write raced a read. A permission error is EACCES on an IPC object whose mode bits or owner do not match.
Here is the order I work in:
strace -f -p PIDto find the blocked call and its arguments.ls -l /proc/PID/fdto see every open descriptor and where it points.cat /proc/PID/statusto read the process state and any pending signals.ipcsto list System V objects and check owner and mode.dmesg -T | tailif you suspect the kernel killed something, since it usually says why.
When the trace still does not explain it, read the kernel source for the call. The exact semantics of a blocking msgrcv or a SIGPIPE on a closed pipe are written down, and the source is the only place that never lies. Understand the failure or it comes back at the worst time.
Build cleanup into every exit path from the start. Close descriptors, unlink FIFOs, and ipcrm or shm_unlink your segments in a handler and on normal exit. A leaked shared segment or a stale semaphore is a bug your next run inherits. For the fork and wait details behind clean process teardown, see my Linux process lifecycle walkthrough.
FAQ
Do I need to call wait() after fork() when using IPC?
Yes, or handle SIGCHLD. A child that exits before the parent reaps it becomes a zombie holding a slot in the process table. It does not consume much, but leak enough of them and you exhaust the table. Call waitpid() in the parent, or install a SIGCHLD handler that reaps children as they die.
Why does my pipe read return zero instead of blocking?
A read() return of zero means end-of-file: every write end of that pipe is closed. The pipe is telling you no more data is coming. If you expected data, a writer closed its descriptor early, or the reading process never closed its own write end and confused itself. Check the open descriptors under /proc/PID/fd.
Can two unrelated processes share memory without System V?
Yes, and you should prefer it. Use the POSIX interface: shm_open to create or open a named segment under /dev/shm, ftruncate to size it, then mmap to map it into both processes. It uses file descriptors, cleans up with shm_unlink, and avoids the key-based lifecycle headaches of the older System V calls.
How do I remove an IPC object that a crashed process left behind?
For System V objects, list them with ipcs and delete by id with ipcrm -m, -q, or -s for memory, queues, and semaphores. For POSIX shared memory, remove the file under /dev/shm or call shm_unlink. Automate this in a cleanup handler so a crash does not force you to do it by hand every time.
Is a Unix domain socket faster than a TCP socket on the same machine?
Yes, noticeably. A Unix domain socket skips the TCP/IP stack, checksums, and loopback routing, so it moves data with less overhead and lower latency than a TCP connection to localhost. It also lets you pass file descriptors between processes, which TCP cannot. Use it whenever both endpoints are on one host and you do not need the network.
