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.
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. |
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.
*/
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
Older API. Uses shmget(), shmat(), shmdt(), shmctl(). Integer key-based. Limited portability.
Newer API. Uses shm_open(), mmap(), shm_unlink(). Named objects under /dev/shm. Cleaner and preferred.
MAP_SHARED + a file or MAP_ANONYMOUS. Most flexible. Used for file I/O, IPC, and anonymous shared memory.
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.
*/
| 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) |
