Linux IPC: A Practical Guide to Pipes, Queues, Shared Memory, and Sockets

Linux tutorial - IT technology blog
Linux tutorial - IT technology blog

Quick Start: Why IPC Matters

Inter-process communication (IPC) is the underlying plumbing of every Linux system. Without these mechanisms, processes would operate in total isolation, unable to coordinate tasks or share data. You likely use IPC every time you open a terminal. When you run ps aux | grep nginx, the vertical bar represents a Pipe, moving data directly from one process to the next.

Try creating a Named Pipe (FIFO) to see this in action. Unlike standard pipes that vanish when a command finishes, a Named Pipe persists as a special file on your disk. You can identify them in a directory listing by the p attribute in the permission string.

# Terminal 1: Create the pipe and wait for data
mkfifo my_pipe
cat < my_pipe

# Terminal 2: Push data into the pipe
echo "Data sent at $(date)" > my_pipe

This simple tool allows two unrelated processes to exchange strings. However, as your architecture scales, you will need more robust methods like Shared Memory or Sockets to handle high-frequency data or complex binary structures.

Choosing the Right IPC Mechanism

Linux provides several ways for processes to talk. Picking the wrong one often creates performance bottlenecks or synchronization bugs. I usually categorize them by throughput and complexity based on my experience building high-load backend services.

1. Pipes and FIFOs

Pipes are the most straightforward IPC method. They operate in half-duplex mode, meaning data flows in one direction. Anonymous pipes work best for parent-child relationships. In contrast, Named Pipes (FIFOs) allow completely unrelated processes to communicate via the filesystem.

Keep in mind that standard pipes have a limited buffer, typically 64KB on modern Linux kernels. If the pipe hits this limit, the sending process will block. It stays paused until the receiver reads enough data to clear space.

2. Message Queues

Message Queues handle discrete blocks of data rather than a continuous byte stream. This allows you to assign a ‘type’ to each message. A receiver can then choose to pull messages out of order, perhaps prioritizing critical alerts over routine logs.

#include <sys/msg.h>
// Standard structure for a message queue
struct msg_buffer {
    long msg_type;     // Priority or category
    char msg_text[1024]; 
} message;

3. Shared Memory

Shared memory is the fastest IPC available. The kernel maps a specific segment of physical RAM into the address space of multiple processes. Because processes read and write directly to RAM, you bypass the overhead of copying data between user space and kernel space.

The trade-off is complexity. You must manage synchronization yourself using Semaphores or Mutexes. If two processes attempt to write to the same memory address simultaneously, you will face data corruption or segmentation faults.

4. Unix Domain Sockets (UDS)

Unix Domain Sockets behave like TCP/IP sockets but stay entirely within the kernel. They use the filesystem as a namespace. Since they skip the heavy lifting of network headers, checksums, and routing logic, they are significantly faster than localhost TCP. In my benchmarks, UDS often shows 50% lower latency than loopback TCP connections.

Advanced Pattern: High-Performance Hybrid IPC

While optimizing a high-throughput logging agent on Ubuntu 22.04, I found that passing massive JSON strings through Sockets spiked CPU usage. To fix this, I moved the heavy lifting to Shared Memory. I only used the Unix Socket to pass a small pointer (the memory address) to the receiving process.

This hybrid workflow follows four steps:

  1. Process A creates a shared memory segment via shmget().
  2. Process A writes a large data payload (e.g., a 2MB log batch) into that segment.
  3. Process A sends the shmid (the segment ID) to Process B through a Unix Domain Socket.
  4. Process B attaches to the memory, processes the data, and sends an acknowledgment back.

This approach reduced my CPU overhead by 40% while maintaining the reliable signaling provided by sockets.

Lessons from the Field

Debugging IPC-related deadlocks and memory leaks is a rite of passage for systems programmers. Here are the rules I follow to keep production environments stable.

Clean up your resources

System V IPC resources like shared memory segments are persistent. If your application crashes and fails to run its cleanup code, those segments stay in the kernel until the next reboot. Use the ipcs command to audit these resources and ipcrm to remove orphans manually.

# List all active shared memory segments
ipcs -m

# Manually remove a segment by ID
ipcrm -m 12345

Default to POSIX over System V

Linux supports both the legacy System V and the modern POSIX IPC APIs. I recommend the POSIX API (shm_open, mq_open). It uses file descriptors, making it more intuitive for developers familiar with standard file I/O. Furthermore, POSIX objects appear in /dev/shm, making them easier to inspect.

Avoid Blocking on FIFOs

If a writer opens a Named Pipe and no reader is connected, the writer will hang indefinitely. To prevent your entire service from freezing, always open FIFOs with the O_NONBLOCK flag in C, or use asynchronous I/O in higher-level languages like Python or Go.

Leverage /dev/shm for Speed

On most modern distros, /dev/shm is a temporary filesystem (tmpfs) mounted directly in RAM. If you need a fast, temporary storage area for scripts, write your files here. It is significantly faster than /tmp because the data never touches the physical disk.

# Instant RAM-based storage for scripts
echo "temp_state_data" > /dev/shm/app.state
cat /dev/shm/app.state

Understanding these channels allows you to build systems that are both fast and resilient. Start with Pipes for basic data flows, use Sockets for structured messaging, and reserve Shared Memory for your most demanding performance bottlenecks.

Share: