Inter-Process Communication

Learn how processes communicate and share data in modern operating systems

What is Inter-Process Communication?

Inter-Process Communication (IPC) refers to the mechanisms an operating system provides to allow processes to communicate with each other and synchronize their actions. Since processes typically run in isolated memory spaces to ensure stability and security, IPC methods provide controlled ways for processes to exchange data and signals.

Why IPC is Essential

Modern computing relies heavily on IPC for everything from client-server applications to microservices architectures. IPC enables the modular design of software systems, where different components can work together while maintaining isolation. This approach improves security, stability, and potentially performance through parallelism.

Core Purposes of IPC

Shared Memory
Pipes
Sockets
Message Queues

Communication Models in IPC

IPC mechanisms generally fall into two communication models:

Process A
Process B
Note: Some IPC mechanisms combine elements of both models or offer variations tailored to specific use cases.

Common IPC Mechanisms

Operating systems provide various IPC mechanisms, each with different characteristics suitable for different scenarios:

Shared Memory

Shared memory allows multiple processes to access the same block of memory. It's typically the fastest IPC method as data doesn't need to be copied between processes.

  • Advantages: Very high speed, especially for large amounts of data
  • Challenges: Requires synchronization mechanisms (semaphores, mutexes) to prevent race conditions
  • Use cases: High-performance computing, multimedia processing

Pipes

Pipes provide a one-way flow of data between processes. Standard pipes connect related processes (like parent and child), while named pipes (FIFOs) allow unrelated processes to communicate.

  • Advantages: Simple to use, built into most shell environments
  • Limitations: Typically unidirectional, often limited to related processes
  • Use cases: Command-line pipelines, producer-consumer patterns

Message Queues

Message queues allow processes to exchange discrete messages in a structured format. They provide asynchronous communication with automatic storage and retrieval of messages.

  • Advantages: Persistence of messages, prioritization capability, many-to-many communication
  • Limitations: More complex than pipes, potential overhead
  • Use cases: Task distribution systems, loosely coupled services

Sockets

Sockets provide a communication endpoint for sending and receiving data across a network or within a single system (Unix domain sockets).

  • Advantages: Work across different machines, standard API, bidirectional
  • Limitations: More overhead than other local IPC methods
  • Use cases: Network applications, client-server architectures, distributed systems

Signals

Signals are software interrupts sent to a process to notify it of important events. They're used for simple notifications rather than data exchange.

  • Advantages: Lightweight, asynchronous, built into the OS
  • Limitations: Limited information transfer, can be lost if not handled promptly
  • Use cases: Terminating processes, alerting about exceptions

Semaphores

Semaphores are synchronization primitives used to control access to shared resources. They don't transfer data directly but coordinate access to shared data.

  • Advantages: Prevent race conditions, relatively simple to implement
  • Limitations: Can lead to deadlocks if not carefully managed
  • Use cases: Synchronizing access to shared memory, implementing mutual exclusion

Memory-Mapped Files

Memory-mapped files allow a file to be mapped directly into a process's address space, allowing processes to share data through the filesystem.

  • Advantages: Persistence, can handle very large data sets efficiently
  • Limitations: Requires file I/O, need for synchronization
  • Use cases: Databases, caches, large data processing
IPC Mechanism Speed Complexity Persistence Network Support
Shared Memory Very Fast Medium
Pipes Fast Low
Message Queues Medium Medium
Sockets Medium High
Memory-Mapped Files Fast Medium

Interactive IPC Simulations

Experience how IPC mechanisms work through these interactive simulations:

Shared Memory Simulation

Explore how processes communicate directly through a common memory region.

  • Write data from Process A
  • Read data from Process B
  • Visualize the shared memory region
Launch Simulation

Message Passing Simulation

See how the kernel mediates messages between isolated processes.

  • Send messages between processes
  • Observe how the kernel manages communication
  • Compare with shared memory approach
Launch Simulation
Learning tip: Try both simulations to understand the key differences between shared memory and message passing approaches to IPC. Note how shared memory provides direct access without kernel mediation, while message passing relies on the kernel as an intermediary.

IPC Implementation Examples

Shared Memory Example

This example demonstrates how to create and use a shared memory segment between two processes.

Shared Memory Example
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>

#define SHM_SIZE 1024  // Size of shared memory segment

int main() {
    key_t key;
    int shmid;
    char *shm_ptr, *s;
    
    // Generate a unique key for shared memory
    if ((key = ftok(".", 'a')) == -1) {
        perror("ftok");
        exit(1);
    }
    
    // Create the shared memory segment
    if ((shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0666)) == -1) {
        perror("shmget");
        exit(1);
    }
    
    printf("Shared memory segment created: %d\n", shmid);
    
    // Attach the shared memory segment
    if ((shm_ptr = shmat(shmid, NULL, 0)) == (char *) -1) {
        perror("shmat");
        exit(1);
    }
    
    printf("Shared memory attached at address: %p\n", shm_ptr);
    
    // Fork a child process
    pid_t pid = fork();
    
    if (pid == -1) {
        perror("fork");
        exit(1);
    }
    
    if (pid == 0) {
        // Child process: read from shared memory
        printf("Child process waiting for data...\n");
        sleep(2);  // Wait for parent to write data
        
        printf("Child process reading: %s\n", shm_ptr);
        
        // Detach from shared memory
        if (shmdt(shm_ptr) == -1) {
            perror("shmdt");
            exit(1);
        }
        
        exit(0);
    } else {
        // Parent process: write to shared memory
        strcpy(shm_ptr, "Hello from the parent process!");
        printf("Parent wrote to shared memory\n");
        
        // Wait for child to finish
        wait(NULL);
        
        // Detach from shared memory
        if (shmdt(shm_ptr) == -1) {
            perror("shmdt");
            exit(1);
        }
        
        // Remove the shared memory segment
        if (shmctl(shmid, IPC_RMID, NULL) == -1) {
            perror("shmctl");
            exit(1);
        }
        
        printf("Shared memory removed\n");
    }
    
    return 0;
}

Pipe Example

This example shows how to use a pipe to send data from a parent process to a child process.

Pipe Example
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

int main() {
    int pipefd[2];  // File descriptors for pipe
    pid_t pid;
    char buffer[100];
    const char *message = "Hello from parent via pipe!";
    
    // Create the pipe
    if (pipe(pipefd) == -1) {
        perror("pipe");
        exit(EXIT_FAILURE);
    }
    
    // Create a child process
    pid = fork();
    
    if (pid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    }
    
    if (pid == 0) {
        // Child process
        
        // Close the write end as we only need to read
        close(pipefd[1]);
        
        // Read from pipe
        int bytes_read = read(pipefd[0], buffer, sizeof(buffer));
        
        if (bytes_read > 0) {
            buffer[bytes_read] = '\0';  // Null-terminate the string
            printf("Child received: %s\n", buffer);
        } else {
            perror("read");
        }
        
        // Close the read end
        close(pipefd[0]);
        
        exit(EXIT_SUCCESS);
    } else {
        // Parent process
        
        // Close the read end as we only need to write
        close(pipefd[0]);
        
        // Write to pipe
        if (write(pipefd[1], message, strlen(message)) == -1) {
            perror("write");
        }
        
        printf("Parent sent: %s\n", message);
        
        // Close the write end
        close(pipefd[1]);
        
        // Wait for child to finish
        wait(NULL);
    }
    
    return 0;
}

Message Queue Example

This example demonstrates how to create and use a message queue for IPC.

Message Queue Example
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <unistd.h>

// Define message structure
struct message {
    long msg_type;
    char msg_text[100];
};

int main() {
    key_t key;
    int msgid;
    struct message msg;
    
    // Generate a unique key for message queue
    if ((key = ftok(".", 'b')) == -1) {
        perror("ftok");
        exit(1);
    }
    
    // Create the message queue
    if ((msgid = msgget(key, IPC_CREAT | 0666)) == -1) {
        perror("msgget");
        exit(1);
    }
    
    printf("Message queue created: %d\n", msgid);
    
    pid_t pid = fork();
    
    if (pid == -1) {
        perror("fork");
        exit(1);
    }
    
    if (pid == 0) {
        // Child process: receive message
        printf("Child waiting for message...\n");
        
        // Receive message
        if (msgrcv(msgid, &msg, sizeof(msg.msg_text), 1, 0) == -1) {
            perror("msgrcv");
            exit(1);
        }
        
        printf("Child received: %s\n", msg.msg_text);
        
        // Send a response back to parent
        msg.msg_type = 2;  // Different message type for parent
        strcpy(msg.msg_text, "Hello from the child process!");
        
        if (msgsnd(msgid, &msg, strlen(msg.msg_text) + 1, 0) == -1) {
            perror("msgsnd");
            exit(1);
        }
        
        exit(0);
    } else {
        // Parent process: send message
        msg.msg_type = 1;  // Message type for child
        strcpy(msg.msg_text, "Hello from the parent process!");
        
        // Send the message
        if (msgsnd(msgid, &msg, strlen(msg.msg_text) + 1, 0) == -1) {
            perror("msgsnd");
            exit(1);
        }
        
        printf("Parent sent message\n");
        
        // Receive response from child
        if (msgrcv(msgid, &msg, sizeof(msg.msg_text), 2, 0) == -1) {
            perror("msgrcv");
            exit(1);
        }
        
        printf("Parent received: %s\n", msg.msg_text);
        
        // Wait for child to finish
        wait(NULL);
        
        // Remove the message queue
        if (msgctl(msgid, IPC_RMID, NULL) == -1) {
            perror("msgctl");
            exit(1);
        }
        
        printf("Message queue removed\n");
    }
    
    return 0;
}

Socket Example (Unix Domain)

This example demonstrates local IPC using Unix domain sockets.

Unix Domain Socket Example
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>

#define SOCKET_PATH "/tmp/ipc_socket"
#define BUFFER_SIZE 100

int main() {
    pid_t pid;
    
    // Create child process
    pid = fork();
    
    if (pid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    }
    
    if (pid == 0) {
        // Child process: client
        sleep(1);  // Wait for server to set up
        
        int client_sock;
        struct sockaddr_un server_addr;
        char buffer[BUFFER_SIZE];
        
        // Create socket
        if ((client_sock = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {
            perror("socket");
            exit(EXIT_FAILURE);
        }
        
        memset(&server_addr, 0, sizeof(server_addr));
        server_addr.sun_family = AF_UNIX;
        strncpy(server_addr.sun_path, SOCKET_PATH, sizeof(server_addr.sun_path) - 1);
        
        // Connect to server
        if (connect(client_sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
            perror("connect");
            exit(EXIT_FAILURE);
        }
        
        printf("Client connected to server\n");
        
        // Send message to server
        const char *message = "Hello from the client!";
        if (send(client_sock, message, strlen(message), 0) == -1) {
            perror("send");
            exit(EXIT_FAILURE);
        }
        
        // Receive response
        int bytes_received = recv(client_sock, buffer, BUFFER_SIZE - 1, 0);
        if (bytes_received == -1) {
            perror("recv");
            exit(EXIT_FAILURE);
        }
        
        buffer[bytes_received] = '\0';
        printf("Client received: %s\n", buffer);
        
        // Close connection
        close(client_sock);
        exit(EXIT_SUCCESS);
    } else {
        // Parent process: server
        int server_sock, client_sock;
        struct sockaddr_un server_addr, client_addr;
        socklen_t client_len;
        char buffer[BUFFER_SIZE];
        
        // Create socket
        if ((server_sock = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {
            perror("socket");
            exit(EXIT_FAILURE);
        }
        
        // Remove socket file if it already exists
        unlink(SOCKET_PATH);
        
        // Setup address structure
        memset(&server_addr, 0, sizeof(server_addr));
        server_addr.sun_family = AF_UNIX;
        strncpy(server_addr.sun_path, SOCKET_PATH, sizeof(server_addr.sun_path) - 1);
        
        // Bind socket
        if (bind(server_sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
            perror("bind");
            exit(EXIT_FAILURE);
        }
        
        // Listen for connections
        if (listen(server_sock, 5) == -1) {
            perror("listen");
            exit(EXIT_FAILURE);
        }
        
        printf("Server waiting for connection...\n");
        
        // Accept connection
        client_len = sizeof(client_addr);
        if ((client_sock = accept(server_sock, (struct sockaddr*)&client_addr, &client_len)) == -1) {
            perror("accept");
            exit(EXIT_FAILURE);
        }
        
        printf("Server accepted connection\n");
        
        // Receive message
        int bytes_received = recv(client_sock, buffer, BUFFER_SIZE - 1, 0);
        if (bytes_received == -1) {
            perror("recv");
            exit(EXIT_FAILURE);
        }
        
        buffer[bytes_received] = '\0';
        printf("Server received: %s\n", buffer);
        
        // Send response
        const char *response = "Hello from the server!";
        if (send(client_sock, response, strlen(response), 0) == -1) {
            perror("send");
            exit(EXIT_FAILURE);
        }
        
        // Clean up
        close(client_sock);
        close(server_sock);
        unlink(SOCKET_PATH);
        
        // Wait for child to finish
        wait(NULL);
    }
    
    return 0;
}

Best Practices and Considerations

Choosing the Right IPC Mechanism

Selecting the appropriate IPC mechanism depends on several factors:

Common Challenges with IPC:
  • Race Conditions: When multiple processes access shared resources without proper synchronization
  • Deadlocks: Processes waiting for each other indefinitely
  • Resource Management: Ensuring proper cleanup of IPC resources
  • Error Handling: Dealing with various failure scenarios

Synchronization Issues

When using IPC mechanisms that involve shared resources (especially shared memory), proper synchronization is essential to prevent race conditions. Common synchronization primitives include:

Simple Mutex Example with Shared Memory
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>

// Structure to be placed in shared memory
struct shared_data {
    pthread_mutex_t mutex;  // Mutex for synchronization
    int counter;            // Shared counter
};

int main() {
    int shm_fd;
    struct shared_data *shared;
    const char *shm_name = "/my_shared_memory";
    
    // Open shared memory object
    shm_fd = shm_open(shm_name, O_CREAT | O_RDWR, 0666);
    if (shm_fd == -1) {
        perror("shm_open");
        exit(1);
    }
    
    // Set the size of the shared memory object
    ftruncate(shm_fd, sizeof(struct shared_data));
    
    // Map shared memory
    shared = mmap(NULL, sizeof(struct shared_data), PROT_READ | PROT_WRITE, 
                  MAP_SHARED, shm_fd, 0);
    if (shared == MAP_FAILED) {
        perror("mmap");
        exit(1);
    }
    
    // Initialize mutex with shared attribute
    pthread_mutexattr_t mutex_attr;
    pthread_mutexattr_init(&mutex_attr);
    pthread_mutexattr_setpshared(&mutex_attr, PTHREAD_PROCESS_SHARED);
    pthread_mutex_init(&shared->mutex, &mutex_attr);
    
    // Initialize counter
    shared->counter = 0;
    
    pid_t pid = fork();
    
    if (pid == -1) {
        perror("fork");
        exit(1);
    }
    
    int i;
    const int iterations = 5;
    
    if (pid == 0) {
        // Child process
        for (i = 0; i < iterations; i++) {
            // Lock mutex
            pthread_mutex_lock(&shared->mutex);
            
            // Critical section
            shared->counter++;
            printf("Child: Incremented counter to %d\n", shared->counter);
            
            // Unlock mutex
            pthread_mutex_unlock(&shared->mutex);
            
            sleep(1);  // Sleep to make the interleaving visible
        }
    } else {
        // Parent process
        for (i = 0; i < iterations; i++) {
            // Lock mutex
            pthread_mutex_lock(&shared->mutex);
            
            // Critical section
            shared->counter += 10;
            printf("Parent: Incremented counter to %d\n", shared->counter);
            
            // Unlock mutex
            pthread_mutex_unlock(&shared->mutex);
            
            sleep(1);  // Sleep to make the interleaving visible
        }
        
        // Wait for child
        wait(NULL);
        
        // Clean up
        pthread_mutex_destroy(&shared->mutex);
        pthread_mutexattr_destroy(&mutex_attr);
        
        // Unmap and close
        munmap(shared, sizeof(struct shared_data));
        close(shm_fd);
        shm_unlink(shm_name);
    }
    
    return 0;
}

Real-World Applications of IPC

IPC mechanisms are foundational to many modern computing paradigms and applications:

Client-Server Applications

Most client-server applications rely on IPC mechanisms, especially sockets, to facilitate communication between client and server processes.

Microservices Architecture

Microservices often communicate via HTTP/REST APIs, message queues, or gRPC, all of which are implementations of IPC principles.

Database Systems

Database engines use various IPC mechanisms to handle connections from multiple clients and coordinate internal processes.

Web Browsers

Modern browsers use process isolation (with IPC for communication) to enhance security and stability.

Operating System Services

Within an OS, many system services communicate with each other and with user applications through IPC channels.

Beyond Local IPC: While this tutorial focuses on local IPC within a single computer, many of the same principles apply to distributed computing, where processes communicate across network boundaries. Technologies like RPC (Remote Procedure Call), message brokers (RabbitMQ, Kafka), and REST APIs extend IPC concepts to distributed systems.

IPC Quiz

Which IPC mechanism typically provides the fastest data transfer for large amounts of data?
A. Pipes
B. Shared Memory
C. Message Queues
D. Sockets