Chapter 6: Interprocess Communication

6.1 Introduction: The Necessity of Collaboration

In the preceding chapters, we delved deep into the core concepts of process management, memory management, and file systems. We explored how the operating system meticulously creates and isolates processes, each running within its own virtual address space, providing a robust security boundary and preventing accidental interference. This isolation is fundamental for system stability and multi-user environments. However, the real world often demands more than mere isolation; it requires collaboration. Modern applications are rarely monolithic entities; they are often composed of multiple cooperating processes or threads, each responsible for a specific task. Consider a web server that spawns child processes to handle incoming requests, or a complex scientific simulation where different processes compute parts of a larger problem. For these distributed or parallel tasks to function correctly, processes must be able to share information and synchronize their activities. This is precisely where Interprocess Communication (IPC) mechanisms come into play.

Interprocess Communication refers to the set of techniques and tools that an operating system provides to allow separate processes to communicate with each other. These mechanisms are the very fabric that enables complex, multi-component applications to operate cohesively. Without robust IPC, processes would remain isolated islands, severely limiting the capabilities and efficiency of an operating system and the applications built upon it.

6.1.1 Why IPC? The Challenges of Cooperation

The inherent isolation of processes, while beneficial for security and stability, presents several challenges when cooperation is required:

The operating system provides a diverse set of IPC mechanisms, each with its own trade-offs in terms of speed, complexity, flexibility, and suitability for different communication patterns. We will explore the most common and fundamental of these in detail, focusing on their underlying implementation and how they are utilized across various operating systems.

6.2 Shared Memory: The Fastest Lane

6.2.1 Core Concept and Working

Shared memory is arguably the fastest form of Interprocess Communication. Unlike other IPC mechanisms that involve the kernel mediating data transfer (e.g., copying data between kernel buffers and user-space), shared memory allows multiple processes to directly access the same region of physical memory. The operating system's role is primarily limited to setting up this shared region and mapping it into the virtual address spaces of the participating processes. Once set up, data transfer becomes a direct memory access operation, bypassing the kernel entirely for read/write operations.

The core idea is simple: a designated segment of RAM is made accessible to multiple processes. Each process maps this shared segment into its own virtual address space. Crucially, while the virtual addresses used by each process to access the shared segment might differ, they all resolve to the same underlying physical memory pages.

Diagram 6.1: Shared Memory Mechanism


+-----------------+                      +-----------------+
|   Process 1     |                      |   Process 2     |
| Virtual Addr Sp |                      | Virtual Addr Sp |
| +-------------+ |                      | +-------------+ |
| | VA: 0x1000  | |  <--- maps to --->   | | VA: 0x2000  | |
| +-------------+ |                      | +-------------+ |
+-------|---------+                      +-------|---------+
        |                                        |
        |       SHARED MEMORY REGION             |
        |       (Physical Memory)                |
        |                                        |
        +-------+================================+
                |                                |
                |   Physical Address Space (PA)  |
                |   e.g., PA: 0x8000             |
                |                                |
                +================================+
                

Illustrates two processes, P1 and P2, mapping the same physical memory region into their distinct virtual address spaces. Both P1's virtual address X and P2's virtual address Y map to the same physical address Z in the shared segment.

However, this speed comes with a significant caveat: shared memory offers no inherent synchronization. Processes accessing shared memory must explicitly coordinate their access using other IPC mechanisms like semaphores or mutexes to prevent race conditions and ensure data consistency. Without proper synchronization, writes from one process might overwrite data before another process has read it, leading to corruption or incorrect state.

6.2.2 Implementation Details

The implementation of shared memory relies heavily on the operating system's memory management unit (MMU) and virtual memory capabilities.

  1. Creation: A process requests the OS to create a shared memory segment of a specific size. The OS allocates physical memory pages for this segment.
  2. Attachment/Mapping: Participating processes then "attach" to this shared segment. This involves the OS adding entries to the process's page table that map a range of virtual addresses in the process's address space to the physical pages of the shared segment.
  3. Access: Once mapped, processes can read from and write to this memory region as if it were their own private memory, using standard load/store instructions.
  4. Detachment: When a process no longer needs the shared segment, it detaches from it, removing the mapping from its page table.
  5. Deletion: The shared segment typically persists until all processes have detached from it or until explicitly marked for deletion (e.g., by the creating process or when the system reboots).

6.2.3 Shared Memory in Different Operating Systems

Unix/Linux (System V IPC and POSIX IPC)

Unix-like systems offer two primary APIs for shared memory: System V IPC and POSIX IPC. POSIX IPC is generally preferred for new development due to its simpler API and better portability.

Code Snippet: POSIX Shared Memory Example (Linux)

// shm_writer.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h> // For mode constants
#include <fcntl.h>    // For O_CREAT, O_RDWR
#include <unistd.h>   // For ftruncate

const char* SHM_NAME = "/my_shared_memory";
const int SHM_SIZE = 1024;

int main() {
    int fd;
    void* ptr;

    // Create shared memory object
    fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0666);
    if (fd == -1) {
        perror("shm_open");
        exit(EXIT_FAILURE);
    }

    // Set size of shared memory object
    ftruncate(fd, SHM_SIZE);

    // Map shared memory object into process address space
    ptr = mmap(NULL, SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (ptr == MAP_FAILED) {
        perror("mmap");
        exit(EXIT_FAILURE);
    }

    printf("Writer: Writing to shared memory...\n");
    sprintf((char*)ptr, "Hello from shared memory!");

    printf("Writer: Waiting for reader (press Enter to detach)...\n");
    getchar();

    // Unmap and close
    munmap(ptr, SHM_SIZE);
    close(fd);
    shm_unlink(SHM_NAME); // Remove shared memory object

    printf("Writer: Exiting.\n");
    return 0;
}
// shm_reader.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

const char* SHM_NAME = "/my_shared_memory";
const int SHM_SIZE = 1024;

int main() {
    int fd;
    void* ptr;

    // Open shared memory object (must exist)
    fd = shm_open(SHM_NAME, O_RDONLY, 0666);
    if (fd == -1) {
        perror("shm_open");
        exit(EXIT_FAILURE);
    }

    // Map shared memory object into process address space
    ptr = mmap(NULL, SHM_SIZE, PROT_READ, MAP_SHARED, fd, 0);
    if (ptr == MAP_FAILED) {
        perror("mmap");
        exit(EXIT_FAILURE);
    }

    printf("Reader: Reading from shared memory: '%s'\n", (char*)ptr);

    // Unmap and close
    munmap(ptr, SHM_SIZE);
    close(fd);

    printf("Reader: Exiting.\n");
    return 0;
}

To compile and run:

gcc shm_writer.c -o shm_writer -lrt
gcc shm_reader.c -o shm_reader -lrt
./shm_writer   // In one terminal
./shm_reader   // In another terminal, then press Enter in writer's terminal
                

Windows

Windows provides shared memory through "file mapping objects" (also known as memory-mapped files), which can be backed by the paging file or a physical file. This is the primary and most robust way to achieve shared memory IPC on Windows.

FreeRTOS (Real-time Operating System)

FreeRTOS is a real-time operating system primarily designed for embedded systems. Its "processes" are typically referred to as "tasks," and they share a single address space (or utilize memory protection units (MPU) for limited isolation). Therefore, explicit "shared memory" IPC in the Unix/Windows sense (mapping distinct virtual address spaces to common physical memory) is not a common pattern. All tasks inherently share the global memory.

Instead, FreeRTOS tasks achieve data sharing and coordination through:

The concept of shared memory in FreeRTOS is more about managing concurrent access to globally accessible data than about mapping separate address spaces.

6.3 Message Queues: Structured Communication

6.3.1 Core Concept and Working

Message queues provide an indirect, asynchronous method of communication between processes. Instead of direct memory access, processes communicate by sending and receiving messages via a queue managed by the operating system kernel. A sender process places a message onto the queue, and a receiver process retrieves messages from it. This mechanism decouples the sender and receiver, meaning they don't need to be running simultaneously or coordinate their timing as tightly as with shared memory.

Each message typically has a type (or priority) and a data payload. Messages are stored in the queue in the order they are sent (or by priority), and receivers can often choose to retrieve messages of a specific type or simply the next available message. The kernel handles the buffering, synchronization (e.g., blocking send/receive operations), and delivery of messages.

Diagram 6.2: Message Queue Mechanism


+------------+       +----------------------------+       +------------+
|  Process A | ----> |        Kernel-Managed      | ----> |  Process B |
|  (Sender)  |  Msg M1|       Message Queue        | Msg M1| (Receiver) |
|            | ----> |                            | ----> |            |
|  send(M1)  |  Msg M2|  [ M1 ] <- M2 <- [ M3 ]     | Msg M2| receive()  |
|  send(M2)  |        |  (FIFO Buffer)             |       | receive()  |
+------------+       +----------------------------+       +------------+
                       ^          ^          ^
                       |          |          |
                       |  Kernel Buffer     |
                       | (Messages copied)  |
                

Shows Process A sending messages to a kernel-managed Message Queue, and Process B receiving messages from it. Messages are buffered within the kernel.

**Advantages:**

**Disadvantages:**

6.3.2 Implementation Details

The operating system maintains a data structure for each message queue, which includes:

When a message is sent, the kernel allocates memory for it within its own space, copies the data from the sender's buffer, and adds it to the queue. When received, the kernel copies the data to the receiver's buffer and deallocates the message's kernel memory.

6.3.3 Message Queues in Different Operating Systems

Unix/Linux (System V IPC and POSIX IPC)

Similar to shared memory, Unix-like systems provide both System V and POSIX message queue APIs.

Windows

Windows does not have a direct kernel-level analogue to POSIX message queues in its general IPC mechanisms like Unix. However, it offers several ways to achieve message-passing paradigms:

FreeRTOS (Real-time Operating System)

Message queues are a cornerstone of inter-task communication in FreeRTOS. They are a fundamental and highly efficient mechanism for passing data between tasks.

Code Snippet: FreeRTOS Message Queue Example

#include <FreeRTOS.h>
#include <task.h>
#include <queue.h>
#include <stdio.h> // For printf, usually redirected to UART

QueueHandle_t xMessageQueue;

// Sender Task
void vSenderTask(void* pvParameters) {
    int messageCounter = 0;
    char txBuffer[50];
    for (;;) {
        sprintf(txBuffer, "Hello from Sender! Msg #%d", messageCounter++);
        // Send message to queue, block for 100 ticks if queue is full
        if (xQueueSend(xMessageQueue, (void*)&txBuffer, (TickType_t)100) != pdPASS) {
            printf("Sender: Could not send message, queue full.\n");
        } else {
            printf("Sender: Sent '%s'\n", txBuffer);
        }
        vTaskDelay(pdMS_TO_TICKS(500)); // Send every 500ms
    }
}

// Receiver Task
void vReceiverTask(void* pvParameters) {
    char rxBuffer[50];
    for (;;) {
        // Receive message from queue, block indefinitely if queue is empty
        if (xQueueReceive(xMessageQueue, (void*)&rxBuffer, portMAX_DELAY) == pdPASS) {
            printf("Receiver: Received '%s'\n", rxBuffer);
        } else {
            printf("Receiver: No message received (should not happen with portMAX_DELAY).\n");
        }
    }
}

// Main function (simulated environment)
int main(void) {
    // Create a queue that can hold 5 messages, each max 50 bytes long
    xMessageQueue = xQueueCreate(5, sizeof(char[50]));

    if (xMessageQueue != NULL) {
        // Create tasks
        xTaskCreate(vSenderTask, "Sender", configMINIMAL_STACK_SIZE, NULL, 1, NULL);
        xTaskCreate(vReceiverTask, "Receiver", configMINIMAL_STACK_SIZE, NULL, 2, NULL);
        
        // Start the scheduler
        vTaskStartScheduler();
    } else {
        printf("Failed to create queue.\n");
    }

    // Should never reach here if scheduler starts successfully
    return 0;
}

6.4 Pipes: Sequential Byte Streams

6.4.1 Core Concept and Working

Pipes are one of the oldest and simplest forms of IPC in Unix-like systems, enabling unidirectional byte-stream communication between processes. They operate on a producer-consumer model, where data written to one end of the pipe is read from the other end. The operating system manages an internal buffer for the pipe.

There are two main types of pipes:

  1. Unnamed Pipes (Anonymous Pipes): These are temporary, created for specific parent-child or sibling process communication. They exist only as long as the processes using them are alive and are typically passed from a parent to its child process after a fork() call. They have no name in the file system.
  2. Named Pipes (FIFOs - First-In, First-Out): These have a name in the file system (appearing as a special file type). They can be used by any two unrelated processes, provided they know the FIFO's name. They persist until explicitly deleted or the system reboots.

Both types of pipes offer a byte stream interface, meaning there's no inherent message boundary; data is read as a continuous sequence of bytes, similar to reading from a file.

Diagram 6.3: Unnamed Pipe between Parent and Child


+-----------------+      +-----------------+
|     Parent      |      |      Child      |
|     Process     |      |     Process     |
+-----------------+      +-----------------+
        |                        |
        | pipefd[1] (Write End)  | pipefd[0] (Read End)
        |  (Parent keeps)        |  (Child keeps)
        V                        ^
        +------------------------+
        |  Kernel Pipe Buffer    |
        |  (Unidirectional FIFO) |
        +------------------------+
             |       ^
             | Data  | Data
             | Wrote | Read
             V       |
          (Parent)   (Child)

Parent closes pipefd[0] (its unused read end)
Child closes pipefd[1] (its unused write end)
                

Illustrates a parent process creating a pipe, then forking a child. The parent typically closes the read end and writes to the write end, while the child closes the write end and reads from the read end.

Diagram 6.4: Named Pipe (FIFO) between Unrelated Processes


+-----------------+                     +-----------------+
|   Process A     |                     |   Process B     |
+-----------------+                     +-----------------+
        |                                       |
        | open("/tmp/my_fifo", O_WRONLY)        | open("/tmp/my_fifo", O_RDONLY)
        | write()                               | read()
        V                                       ^
+-------------------------------------------------+
|         Named Pipe (FIFO) File                |
|         /tmp/my_fifo (Special File Type)      |
+-------------------------------------------------+
        |                                       |
        |          Kernel Pipe Buffer           |
        |          (Unidirectional FIFO)        |
        |                                       |
        +---------------------------------------+
                

Shows two unrelated processes communicating via a named pipe, which is represented as a special file in the file system.

Advantages:

Disadvantages:

6.4.2 Implementation Details

Pipes are implemented by the kernel using a circular buffer. When a pipe is created, the kernel allocates a fixed-size buffer (e.g., 64KB on Linux).

6.4.3 Pipes in Different Operating Systems

Unix/Linux

Code Snippet: Unnamed Pipe Example (Unix/Linux)

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h> // For pipe(), fork(), read(), write(), close()
#include <string.h>

int main() {
    int pipefd[2]; // pipefd[0] for read, pipefd[1] for write
    pid_t pid;
    char buffer[50];
    const char* message = "Hello from parent process!";

    // Create the pipe
    if (pipe(pipefd) == -1) {
        perror("pipe");
        exit(EXIT_FAILURE);
    }

    // Fork a child process
    pid = fork();

    if (pid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    }

    if (pid == 0) { // Child process
        close(pipefd[1]); // Close unused write end
        printf("Child: Waiting for message...\n");
        ssize_t bytes_read = read(pipefd[0], buffer, sizeof(buffer) - 1);
        if (bytes_read > 0) {
            buffer[bytes_read] = '\0'; // Null-terminate the string
            printf("Child: Received message: '%s'\n", buffer);
        } else {
            perror("read");
        }
        close(pipefd[0]); // Close read end
        exit(EXIT_SUCCESS);
    } else { // Parent process
        close(pipefd[0]); // Close unused read end
        printf("Parent: Sending message: '%s'\n", message);
        write(pipefd[1], message, strlen(message));
        close(pipefd[1]); // Close write end, signals EOF to reader
        wait(NULL); // Wait for child to finish
        printf("Parent: Child finished, exiting.\n");
        exit(EXIT_SUCCESS);
    }
}

Windows

Windows provides analogous functionality:

FreeRTOS (Real-time Operating System)

FreeRTOS does not have a direct equivalent of Unix-like pipes (named or unnamed) as a core IPC mechanism. The concept of "pipes" typically implies a kernel-managed, buffered byte stream for processes that might not share memory.

6.5 Semaphores and Mutexes: The Synchronizers

6.5.1 Core Concept and Working

While not directly used for data transfer, semaphores and mutexes are crucial IPC mechanisms for synchronization. They are fundamental for coordinating access to shared resources (like shared memory segments, files, or critical sections of code) among multiple processes or threads, preventing race conditions and ensuring data consistency.

Semaphores

A semaphore is a signaling mechanism. It's an integer variable that is accessed only through two atomic operations:

There are two main types:

Diagram 6.5: Processes using a Semaphore for Shared Resource Access


             +-----------------+   +-----------------+
             |     Process 1   |   |     Process 2   |
             +-----------------+   +-----------------+
                     |                     |
                     | sem_wait()          | sem_wait()
                     | (Acquire Lock)      | (Acquire Lock)
                     V                     V
        +-------------------------------------+
        |  Binary Semaphore (Initial value: 1)|
        |  'sem_sync'                         |
        +-------------------------------------+
                     |
         [Only one process can pass at a time]
                     |
                     V
        +-------------------------------------+
        |         CRITICAL SECTION            |
        |     (Accessing Shared Resource)     |
        +-------------------------------------+
                     |
                     | sem_post()
                     | (Release Lock)
                     V
                 (Continue Execution)
                

Illustrates two processes attempting to enter a critical section to access a shared resource. A semaphore (or mutex) controls access, ensuring only one process is in the critical section at a time.

Mutexes (Mutual Exclusion Locks)

A mutex is a synchronization primitive that grants exclusive access to a resource. It's essentially a binary semaphore with a specific ownership concept: only the process that locked the mutex can unlock it. Mutexes are primarily used to protect critical sections of code, ensuring that only one thread or process executes that section at any given time.

Mutexes are often preferred over binary semaphores for mutual exclusion due to additional features they may offer, such as priority inheritance (to prevent priority inversion) and error checking.

6.5.2 Implementation Details

Semaphores and mutexes rely on atomic operations (e.g., test-and-set, compare-and-swap) at the hardware level to ensure that their core increment/decrement or lock/unlock operations are indivisible and free from race conditions. The OS kernel manages the semaphore/mutex state and the queues of processes/threads waiting for them.

6.5.3 Semaphores and Mutexes in Different Operating Systems

Unix/Linux (System V IPC and POSIX IPC)

Unix-like systems offer both System V and POSIX APIs for semaphores. POSIX semaphores are generally preferred. Mutexes are primarily a POSIX Threads (Pthreads) concept, but can be used for process synchronization if they are allocated in shared memory.

Code Snippet: POSIX Semaphore Example (Linux) for Shared Memory Sync

// shared_memory_sync.c (Combines shared memory and semaphore)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h_>
#include <semaphore.h> // For POSIX semaphores

const char* SHM_NAME = "/my_shm_sync";
const char* SEM_NAME = "/my_sem_sync";
const int SHM_SIZE = 1024;

int main(int argc, char* argv[]) {
    int fd;
    char* shm_ptr;
    sem_t* sem;
    int is_creator = (argc > 1 && strcmp(argv[1], "creator") == 0);

    // Open/create shared memory
    fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0666);
    if (fd == -1) { perror("shm_open"); exit(EXIT_FAILURE); }
    ftruncate(fd, SHM_SIZE);
    shm_ptr = mmap(NULL, SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    close(fd); // FD no longer needed after mmap
    if (shm_ptr == MAP_FAILED) { perror("mmap"); exit(EXIT_FAILURE); }

    // Open/create semaphore
    sem = sem_open(SEM_NAME, O_CREAT, 0666, 1); // Initial value 1 (unlocked)
    if (sem == SEM_FAILED) { perror("sem_open"); exit(EXIT_FAILURE); }

    if (is_creator) {
        printf("Creator: Waiting to write to shared memory...\n");
        sem_wait(sem); // Acquire lock
        sprintf(shm_ptr, "Data from creator process.");
        printf("Creator: Wrote '%s' to shared memory.\n", shm_ptr);
        sem_post(sem); // Release lock
        printf("Creator: Press Enter to clean up...\n");
        getchar();
    } else {
        printf("Reader: Waiting to read from shared memory...\n");
        sem_wait(sem); // Acquire lock
        printf("Reader: Read from shared memory: '%s'\n", shm_ptr);
        sem_post(sem); // Release lock
    }

    // Cleanup
    munmap(shm_ptr, SHM_SIZE);
    sem_close(sem);
    if (is_creator) {
        shm_unlink(SHM_NAME);
        sem_unlink(SEM_NAME);
    }
    return 0;
}

Compile and run:

gcc shared_memory_sync.c -o shm_sync -lrt -pthread
./shm_sync creator   // In one terminal
./shm_sync           // In another terminal, then press Enter in creator's terminal
                

Windows

Windows provides specific kernel objects for synchronization that can be shared between processes.

FreeRTOS (Real-time Operating System)

Synchronization is absolutely critical in FreeRTOS for managing shared resources between tasks. FreeRTOS provides efficient and lightweight semaphore and mutex implementations.

6.6 Sockets: Network-Transparent Communication

6.6.1 Core Concept and Working

Sockets provide a powerful and flexible mechanism for Interprocess Communication, particularly when processes need to communicate across network boundaries (i.e., on different machines) or when a client-server architecture is desired, even on the same machine. A socket acts as an endpoint for communication, typically identified by an IP address and a port number for network communication, or a file system path for local communication.

The communication model is fundamentally client-server: a server process creates a socket, binds it to a specific address/port, and listens for incoming connections. Client processes create their own sockets and attempt to connect to the server's address/port. Once a connection is established, data can flow bidirectionally between the client and server sockets.

Sockets abstract the underlying network protocols, allowing applications to communicate over TCP (stream sockets for reliable, ordered byte streams) or UDP (datagram sockets for unreliable, unordered message transfer).

Diagram 6.6: Client-Server Communication using Sockets


+-------------------+           Network/IPC           +-------------------+
|     Client        |          (via Sockets)          |      Server       |
|     Process       |                                 |      Process      |
+-------------------+                                 +-------------------+
        |                                                     |
        | socket()                                            | socket()
        | connect(Server_IP:Port)                             | bind(Server_IP:Port)
        |                                                     | listen()
        |                                                     |
        |------------------- Connection Established ----------|
        | send() <-------------------------------------------> recv()
        | recv() <-------------------------------------------> send()
        |                                                     |
        +-------------------+                                 +-------------------+
                

Shows a Client Process establishing a connection with a Server Process via sockets. This can be local (Unix domain sockets) or across a network (Internet sockets).

6.6.2 Implementation Details

Socket implementation involves significant kernel support for network stack protocols (TCP/IP, UDP) and managing socket states (e.g., listening, connected).

6.6.3 Sockets in Different Operating Systems

Unix/Linux

The Berkeley Sockets API is the standard for network programming and IPC using sockets.

Code Snippet: Basic Unix Domain Socket Example (Linux)

// uds_server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h> // For sockaddr_un

const char* SOCKET_PATH = "/tmp/my_uds_socket";

int main() {
    int server_fd, client_fd;
    struct sockaddr_un addr;
    char buffer[256];

    // Create socket
    server_fd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (server_fd == -1) { perror("socket"); exit(EXIT_FAILURE); }

    // Remove old socket file if it exists
    unlink(SOCKET_PATH);

    // Bind socket to a file path
    memset(&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, SOCKET_PATH, sizeof(addr.sun_path) - 1);
    
    if (bind(server_fd, (struct sockaddr*)&addr, sizeof(struct sockaddr_un)) == -1) {
        perror("bind");
        exit(EXIT_FAILURE);
    }

    // Listen for incoming connections
    if (listen(server_fd, 5) == -1) { perror("listen"); exit(EXIT_FAILURE); }
    printf("Server: Listening on %s\n", SOCKET_PATH);

    // Accept a connection
    client_fd = accept(server_fd, NULL, NULL);
    if (client_fd == -1) { perror("accept"); exit(EXIT_FAILURE); }
    printf("Server: Client connected.\n");

    // Read from client
    ssize_t bytes_read = read(client_fd, buffer, sizeof(buffer) - 1);
    if (bytes_read > 0) {
        buffer[bytes_read] = '\0';
        printf("Server: Received '%s'\n", buffer);
        write(client_fd, "Server got your message!", 24);
    }

    close(client_fd);
    close(server_fd);
    unlink(SOCKET_PATH);
    return 0;
}
// uds_client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>

const char* SOCKET_PATH = "/tmp/my_uds_socket";

int main() {
    int client_fd;
    struct sockaddr_un addr;
    char buffer[256];
    const char* message = "Hello from client!";

    // Create socket
    client_fd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (client_fd == -1) { perror("socket"); exit(EXIT_FAILURE); }

    // Connect to server socket
    memset(&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, SOCKET_PATH, sizeof(addr.sun_path) - 1);

    if (connect(client_fd, (struct sockaddr*)&addr, sizeof(struct sockaddr_un)) == -1) {
        perror("connect");
        exit(EXIT_FAILURE);
    }
    printf("Client: Connected to server.\n");

    // Write to server
    write(client_fd, message, strlen(message));
    printf("Client: Sent '%s'\n", message);

    // Read response from server
    ssize_t bytes_read = read(client_fd, buffer, sizeof(buffer) - 1);
    if (bytes_read > 0) {
        buffer[bytes_read] = '\0';
        printf("Client: Received response: '%s'\n", buffer);
    }

    close(client_fd);
    return 0;
}

Compile and run:

gcc uds_server.c -o uds_server
gcc uds_client.c -o uds_client
./uds_server   // In one terminal
./uds_client   // In another terminal
                

Windows

Windows implements sockets through the Winsock API (Windows Sockets), which is largely compatible with the Berkeley Sockets API, making porting network applications relatively straightforward.

FreeRTOS (Real-time Operating System)

FreeRTOS itself does not natively provide a full-fledged network stack or socket API as part of its kernel. However, it is common to integrate lightweight TCP/IP stacks, such as FreeRTOS+TCP or LwIP.

6.7 Signals: Asynchronous Event Notification

6.7.1 Core Concept and Working

Signals are a lightweight mechanism for notifying a process of an event. Unlike the other IPC methods discussed, signals are not designed for transferring data. Their primary purpose is asynchronous notification and handling of specific events, such as:

When a signal is sent to a process, the kernel interrupts the process's normal flow of execution (if it's running) and forces it to handle the signal. A process can either:

Signals are inherently unreliable for complex data transfer and should generally be used for simple event notification. The order of delivery is not guaranteed, and signal handlers must be carefully written to avoid re-entrancy issues and to be "async-signal-safe."

6.7.2 Implementation Details

Signals are implemented within the kernel. When a signal is generated (by another process, the kernel itself, or hardware), the kernel marks the target process as having a pending signal.

6.7.3 Signals in Different Operating Systems

Unix/Linux

Signals are a fundamental part of Unix/Linux.

Windows

Windows does not have a direct equivalent of Unix signals. It uses different mechanisms for event notification and error handling:

FreeRTOS (Real-time Operating System)

FreeRTOS tasks do not use "signals" in the Unix sense. Instead, tasks communicate events using:

6.8 Other Advanced and Higher-Level IPC Mechanisms

While the mechanisms discussed so far form the bedrock of IPC, modern operating systems and distributed computing environments often provide higher-level abstractions built upon these primitives.

6.9 Conclusion: Choosing the Right IPC Mechanism

Interprocess communication is a vital component of any robust operating system, enabling diverse processes to cooperate, share resources, and synchronize activities. We've explored a range of fundamental IPC mechanisms, from the raw speed of shared memory to the structured nature of message queues, the simplicity of pipes, the critical role of semaphores and mutexes for synchronization, and the network-transparent capabilities of sockets.

The choice of IPC mechanism depends heavily on the specific requirements of the application:

Real-world applications often combine multiple IPC mechanisms to achieve their goals. For instance, a system might use shared memory for large data blocks but employ a message queue or a semaphore to signal when the shared data is ready for consumption. Understanding the underlying implementation details and the trade-offs of each mechanism is crucial for designing efficient, reliable, and scalable multi-process applications.