Communication Facilities Shared Memory · Pipes · POSIX MQ

 

Chapter 43 – Communication Facilities
Data Transfer · Shared Memory · Pipes · POSIX MQ · mmap
43.2
Section
2
Main Types
File 2/3
of Chapter 43

Key Terms in This File
Data Transfer Byte Stream Message Queue Pipe FIFO Shared Memory mmap POSIX SHM Destructive Read User Space / Kernel Space

Communication: The Two Big Categories

All IPC communication mechanisms fall into two fundamental categories: data-transfer facilities and shared memory. The key question is: where does the data live while being transferred?

  • Data-transfer: Data moves through the kernel — one process writes to a kernel buffer, another reads from it.
  • Shared memory: Processes map the same physical RAM pages into their address spaces and access data directly — no kernel round-trip per read/write.

Data Transfer — How Data Moves Through the Kernel

Pipe / FIFO Data Flow

Process A
User Buffer
write()

copy to kernel

KERNEL SPACE
Pipe Buffer
[ data bytes ]

copy to user

Process B
User Buffer
read()
Two copies: User→Kernel on write, Kernel→User on read

Every data-transfer facility requires two memory copies: one from user space to kernel space during write(), and another from kernel space to user space during read(). This is a cost that shared memory avoids entirely.

Sub-Types of Data Transfer

Type Facilities Behaviour
Byte Stream Pipe, FIFO, Stream Socket (TCP) No message boundaries. read() can get any number of bytes. Like reading a file byte-by-byte.
Message System V MQ, POSIX MQ, Datagram Socket (UDP) Message boundaries preserved. Each read() gets exactly one complete message written by sender.
Pseudoterminal PTY (pty/tty) Specialized; used in terminal emulators, SSH. See Chapter 64.
Key insight – Destructive Reads: When Process B reads data from a pipe or message queue, that data is consumed. No other process can read it again. This is different from shared memory where all processes see the same data simultaneously.
Automatic Blocking: If Process B calls read() on an empty pipe, it blocks automatically until Process A writes something. No polling needed — the kernel handles this.

Coding Example 1 – Pipe (Byte Stream) Between Parent and Child

/*
 * pipe_bytestream.c
 * Classic pipe example: parent writes a string,
 * child reads it. Demonstrates byte stream semantics.
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main(void) {
    int pipefd[2];   /* pipefd[0] = read end, pipefd[1] = write end */
    pid_t child_pid;
    char write_msg[] = "Hello from parent via pipe!";
    char read_buf[128];
    ssize_t bytes_read;

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

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

    if (child_pid == 0) {
        /* CHILD: reader */
        close(pipefd[1]);   /* Close unused write end */

        bytes_read = read(pipefd[0], read_buf, sizeof(read_buf) - 1);
        if (bytes_read == -1) {
            perror("read");
            exit(EXIT_FAILURE);
        }
        read_buf[bytes_read] = '\0';

        printf("[Child] Read %zd bytes: \"%s\"\n", bytes_read, read_buf);
        close(pipefd[0]);
        exit(EXIT_SUCCESS);

    } else {
        /* PARENT: writer */
        close(pipefd[0]);   /* Close unused read end */

        printf("[Parent] Writing: \"%s\"\n", write_msg);
        if (write(pipefd[1], write_msg, strlen(write_msg)) == -1) {
            perror("write");
            exit(EXIT_FAILURE);
        }
        close(pipefd[1]);   /* EOF signal to reader */

        wait(NULL);
        printf("[Parent] Done.\n");
    }

    return 0;
}
/*
 * Compile: gcc pipe_bytestream.c -o pipe_bytestream
 * Run:     ./pipe_bytestream
 * Output:
 *   [Parent] Writing: "Hello from parent via pipe!"
 *   [Child] Read 27 bytes: "Hello from parent via pipe!"
 *   [Parent] Done.
 *
 * NOTE: A pipe is a byte stream — if parent wrote in 2 chunks,
 * child's single read() might get both chunks merged.
 */

Coding Example 2 – POSIX Message Queue (Message Boundaries)

/*
 * posix_mq_demo.c
 * POSIX message queue: messages preserve boundaries.
 * Each mq_receive() gets exactly one message sent by mq_send().
 *
 * Compile: gcc posix_mq_demo.c -o posix_mq_demo -lrt
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <mqueue.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>

#define MQ_NAME   "/demo_mq"
#define MAX_MSG   256
#define MAX_MSGS  10

int main(void) {
    mqd_t mqd;
    struct mq_attr attr;
    char send_buf1[] = "Message ONE";
    char send_buf2[] = "Message TWO -- longer content here";
    char recv_buf[MAX_MSG + 1];
    unsigned int priority;
    ssize_t bytes;

    /* Configure queue attributes */
    attr.mq_flags   = 0;
    attr.mq_maxmsg  = MAX_MSGS;
    attr.mq_msgsize = MAX_MSG;
    attr.mq_curmsgs = 0;

    /* Open/create the queue */
    mqd = mq_open(MQ_NAME, O_CREAT | O_RDWR, 0644, &attr);
    if (mqd == (mqd_t)-1) {
        perror("mq_open");
        exit(EXIT_FAILURE);
    }

    /* Send two messages of DIFFERENT sizes */
    printf("[Sender] Sending message 1: \"%s\"\n", send_buf1);
    mq_send(mqd, send_buf1, strlen(send_buf1), 1 /* priority */);

    printf("[Sender] Sending message 2: \"%s\"\n", send_buf2);
    mq_send(mqd, send_buf2, strlen(send_buf2), 2 /* higher priority */);

    /* Receive — message boundaries are preserved!
     * mq_receive always returns exactly one message. */
    bytes = mq_receive(mqd, recv_buf, MAX_MSG, &priority);
    recv_buf[bytes] = '\0';
    printf("[Receiver] Got message (prio=%u, %zd bytes): \"%s\"\n",
           priority, bytes, recv_buf);

    bytes = mq_receive(mqd, recv_buf, MAX_MSG, &priority);
    recv_buf[bytes] = '\0';
    printf("[Receiver] Got message (prio=%u, %zd bytes): \"%s\"\n",
           priority, bytes, recv_buf);

    mq_close(mqd);
    mq_unlink(MQ_NAME);  /* Remove the queue from the system */

    return 0;
}
/*
 * Output (higher priority message delivered first):
 *   [Sender] Sending message 1: "Message ONE"
 *   [Sender] Sending message 2: "Message TWO -- longer content here"
 *   [Receiver] Got message (prio=2, 34 bytes): "Message TWO -- longer content here"
 *   [Receiver] Got message (prio=1, 11 bytes): "Message ONE"
 *
 * Key point: Each mq_receive() returns exactly one message.
 * Message boundaries are preserved — unlike byte-stream pipes.
 */

Shared Memory – Direct RAM Access Between Processes

How Shared Memory Works

Process A
Stack
Heap
Shared Region
mapped here

page table
PHYSICAL RAM
Shared Pages
same physical address
page table

Process B
Stack
Heap
Shared Region
mapped here
Both processes’ page tables point to the same physical RAM pages — no kernel copy needed

With shared memory, the kernel sets up page table entries in both Process A and Process B to point to the same physical RAM pages. Once that setup is done, reading and writing shared memory is as fast as reading/writing normal variables — there are no system calls per access.

Three Types of Shared Memory on Linux

System V Shared Memory

Older API. Uses shmget(), shmat(), shmdt(), shmctl(). Integer key-based. Limited portability.

POSIX Shared Memory

Newer API. Uses shm_open(), mmap(), shm_unlink(). Named objects under /dev/shm. Cleaner and preferred.

Memory-Mapped Files (mmap)

MAP_SHARED + a file or MAP_ANONYMOUS. Most flexible. Used for file I/O, IPC, and anonymous shared memory.

Speed vs. Safety trade-off: Shared memory is the fastest IPC because there are no kernel copies per access. But you must use a semaphore or mutex to synchronize access. If two processes write to shared memory at the same time without synchronization, you get a race condition and corrupted data.
Visibility: All data placed in shared memory is visible to all processes sharing it simultaneously. This is the opposite of data-transfer where a read() consumes the data.

Coding Example 3 – POSIX Shared Memory (shm_open + mmap)

/*
 * posix_shm_demo.c
 * Two cooperating processes share a struct via POSIX shared memory.
 * Run in two terminals: first with "write", then with "read".
 *
 * Compile: gcc posix_shm_demo.c -o posix_shm_demo -lrt
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>

#define SHM_NAME  "/demo_shared_mem"
#define SHM_SIZE  sizeof(struct shared_data)

struct shared_data {
    int   counter;
    char  message[64];
};

int main(int argc, char *argv[]) {
    if (argc < 2) {
        fprintf(stderr, "Usage: %s [write|read]\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    int shm_fd;
    struct shared_data *shm_ptr;

    if (strcmp(argv[1], "write") == 0) {
        /* WRITER: create shared memory object */
        shm_fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0644);
        if (shm_fd == -1) { perror("shm_open"); exit(EXIT_FAILURE); }

        /* Set the size */
        ftruncate(shm_fd, SHM_SIZE);

        /* Map into our address space */
        shm_ptr = mmap(NULL, SHM_SIZE,
                       PROT_READ | PROT_WRITE, MAP_SHARED,
                       shm_fd, 0);
        if (shm_ptr == MAP_FAILED) { perror("mmap"); exit(EXIT_FAILURE); }

        /* Write data directly — no kernel copy! */
        shm_ptr->counter = 42;
        strcpy(shm_ptr->message, "Hello from writer process!");

        printf("[Writer] Wrote counter=%d, message=\"%s\"\n",
               shm_ptr->counter, shm_ptr->message);

        munmap(shm_ptr, SHM_SIZE);
        close(shm_fd);

    } else if (strcmp(argv[1], "read") == 0) {
        /* READER: open existing shared memory object */
        shm_fd = shm_open(SHM_NAME, O_RDONLY, 0);
        if (shm_fd == -1) { perror("shm_open"); exit(EXIT_FAILURE); }

        shm_ptr = mmap(NULL, SHM_SIZE,
                       PROT_READ, MAP_SHARED,
                       shm_fd, 0);
        if (shm_ptr == MAP_FAILED) { perror("mmap"); exit(EXIT_FAILURE); }

        printf("[Reader] Read counter=%d, message=\"%s\"\n",
               shm_ptr->counter, shm_ptr->message);

        munmap(shm_ptr, SHM_SIZE);
        close(shm_fd);

        /* Clean up: remove shared memory object */
        shm_unlink(SHM_NAME);
    }

    return 0;
}
/*
 * Usage:
 *   Terminal 1: ./posix_shm_demo write
 *     [Writer] Wrote counter=42, message="Hello from writer process!"
 *
 *   Terminal 2: ./posix_shm_demo read
 *     [Reader] Read counter=42, message="Hello from writer process!"
 *
 * Note: /dev/shm/demo_shared_mem appears as a file while it exists.
 * ls /dev/shm to see it.
 */

Coding Example 4 – Anonymous Shared Memory (MAP_ANONYMOUS)

/*
 * anon_mmap_ipc.c
 * MAP_ANONYMOUS + MAP_SHARED: share memory between parent and child
 * without creating a named file or shm object. Fastest setup.
 *
 * Compile: gcc anon_mmap_ipc.c -o anon_mmap_ipc
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/wait.h>

int main(void) {
    /* Allocate anonymous shared memory — visible after fork() */
    int *shared_counter = mmap(NULL, sizeof(int),
                                PROT_READ | PROT_WRITE,
                                MAP_SHARED | MAP_ANONYMOUS,
                                -1, 0);
    if (shared_counter == MAP_FAILED) {
        perror("mmap");
        exit(EXIT_FAILURE);
    }

    *shared_counter = 0;  /* Initialize */

    pid_t pid = fork();

    if (pid == 0) {
        /* CHILD: increment the shared counter 5 times */
        for (int i = 0; i < 5; i++) {
            (*shared_counter)++;   /* Direct memory write — no system call! */
            printf("[Child]  counter = %d\n", *shared_counter);
            usleep(10000);
        }
        exit(EXIT_SUCCESS);

    } else {
        /* PARENT: wait for child, then read final value */
        wait(NULL);
        printf("[Parent] Final shared counter = %d\n", *shared_counter);

        munmap(shared_counter, sizeof(int));
    }

    return 0;
}
/*
 * Output:
 *   [Child]  counter = 1
 *   [Child]  counter = 2
 *   [Child]  counter = 3
 *   [Child]  counter = 4
 *   [Child]  counter = 5
 *   [Parent] Final shared counter = 5
 *
 * WARNING: In real code you need a mutex/semaphore to protect
 * concurrent access to shared_counter.
 */

Data Transfer vs Shared Memory – Quick Comparison
Feature Data Transfer (Pipe/MQ) Shared Memory
Data copies 2 (user↔kernel each direction) 0 (direct RAM access)
Speed Slower for large data Very fast
Synchronization Automatic (blocking reads) Manual (semaphore/mutex required)
Read semantics Destructive (data consumed) Non-destructive (all see same data)
Multiple readers Only one reader gets data All mapped processes see data
Complexity Simple API (read/write/mq_send) More complex (needs locking)

Interview Questions – Communication Facilities
Q1. What is the difference between a byte-stream and a message-based IPC facility?
Byte-stream (pipes, FIFOs, TCP sockets): data has no boundaries. A single read() may return bytes from multiple write() calls, or only part of one. Message-based (POSIX MQ, System V MQ, UDP sockets): boundaries are preserved. One mq_receive() returns exactly one message, regardless of its size.
Q2. Why is shared memory faster than a pipe for large data transfer?
A pipe requires two memory copies: user→kernel on write(), and kernel→user on read(). Shared memory maps the same physical RAM pages into both processes’ address spaces. Once mapped, reading and writing involves zero system calls and zero copies — it’s as fast as normal memory access.
Q3. What does “destructive read” mean? Which IPC facilities have this property?
A destructive read means the act of reading consumes the data — once Process B reads it, it is gone and no other process can read that same data. Pipes, FIFOs, message queues, and stream sockets all have destructive read semantics. Shared memory does NOT — all processes see the same data simultaneously.
Q4. What happens if a process calls read() on an empty pipe? Is this configurable?
By default, read() on an empty pipe blocks — the calling process is put to sleep until another process writes data. This automatic synchronization is convenient. To change this behavior, you can set the pipe’s file descriptor to non-blocking mode using fcntl(fd, F_SETFL, O_NONBLOCK), after which read() returns -1 with errno=EAGAIN when the pipe is empty.
Q5. What is the difference between shm_open() and shmget()? Which is preferred?
shmget() is the System V API — older, uses integer keys, not POSIX-compliant. shm_open() is the POSIX API — uses named objects (e.g., /demo_shm), integrates with the filesystem namespace (/dev/shm), returns a file descriptor compatible with ftruncate/mmap/close. POSIX (shm_open) is preferred in new code for cleaner API and better portability.
Q6. Can MAP_ANONYMOUS | MAP_SHARED be used for IPC? What is the limitation?
Yes — MAP_ANONYMOUS|MAP_SHARED creates a shared memory region usable between a process and its fork()ed children (since children inherit the mapping). The limitation is that it cannot be used between unrelated processes (ones that didn’t fork from a common ancestor), because there is no name or identifier to share the mapping through.
Q7. Why must you use a semaphore with shared memory?
Shared memory gives multiple processes direct access to the same RAM simultaneously. Without synchronization, two processes can update the same data structure at the same time — a classic race condition. A semaphore (or mutex) ensures only one process accesses the critical section at a time, preventing corruption.

Leave a Reply

Your email address will not be published. Required fields are marked *