Inter-Process Communication (IPC)
IPC allows processes to exchange data and synchronize actions. Choosing the right IPC mechanism is crucial for performance and is a key topic in systems design interviews.
Interview Frequency : High
Key Topics : Pipes, shared memory, message queues, sockets
Time to Master : 10-12 hours
IPC Overview
Pipes
Anonymous Pipes
For parent-child communication:
#include <unistd.h>
int main () {
int pipefd [ 2 ]; // [0] = read end, [1] = write end
if ( pipe (pipefd) == - 1 ) {
perror ( "pipe" );
return 1 ;
}
pid_t pid = fork ();
if (pid == 0 ) {
// Child: read from pipe
close ( pipefd [ 1 ]); // Close unused write end
char buffer [ 100 ];
ssize_t n = read ( pipefd [ 0 ], buffer, sizeof (buffer));
printf ( "Child received: %.*s \n " , ( int )n, buffer);
close ( pipefd [ 0 ]);
} else {
// Parent: write to pipe
close ( pipefd [ 0 ]); // Close unused read end
const char * msg = "Hello from parent!" ;
write ( pipefd [ 1 ], msg, strlen (msg));
close ( pipefd [ 1 ]);
wait ( NULL );
}
return 0 ;
}
Named Pipes (FIFOs)
For unrelated processes:
#include <sys/stat.h>
#include <fcntl.h>
// Writer process
int writer () {
mkfifo ( "/tmp/myfifo" , 0 666 ); // Create FIFO
int fd = open ( "/tmp/myfifo" , O_WRONLY);
write (fd, "Hello FIFO!" , 11 );
close (fd);
return 0 ;
}
// Reader process (separate program)
int reader () {
int fd = open ( "/tmp/myfifo" , O_RDONLY);
char buffer [ 100 ];
ssize_t n = read (fd, buffer, sizeof (buffer));
printf ( "Received: %.*s \n " , ( int )n, buffer);
close (fd);
return 0 ;
}
Pipe Characteristics
Property Value Capacity 64KB typical (Linux) Ordering FIFO Persistence Kernel buffer only Blocking Yes (when full/empty) Direction Unidirectional
Shared Memory
The fastest IPC — no kernel involvement for data transfer:
POSIX Shared Memory
#include <sys/mman.h>
#include <fcntl.h>
#include <semaphore.h>
typedef struct {
sem_t sem;
int counter;
char data [ 1024 ];
} shared_data_t ;
// Process 1: Create and write
int producer () {
// Create shared memory object
int fd = shm_open ( "/myshm" , O_CREAT | O_RDWR, 0 666 );
ftruncate (fd, sizeof ( shared_data_t ));
// Map into address space
shared_data_t * shared = mmap ( NULL , sizeof ( shared_data_t ),
PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0 );
// Initialize
sem_init ( & shared -> sem , 1 , 1 ); // 1 = shared between processes
shared -> counter = 0 ;
// Write data
sem_wait ( & shared -> sem );
strcpy ( shared -> data , "Hello shared memory!" );
shared -> counter ++ ;
sem_post ( & shared -> sem );
// Keep running or cleanup
munmap (shared, sizeof ( shared_data_t ));
close (fd);
return 0 ;
}
// Process 2: Read
int consumer () {
int fd = shm_open ( "/myshm" , O_RDWR, 0 666 );
shared_data_t * shared = mmap ( NULL , sizeof ( shared_data_t ),
PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0 );
sem_wait ( & shared -> sem );
printf ( "Data: %s , Counter: %d \n " , shared -> data , shared -> counter );
sem_post ( & shared -> sem );
munmap (shared, sizeof ( shared_data_t ));
close (fd);
// Cleanup when done
shm_unlink ( "/myshm" );
return 0 ;
}
mmap for Shared Memory
// Anonymous shared mapping (for related processes)
void * shared = mmap ( NULL , size,
PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS,
- 1 , 0 );
// File-backed shared mapping
int fd = open ( "shared_file" , O_RDWR);
void * shared = mmap ( NULL , file_size,
PROT_READ | PROT_WRITE,
MAP_SHARED,
fd, 0 );
// Changes written back to file
Shared Memory Synchronization
Critical : Shared memory requires explicit synchronization!
// Options for synchronization:
// 1. POSIX semaphores (in shared memory)
sem_t * sem = sem_open ( "/mysem" , O_CREAT, 0 666 , 1 );
// 2. pthread mutex with PTHREAD_PROCESS_SHARED
pthread_mutexattr_t attr;
pthread_mutexattr_init ( & attr );
pthread_mutexattr_setpshared ( & attr , PTHREAD_PROCESS_SHARED);
pthread_mutex_init ( & shared -> mutex , & attr );
// 3. Atomic operations
#include <stdatomic.h>
atomic_fetch_add ( & shared -> counter , 1 );
Message Queues
Structured message passing with priority support:
POSIX Message Queues
#include <mqueue.h>
#define QUEUE_NAME "/myqueue"
#define MAX_MSG_SIZE 256
#define MAX_MSGS 10
// Sender
int sender () {
struct mq_attr attr = {
.mq_flags = 0 ,
.mq_maxmsg = MAX_MSGS,
.mq_msgsize = MAX_MSG_SIZE,
.mq_curmsgs = 0
};
mqd_t mq = mq_open (QUEUE_NAME, O_CREAT | O_WRONLY, 0 666 , & attr);
// Send with priority (higher = more urgent)
mq_send (mq, "High priority!" , 14 , 10 );
mq_send (mq, "Low priority" , 12 , 1 );
mq_close (mq);
return 0 ;
}
// Receiver
int receiver () {
mqd_t mq = mq_open (QUEUE_NAME, O_RDONLY);
char buffer [MAX_MSG_SIZE];
unsigned int priority;
// Receives highest priority first
while ( 1 ) {
ssize_t bytes = mq_receive (mq, buffer, MAX_MSG_SIZE, & priority);
if (bytes > 0 ) {
printf ( "Priority %u : %.*s \n " , priority, ( int )bytes, buffer);
}
}
mq_close (mq);
mq_unlink (QUEUE_NAME);
return 0 ;
}
Message Queue vs Pipe
Feature Pipe Message Queue Structure Byte stream Discrete messages Priority No Yes Multiple readers No Yes Persistence Kernel only Can survive process Size limit ~64KB Configurable
Signals
Asynchronous notification mechanism:
#include <signal.h>
volatile sig_atomic_t got_signal = 0 ;
void handler ( int sig ) {
got_signal = 1 ;
// Keep handler simple! Only async-signal-safe functions
}
int main () {
// Set up handler
struct sigaction sa = {
.sa_handler = handler,
.sa_flags = SA_RESTART, // Restart interrupted syscalls
};
sigemptyset ( & sa . sa_mask );
sigaction (SIGUSR1, & sa, NULL );
printf ( "PID: %d , waiting for SIGUSR1... \n " , getpid ());
while ( ! got_signal) {
pause (); // Wait for signal
}
printf ( "Received signal! \n " );
return 0 ;
}
// Send from another process:
// kill -USR1 <pid>
Common Signals
Signal Default Action Use Case SIGINTTerminate Ctrl+C SIGTERMTerminate Graceful shutdown request SIGKILLTerminate Forceful kill (can’t catch) SIGSTOPStop Pause process (can’t catch) SIGCONTContinue Resume paused process SIGUSR1/2Terminate User-defined SIGCHLDIgnore Child process state change SIGPIPETerminate Write to closed pipe
Signal Safety
Only call async-signal-safe functions in handlers!
// UNSAFE - can cause deadlock/corruption
void bad_handler ( int sig ) {
printf ( "Signal received \n " ); // NOT safe!
malloc ( 100 ); // NOT safe!
}
// SAFE - minimal work
void good_handler ( int sig ) {
// Set flag only
got_signal = 1 ;
// Or write to pipe for self-pipe trick
write ( signal_pipe [ 1 ], "x" , 1 ); // write() is safe
}
Unix Domain Sockets
For high-performance local IPC:
#include <sys/socket.h>
#include <sys/un.h>
#define SOCKET_PATH "/tmp/mysocket"
// Server
int server () {
int server_fd = socket (AF_UNIX, SOCK_STREAM, 0 );
struct sockaddr_un addr = {
.sun_family = AF_UNIX,
};
strncpy ( addr . sun_path , SOCKET_PATH, sizeof ( addr . sun_path ) - 1 );
unlink (SOCKET_PATH);
bind (server_fd, ( struct sockaddr * ) & addr, sizeof (addr));
listen (server_fd, 5 );
while ( 1 ) {
int client_fd = accept (server_fd, NULL , NULL );
char buffer [ 1024 ];
ssize_t n = read (client_fd, buffer, sizeof (buffer));
write (client_fd, "ACK" , 3 );
close (client_fd);
}
close (server_fd);
unlink (SOCKET_PATH);
return 0 ;
}
// Client
int client () {
int sock = socket (AF_UNIX, SOCK_STREAM, 0 );
struct sockaddr_un addr = {
.sun_family = AF_UNIX,
};
strncpy ( addr . sun_path , SOCKET_PATH, sizeof ( addr . sun_path ) - 1 );
connect (sock, ( struct sockaddr * ) & addr, sizeof (addr));
write (sock, "Hello server!" , 13 );
char buffer [ 1024 ];
read (sock, buffer, sizeof (buffer));
close (sock);
return 0 ;
}
Unix Sockets vs Network Sockets
Aspect Unix Domain TCP/IP Speed 2-3x faster Network overhead Scope Same machine Network-wide File descriptors Can pass FDs! No Credentials Can verify Harder Address Filesystem path IP:port
Passing File Descriptors
// Send a file descriptor to another process!
void send_fd ( int sock , int fd_to_send ) {
struct msghdr msg = { 0 };
struct cmsghdr * cmsg;
char buf [ CMSG_SPACE ( sizeof ( int ))];
msg . msg_control = buf;
msg . msg_controllen = sizeof (buf);
cmsg = CMSG_FIRSTHDR ( & msg);
cmsg -> cmsg_level = SOL_SOCKET;
cmsg -> cmsg_type = SCM_RIGHTS;
cmsg -> cmsg_len = CMSG_LEN ( sizeof ( int ));
* (( int * ) CMSG_DATA (cmsg)) = fd_to_send;
sendmsg (sock, & msg, 0 );
}
// Receive the file descriptor
int receive_fd ( int sock ) {
struct msghdr msg = { 0 };
char buf [ CMSG_SPACE ( sizeof ( int ))];
msg . msg_control = buf;
msg . msg_controllen = sizeof (buf);
recvmsg (sock, & msg, 0 );
struct cmsghdr * cmsg = CMSG_FIRSTHDR ( & msg);
return * (( int * ) CMSG_DATA (cmsg));
}
D-Bus
Modern IPC for desktop/system services:
// D-Bus is typically used via libraries
// Example concept (not complete code):
// Service: Register a method
void register_method () {
// "org.example.MyService" provides "Hello" method
// Other processes can call it
}
// Client: Call the method
void call_method () {
// Call org.example.MyService.Hello("World")
// Returns "Hello, World!"
}
// Used by: systemd, GNOME, KDE, many desktop apps
Interview Deep Dive Questions
Q1: Design IPC for a web server with worker processes
Answer: Architecture (like Nginx):IPC choices :
Shared memory for hot data :
Configuration
Stats counters
Shared cache (with locking)
Unix sockets for control :
Master → Worker commands
Passing file descriptors (socket passing)
Signals for lifecycle :
SIGTERM: Graceful shutdown
SIGHUP: Reload config
SIGCHLD: Worker died
Implementation :// Master creates shared memory
shared_state_t * state = mmap ( NULL , sizeof ( shared_state_t ),
PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS,
- 1 , 0 );
// Fork workers
for ( int i = 0 ; i < num_workers; i ++ ) {
if ( fork () == 0 ) {
// Worker inherits shared memory mapping
worker_main (state, i);
exit ( 0 );
}
}
// For socket passing: master accepts, passes to least loaded worker
void pass_connection ( int worker_socket , int client_fd ) {
send_fd (worker_socket, client_fd);
close (client_fd); // Worker now owns it
}
Q2: When would you use shared memory vs message passing?
Answer: Use Shared Memory when :
Maximum performance is critical
Large data volumes
Frequent access to same data
Can implement synchronization correctly
Processes on same machine
Use Message Passing when :
Simpler programming model
Natural request-response pattern
Processes may be on different machines
Message boundaries important
Don’t want to deal with sync primitives
Comparison :Aspect Shared Memory Message Passing Speed Fastest Slower (copy) Sync Manual Built-in Coupling Tight Loose Debugging Harder Easier Scaling Single machine Distributed
Real examples :
Database buffer pool: Shared memory
Microservices: Message queues
Chrome tabs: Combination (shared for bitmaps, messages for control)
Q3: Implement a producer-consumer with shared memory
Answer: #include <sys/mman.h>
#include <semaphore.h>
#include <fcntl.h>
#define BUFFER_SIZE 10
typedef struct {
int buffer [BUFFER_SIZE];
int in;
int out;
sem_t empty; // Counts empty slots
sem_t full; // Counts full slots
sem_t mutex; // Protects buffer access
} shared_buffer_t ;
shared_buffer_t * create_shared_buffer () {
int fd = shm_open ( "/buffer" , O_CREAT | O_RDWR, 0 666 );
ftruncate (fd, sizeof ( shared_buffer_t ));
shared_buffer_t * buf = mmap ( NULL , sizeof ( shared_buffer_t ),
PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0 );
buf -> in = buf -> out = 0 ;
sem_init ( & buf -> empty , 1 , BUFFER_SIZE);
sem_init ( & buf -> full , 1 , 0 );
sem_init ( & buf -> mutex , 1 , 1 );
return buf;
}
void producer ( shared_buffer_t * buf ) {
while ( 1 ) {
int item = produce_item ();
sem_wait ( & buf -> empty );
sem_wait ( & buf -> mutex );
buf -> buffer [ buf -> in ] = item;
buf -> in = ( buf -> in + 1 ) % BUFFER_SIZE;
sem_post ( & buf -> mutex );
sem_post ( & buf -> full );
}
}
void consumer ( shared_buffer_t * buf ) {
while ( 1 ) {
sem_wait ( & buf -> full );
sem_wait ( & buf -> mutex );
int item = buf -> buffer [ buf -> out ];
buf -> out = ( buf -> out + 1 ) % BUFFER_SIZE;
sem_post ( & buf -> mutex );
sem_post ( & buf -> empty );
consume_item (item);
}
}
Q4: How does Chrome isolate tabs using IPC?
Answer: Chrome’s multi-process architecture :IPC mechanisms used :
Mojo (Chrome’s IPC framework):
Message pipes for bidirectional communication
Shared memory for large data (images, video frames)
Type-safe interfaces defined in Mojo IDL
Named pipes (Windows) / Unix sockets (Linux/Mac):
Base transport layer
Renderer connects to browser on startup
Shared memory for:
Rendered page bitmaps
Video frames
Audio buffers
Why this design :
Security : Renderer crash doesn’t take down browser
Stability : One bad tab doesn’t crash others
Sandbox : Renderers have minimal OS access
Parallelism : Tabs can use multiple CPUs
Example message flow :Renderer wants to fetch URL:
1. Renderer → IPC → Browser: "Fetch https://..."
2. Browser performs network request (renderer can't)
3. Browser → IPC → Renderer: Response data
4. Renderer processes HTML (sandboxed)
5. Renderer → IPC → Browser: "Render this bitmap"
Q5: Implement self-pipe trick for signal handling
Answer: Problem : Need to handle signals in event loop (select/poll/epoll)Solution : Write to pipe from signal handler, read in event loop#include <signal.h>
#include <sys/select.h>
#include <unistd.h>
int signal_pipe [ 2 ];
void signal_handler ( int sig ) {
// write() is async-signal-safe
char c = sig;
write ( signal_pipe [ 1 ], & c, 1 );
}
int main () {
pipe (signal_pipe);
// Make write end non-blocking
fcntl ( signal_pipe [ 1 ], F_SETFL, O_NONBLOCK);
// Set up signal handler
struct sigaction sa = {
.sa_handler = signal_handler,
.sa_flags = SA_RESTART,
};
sigemptyset ( & sa . sa_mask );
sigaction (SIGINT, & sa, NULL );
sigaction (SIGTERM, & sa, NULL );
// Main event loop
while ( 1 ) {
fd_set readfds;
FD_ZERO ( & readfds);
FD_SET ( signal_pipe [ 0 ], & readfds);
FD_SET (client_socket, & readfds);
int max_fd = ( signal_pipe [ 0 ] > client_socket)
? signal_pipe [ 0 ] : client_socket;
int ready = select (max_fd + 1 , & readfds, NULL , NULL , NULL );
if ( FD_ISSET ( signal_pipe [ 0 ], & readfds)) {
char sig;
read ( signal_pipe [ 0 ], & sig, 1 );
if (sig == SIGINT || sig == SIGTERM) {
printf ( "Shutting down gracefully... \n " );
break ;
}
}
if ( FD_ISSET (client_socket, & readfds)) {
handle_client ();
}
}
cleanup ();
return 0 ;
}
Modern alternative : signalfd() on Linuxsigset_t mask;
sigemptyset ( & mask );
sigaddset ( & mask , SIGINT);
sigprocmask (SIG_BLOCK, & mask , NULL );
int sfd = signalfd ( - 1 , & mask , 0 );
// Now read signals from sfd like a regular file descriptor
Practice Exercises
Chat Application
Build a multi-user chat using Unix domain sockets.
Shared Memory Cache
Implement a shared memory hash table with reader-writer locks.
Signal-Based Watchdog
Create a watchdog process that monitors children via signals.
Message Queue Priority
Build a task queue with priority support using POSIX mqueues.
Key Takeaways
Shared Memory is Fastest But requires careful synchronization. Best for high-volume data.
Pipes are Simple Byte streams, good for parent-child. FIFOs for unrelated processes.
Sockets are Flexible Unix domain for local (fast), TCP for network. Can pass FDs!
Signals are Tricky Only for notification. Use self-pipe trick for event loops.
Next: Linux Kernel →