Skip to main content

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

IPC Mechanisms 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", 0666);  // 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

PropertyValue
Capacity64KB typical (Linux)
OrderingFIFO
PersistenceKernel buffer only
BlockingYes (when full/empty)
DirectionUnidirectional

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, 0666);
    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, 0666);
    
    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, 0666, 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, 0666, &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

FeaturePipeMessage Queue
StructureByte streamDiscrete messages
PriorityNoYes
Multiple readersNoYes
PersistenceKernel onlyCan survive process
Size limit~64KBConfigurable

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

SignalDefault ActionUse Case
SIGINTTerminateCtrl+C
SIGTERMTerminateGraceful shutdown request
SIGKILLTerminateForceful kill (can’t catch)
SIGSTOPStopPause process (can’t catch)
SIGCONTContinueResume paused process
SIGUSR1/2TerminateUser-defined
SIGCHLDIgnoreChild process state change
SIGPIPETerminateWrite 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

AspectUnix DomainTCP/IP
Speed2-3x fasterNetwork overhead
ScopeSame machineNetwork-wide
File descriptorsCan pass FDs!No
CredentialsCan verifyHarder
AddressFilesystem pathIP: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

IPC Performance Comparison

IPC Performance Comparison

Interview Deep Dive Questions

Answer:Architecture (like Nginx):Web Server IPC ArchitectureIPC choices:
  1. Shared memory for hot data:
    • Configuration
    • Stats counters
    • Shared cache (with locking)
  2. Unix sockets for control:
    • Master → Worker commands
    • Passing file descriptors (socket passing)
  3. 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
}
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:
AspectShared MemoryMessage Passing
SpeedFastestSlower (copy)
SyncManualBuilt-in
CouplingTightLoose
DebuggingHarderEasier
ScalingSingle machineDistributed
Real examples:
  • Database buffer pool: Shared memory
  • Microservices: Message queues
  • Chrome tabs: Combination (shared for bitmaps, messages for control)
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, 0666);
    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);
    }
}
Answer:Chrome’s multi-process architecture:Chrome Multi-Process ArchitectureIPC mechanisms used:
  1. 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
  2. Named pipes (Windows) / Unix sockets (Linux/Mac):
    • Base transport layer
    • Renderer connects to browser on startup
  3. 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"
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 Linux
sigset_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

1

Chat Application

Build a multi-user chat using Unix domain sockets.
2

Shared Memory Cache

Implement a shared memory hash table with reader-writer locks.
3

Signal-Based Watchdog

Create a watchdog process that monitors children via signals.
4

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