Interprocess Communication — Pipes, Message Queues, Shared Memory & Sockets
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 sentShell 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
| Mechanism | Throughput | Latency | Complexity | Sharing Scope |
|---|---|---|---|---|
| Pipe | Medium | Low | Low | Related processes |
| FIFO | Medium | Low | Low | Any processes |
| Message Queue | Medium | Medium | Medium | Any processes |
| Shared Memory | Highest | Lowest | High | Any processes |
| Socket (Unix) | High | Low | Medium | Any processes |
| Socket (TCP) | Low | High | Medium | Network |
| Signals | N/A | N/A | Low | Related processes |
| Memory-mapped file | High | Low | Medium | Any 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.089sShared 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
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.
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.
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.
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().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
Mini Project: Multi-Process Chat Application
Build a chat application where:
- A central server manages connections via Unix domain sockets
- Clients connect and send messages to the server
- The server broadcasts messages to all other connected clients
- 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