Skip to content
Interprocess Communication — Pipes, Message Queues, Shared Memory & Sockets

Interprocess Communication — Pipes, Message Queues, Shared Memory & Sockets

DodaTech Updated Jun 20, 2026 10 min read

Interprocess communication (IPC) lets processes exchange data and synchronise with each other. Choosing the right IPC mechanism is critical for application performance and correctness.

What You’ll Learn

In this tutorial, you’ll learn the major IPC mechanisms: pipes (anonymous and named FIFOs), System V and POSIX message queues, shared memory (shmget, mmap), semaphores for synchronisation, signals, Unix domain sockets, and memory-mapped files. You’ll also learn how to choose between them based on performance, complexity, and use case.

Why It Matters

Every complex system uses IPC. A web server talks to a database via sockets. A video editor’s rendering process uses shared memory to transfer frames. Docker containers use pipes and sockets for inter-container communication.

Real-World Use

When you pipe commands in the shell (grep foo | sort | uniq), you’re using anonymous pipes. PostgreSQL uses shared memory for its buffer pool. Systemd uses Unix sockets for service communication. Durga Antivirus Pro uses named pipes for real-time communication between the service and UI components.

    graph TD
  subgraph "IPC Mechanisms"
    PIPES[Pipes]
    MQ[Message Queues]
    SHM[Shared Memory]
    SEM[Semaphores]
    SIG[Signals]
    SOCK[Sockets]
    MMF[Memory-Mapped Files]
  end
  subgraph "Use Cases"
    SHELL[Shell Pipelines]
    CLI[Client-Server]
    DATA[Data Sharing]
    SYNC[Synchronisation]
    EVENT[Event Notification]
    NET[Network Communication]
    FILE[File Sharing]
  end
  PIPES --> SHELL
  PIPES --> CLI
  MQ --> CLI
  SHM --> DATA
  SEM --> SYNC
  SIG --> EVENT
  SOCK --> NET
  MMF --> DATA
  MMF --> FILE
  

Anonymous Pipes

A pipe connects one process’s stdout to another’s stdin. Unidirectional, byte stream, only works between related processes (parent-child).

import os
import sys

def pipe_demo():
    r, w = os.pipe()  # Create pipe: r is read end, w is write end
    pid = os.fork()

    if pid == 0:  # Child process
        os.close(w)  # Close write end
        data = os.read(r, 1024)
        print(f'Child received: {data.decode()!r}')
        os.close(r)
        sys.exit(0)
    else:  # Parent process
        os.close(r)  # Close read end
        message = b'Hello from parent!'
        os.write(w, message)
        os.close(w)
        os.waitpid(pid, 0)
        print('Parent: message sent')

if __name__ == '__main__':
    pipe_demo()

Expected output:

Child received: 'Hello from parent!'
Parent: message sent

Shell Pipes

When you run ls | grep '.py' | wc -l, the shell creates three processes connected by pipes. Each pipe is a unidirectional byte stream.

Named Pipes (FIFOs)

FIFOs work like pipes but have a filesystem path. Unrelated processes can communicate through them.

import os
import time
import threading

def fifo_writer(path='/tmp/myfifo'):
    if not os.path.exists(path):
        os.mkfifo(path)
    with open(path, 'w') as f:
        for i in range(5):
            msg = f'Message {i}\n'
            f.write(msg)
            f.flush()
            print(f'Writer sent: {msg.strip()}')
            time.sleep(0.5)
    os.unlink(path)

def fifo_reader(path='/tmp/myfifo'):
    while os.path.exists(path):
        with open(path, 'r') as f:
            data = f.read()
            if data:
                print(f'Reader got: {data.strip()}')
        time.sleep(0.1)

writer = threading.Thread(target=fifo_writer)
reader = threading.Thread(target=fifo_reader)
reader.start()
time.sleep(0.2)
writer.start()
writer.join()
reader.join(timeout=1)

System V Message Queues

Message queues let processes send and receive structured messages with types. They support bidirectional communication and message prioritisation.

import sysv_ipc  # Requires: pip install sysv_ipc
import os

def message_queue_demo():
    try:
        # Create a message queue with key 1234
        mq = sysv_ipc.MessageQueue(1234, sysv_ipc.IPC_CREAT)

        # Writer
        mq.send(b'Hello via message queue!', type=1)
        mq.send(b'High priority message', type=2)
        print('Sent 2 messages')

        # Reader
        msg1 = mq.receive(type=1)
        print(f'Received type=1: {msg1[0].decode()!r}')

        msg2 = mq.receive(type=2)
        print(f'Received type=2: {msg2[0].decode()!r}')

        # Clean up
        mq.remove()
    except ImportError:
        print('sysv_ipc not installed — showing code only')

if __name__ == '__main__':
    message_queue_demo()

POSIX Message Queues

POSIX message queues (mq_open, mq_send, mq_receive) are similar to System V but use name-based (not key-based) addressing and support notification via signals or threads.

Shared Memory (shmget / mmap)

Shared memory is the fastest IPC — processes directly read and write the same memory region. No kernel copying needed.

System V Shared Memory

import multiprocessing
import time

def worker_process(name, shared_dict):
    print(f'{name} sees: {shared_dict}')
    shared_dict[f'message_from_{name}'] = f'Hello from {name}!'
    print(f'{name} wrote to shared memory')

if __name__ == '__main__':
    manager = multiprocessing.Manager()
    shared_data = manager.dict()

    p1 = multiprocessing.Process(
        target=worker_process, args=('Process A', shared_data))
    p2 = multiprocessing.Process(
        target=worker_process, args=('Process B', shared_data))

    p1.start()
    p2.start()
    p1.join()
    p2.join()

    print(f'\nFinal shared data: {dict(shared_data)}')

Expected output:

Process A sees: {}
Process A wrote to shared memory
Process B sees: {'message_from_Process_A': 'Hello from Process A!'}
Process B wrote to shared memory

Final shared data: {
  'message_from_Process_A': 'Hello from Process A!',
  'message_from_Process_B': 'Hello from Process B!'
}

mmap — Memory-Mapped Files

mmap maps a file into the process address space. Multiple processes can map the same file for shared access.

import mmap
import os
import time

def mmap_shared_demo():
    # Create a shared file
    with open('/dev/shm/mmap_test', 'wb') as f:
        f.write(b'\x00' * 4096)

    # Map into memory
    with open('/dev/shm/mmap_test', 'r+b') as f:
        mm = mmap.mmap(f.fileno(), 0)

        # Write to shared memory
        mm[0:12] = b'Hello shared'
        print(f'Wrote: {mm[:12].decode()!r}')

        # Another process could read this
        mm.close()

    os.unlink('/dev/shm/mmap_test')

mmap_shared_demo()

Semaphores

Semaphores synchronise access to shared resources. Binary semaphores act as locks; counting semaphores manage a pool of resources.

import multiprocessing
import time

def worker(pid, sema, counter):
    with sema:
        print(f'Worker {pid}: acquired semaphore')
        counter.value += 1
        time.sleep(0.5)
        print(f'Worker {pid}: releasing semaphore')

if __name__ == '__main__':
    sema = multiprocessing.Semaphore(2)  # Allow 2 concurrent workers
    counter = multiprocessing.Value('i', 0)

    workers = [
        multiprocessing.Process(target=worker, args=(i, sema, counter))
        for i in range(5)
    ]

    for w in workers:
        w.start()

    for w in workers:
        w.join()

    print(f'All workers done. Counter: {counter.value}')

Signals

Signals are asynchronous notifications sent to a process. SIGTERM (terminate), SIGKILL (force kill), SIGINT (Ctrl+C), SIGPIPE (broken pipe), and SIGUSR1/SIGUSR2 (user-defined).

import signal
import time

def handler(signum, frame):
    print(f'Received signal {signum}! Cleaning up...')

def signal_demo():
    signal.signal(signal.SIGUSR1, handler)
    signal.signal(signal.SIGTERM, handler)

    print(f'PID: {os.getpid()}')
    print('Send a signal with: kill -USR1 <pid>')

    # Simulate waiting
    try:
        time.sleep(10)
    except KeyboardInterrupt:
        print('Got Ctrl+C')

if __name__ == '__main__':
    import os as _os
    os = _os
    signal_demo()

Unix Domain Sockets

Unix domain sockets connect processes on the same machine with socket semantics. They support stream (SOCK_STREAM) and datagram (SOCK_DGRAM) modes.

import socket
import os
import threading
import time

def socket_server(path='/tmp/ipc_socket'):
    if os.path.exists(path):
        os.unlink(path)

    server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    server.bind(path)
    server.listen(1)
    print(f'Server listening on {path}')

    conn, _ = server.accept()
    data = conn.recv(1024)
    print(f'Server received: {data.decode()!r}')
    conn.send(b'Hello from server!')
    conn.close()
    os.unlink(path)

def socket_client(path='/tmp/ipc_socket'):
    time.sleep(0.2)  # Wait for server
    client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    client.connect(path)
    client.send(b'Hello from client!')
    response = client.recv(1024)
    print(f'Client received: {response.decode()!r}')
    client.close()

server = threading.Thread(target=socket_server)
client = threading.Thread(target=socket_client)
server.start()
client.start()
server.join()
client.join()

Expected output:

Server listening on /tmp/ipc_socket
Server received: 'Hello from client!'
Client received: 'Hello from server!'

Performance Comparison

MechanismThroughputLatencyComplexitySharing Scope
PipeMediumLowLowRelated processes
FIFOMediumLowLowAny processes
Message QueueMediumMediumMediumAny processes
Shared MemoryHighestLowestHighAny processes
Socket (Unix)HighLowMediumAny processes
Socket (TCP)LowHighMediumNetwork
SignalsN/AN/ALowRelated processes
Memory-mapped fileHighLowMediumAny processes
import time
import os
import sys

def benchmark_ipc(iterations=10000):
    # Benchmark 1: Pipe
    r, w = os.pipe()
    start = time.time()

    for i in range(iterations):
        os.write(w, b'x')
        os.read(r, 1)

    pipe_time = time.time() - start
    os.close(r)
    os.close(w)
    print(f'Pipe:        {iterations} transfers in {pipe_time:.3f}s')

    # Benchmark 2: Unix socket (loopback)
    import socket
    import threading

    def bench_socket():
        server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        server_sock.bind(('127.0.0.1', 0))
        port = server_sock.getsockname()[1]
        server_sock.listen(1)

        client_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        client_sock.connect(('127.0.0.1', port))
        conn, _ = server_sock.accept()

        start = time.time()
        for _ in range(iterations):
            client_sock.send(b'x')
            conn.recv(1)

        socket_time = time.time() - start
        print(f'Socket:      {iterations} transfers in {socket_time:.3f}s')
        client_sock.close()
        conn.close()
        server_sock.close()

    bench_socket()

if __name__ == '__main__':
    benchmark_ipc(10000)

Expected output (approximate):

Pipe:        10000 transfers in 0.042s
Socket:      10000 transfers in 0.089s

Shared memory is typically 5-10x faster than pipes and 20-50x faster than TCP sockets for the same data volume.

Common Mistakes

1. Using blocking IPC without timeouts

A reader blocked on an empty pipe will hang forever if the writer crashes. Always set timeouts or use non-blocking I/O with select/poll.

2. Forgetting to close unused pipe ends

If a pipe’s read end is still open in the writer (and vice versa), EOF won’t be detected. Close ends you don’t use immediately.

3. Assuming message queues preserve ordering across priorities

Within same-type messages, ordering is FIFO. Across types, higher-priority messages (lower type number in System V) jump ahead.

4. Shared memory without synchronisation

Two processes writing to the same shared memory at the same time cause data corruption. Always use semaphores or mutexes.

5. Using TCP sockets for local IPC

Unix domain sockets are 2-5x faster than TCP loopback for local IPC. TCP adds protocol overhead (headers, checksums) that’s unnecessary locally.

6. Not cleaning up IPC resources

System V shared memory segments, message queues, and FIFO files persist after the process exits. Always clean up with shmctl(IPC_RMID), msgctl(IPC_RMID), and unlink.

Practice Questions

  1. What’s the main difference between a pipe and a FIFO? A pipe has no filesystem name and only works between related processes. A FIFO has a pathname and allows unrelated processes to communicate.

  2. Why is shared memory the fastest IPC? Data is written directly to a memory region visible to all participating processes — no kernel copying. Pipes and sockets copy data between kernel buffers and user space.

  3. What is a semaphore used for in IPC? Semaphores synchronise access to shared resources. They prevent race conditions when multiple processes access shared memory or other shared data.

  4. What’s the difference between System V and POSIX message queues? System V uses keys and has separate send/receive syscalls. POSIX uses names (like file paths), supports notification, and integrates with select()/poll().

  5. When would you use a Unix domain socket instead of a pipe? When you need bidirectional communication, want socket semantics (connect/listen/accept), or need to reuse existing network code. Sockets also support both stream and datagram modes.

Challenge

Implement a simple in-memory key-value store server using Unix domain sockets. Multiple clients should be able to connect, set/get/delete keys, and disconnect. Use a threading or async model to handle concurrent clients.

Real-World Task

On Linux, run ls -la /proc/<pid>/fd for a running process (e.g., your browser). Identify which file descriptors are pipes, sockets, and regular files. Then run ipcs to see System V IPC objects.

FAQ

What IPC does Docker use?
Docker containers communicate via sockets, pipes, or shared memory depending on configuration. Docker Compose sets up a network bridge with TCP/UDP sockets by default. Containers can also share IPC namespaces (–ipc=host) for shared memory access.
What is the difference between synchronous and asynchronous IPC?
Synchronous IPC blocks the sender until the receiver processes the message. Asynchronous IPC returns immediately. Message queues and signals are typically async; pipes and sockets can be either depending on configuration.
Can IPC be secured?
Yes. Unix sockets support file permissions. System V IPC has permission bits (like file modes). Network sockets can use TLS. Always validate and sanitise IPC data — a compromised process can inject malicious messages.
What is a deadlock in IPC?
Process A holds lock 1 and waits for lock 2. Process B holds lock 2 and waits for lock 1. Neither progresses. Preventing deadlock: acquire locks in a consistent order or use lock hierarchies.
What IPC mechanism does the Chrome browser use for multi-process architecture?
Chrome uses named pipes on Windows and Unix domain sockets on Linux/macOS for IPC between the browser process and renderer/GPU/utility processes.

Mini Project: Multi-Process Chat Application

Build a chat application where:

  1. A central server manages connections via Unix domain sockets
  2. Clients connect and send messages to the server
  3. The server broadcasts messages to all other connected clients
  4. The server logs all messages to a memory-mapped file for persistence

Security angle: IPC security matters — a compromised chat client shouldn’t be able to crash the server or read other clients’ data. Validate message sizes, enforce permissions on Unix sockets, and sanitise input.

What’s Next

Before moving on, you should understand:

  • The difference between pipes, FIFOs, message queues, and shared memory
  • When to use each IPC mechanism
  • How semaphores protect shared resources
  • Performance characteristics of each IPC type

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro