Why POSIX IPC?
Linux has two generations of IPC: System V IPC (old, complex) and POSIX IPC (modern, simpler). Both offer message queues, semaphores, and shared memory — but POSIX IPC was designed to fix the pain points of System V.
| Feature | System V IPC | POSIX IPC |
|---|---|---|
| Identification | Integer key (ftok) | Name string like /myobject |
| Open interface | msgget(), semget(), shmget() | mq_open(), sem_open(), shm_open() |
| Semaphore model | Sets of semaphores, complex semop() | Individual semaphores, simple post/wait |
| Message priority | Type field (manual sorting) | Built-in priority per message |
| Ease of use | Complex, verbose | Simpler, file-like API |
The Three POSIX IPC Mechanisms
A message queue is a linked list of messages maintained by the kernel. Processes send messages to the queue and receive them. Unlike a pipe, message boundaries are preserved — you always receive one full message at a time, not a partial stream.
The key advantage over pipes: each message has a priority number. When a process calls mq_receive(), the kernel delivers the highest-priority message first. Among messages with equal priority, they are FIFO.
mq_send()
mq_receive()
/* ---- Basic Message Queue Example ---- */
#include <stdio.h>
#include <string.h>
#include <mqueue.h>
#define QUEUE_NAME "/my_test_queue"
#define MAX_MSG_SIZE 256
int main() {
mqd_t mq;
struct mq_attr attr;
char msg_buf[MAX_MSG_SIZE];
/* Set queue attributes */
attr.mq_flags = 0;
attr.mq_maxmsg = 10; /* max 10 messages in queue */
attr.mq_msgsize = MAX_MSG_SIZE;
attr.mq_curmsgs = 0;
/* Create the queue */
mq = mq_open(QUEUE_NAME, O_CREAT | O_RDWR, 0644, &attr);
if (mq == (mqd_t)-1) {
perror("mq_open failed");
return 1;
}
/* Send a message with priority 5 */
const char *msg = "Hello from Process A";
if (mq_send(mq, msg, strlen(msg) + 1, 5) == -1) {
perror("mq_send failed");
}
/* Receive the highest-priority message */
unsigned int prio;
ssize_t bytes = mq_receive(mq, msg_buf, MAX_MSG_SIZE, &prio);
if (bytes >= 0) {
printf("Received (prio=%u): %s\n", prio, msg_buf);
}
/* Cleanup */
mq_close(mq);
mq_unlink(QUEUE_NAME); /* remove from kernel */
return 0;
}
/* Compile: gcc -o mq_demo mq_demo.c -lrt */
A semaphore is a kernel-maintained integer counter. The rule is simple: the value can never go below zero. It is used to protect shared resources — only one process (or a limited number) can access the resource at a time.
POSIX gives us two flavors:
/my_semAny process with permission can open it
Created with
sem_open()Persistent until
sem_unlink()Used by threads of same process, or processes sharing same memory
Created with
sem_init()Destroyed with
sem_destroy()/* ---- Named Semaphore Example ---- */
#include <stdio.h>
#include <semaphore.h>
#include <fcntl.h>
#define SEM_NAME "/my_semaphore"
int main() {
sem_t *sem;
/* Create named semaphore, initial value = 1 (unlocked) */
sem = sem_open(SEM_NAME, O_CREAT | O_EXCL, 0644, 1);
if (sem == SEM_FAILED) {
perror("sem_open failed");
return 1;
}
printf("Waiting to enter critical section...\n");
/* sem_wait: decrement semaphore (blocks if value == 0) */
if (sem_wait(sem) == -1) {
perror("sem_wait failed");
}
printf("Inside critical section\n");
/* ... do critical work ... */
/* sem_post: increment semaphore (allows others in) */
if (sem_post(sem) == -1) {
perror("sem_post failed");
}
printf("Left critical section\n");
/* Cleanup */
sem_close(sem);
sem_unlink(SEM_NAME);
return 0;
}
/* Compile: gcc -o sem_demo sem_demo.c -lpthread */
/* ---- Unnamed Semaphore (threads) Example ---- */
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
sem_t sem; /* shared between threads in same process */
int counter = 0;
void *increment(void *arg) {
sem_wait(&sem); /* lock */
counter++;
printf("Thread %ld: counter = %d\n", (long)arg, counter);
sem_post(&sem); /* unlock */
return NULL;
}
int main() {
pthread_t t1, t2;
/* sem_init: place in memory (not shared with other processes),
initial value = 1 */
sem_init(&sem, 0, 1);
pthread_create(&t1, NULL, increment, (void*)1);
pthread_create(&t2, NULL, increment, (void*)2);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
sem_destroy(&sem);
return 0;
}
/* Compile: gcc -o usem_demo usem_demo.c -lpthread */
Shared memory is the fastest IPC mechanism because data is not copied between processes. Both processes map the same physical memory page. One writes, the other reads directly.
The handle for POSIX shared memory is a regular file descriptor (int), obtained with shm_open(). Then you call mmap() to actually map it into your process’s address space.
/dev/shm/myobj
/* ---- Shared Memory Writer (Process A) ---- */
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define SHM_NAME "/my_shared_mem"
#define SHM_SIZE 4096
int main() {
/* Step 1: Create shared memory object */
int fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR);
if (fd == -1) {
perror("shm_open failed");
return 1;
}
/* Step 2: Set the size */
if (ftruncate(fd, SHM_SIZE) == -1) {
perror("ftruncate failed");
return 1;
}
/* Step 3: Map into address space */
char *ptr = mmap(NULL, SHM_SIZE, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
if (ptr == MAP_FAILED) {
perror("mmap failed");
return 1;
}
/* Step 4: Write data */
snprintf(ptr, SHM_SIZE, "Hello from Process A!");
printf("Writer: wrote '%s'\n", ptr);
/* Step 5: Unmap (shared memory stays until shm_unlink) */
munmap(ptr, SHM_SIZE);
close(fd);
return 0;
}
/* Compile: gcc -o shm_writer shm_writer.c -lrt */
/* ---- Shared Memory Reader (Process B) ---- */
#include <stdio.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define SHM_NAME "/my_shared_mem"
#define SHM_SIZE 4096
int main() {
/* Open existing shared memory (no O_CREAT) */
int fd = shm_open(SHM_NAME, O_RDONLY, 0);
if (fd == -1) {
perror("shm_open failed");
return 1;
}
/* Map read-only */
char *ptr = mmap(NULL, SHM_SIZE, PROT_READ, MAP_SHARED, fd, 0);
if (ptr == MAP_FAILED) {
perror("mmap failed");
return 1;
}
printf("Reader: got '%s'\n", ptr);
/* Cleanup */
munmap(ptr, SHM_SIZE);
close(fd);
shm_unlink(SHM_NAME); /* remove the object */
return 0;
}
/* Compile: gcc -o shm_reader shm_reader.c -lrt */
POSIX IPC API Summary Table
Here is the full side-by-side API for all three mechanisms. Think of it as your quick-reference cheat sheet.
| Interface | Message Queues | Semaphores | Shared Memory |
|---|---|---|---|
| Header | <mqueue.h> |
<semaphore.h> |
<sys/mman.h> |
| Handle type | mqd_t |
sem_t * |
int (file descriptor) |
| Create/Open | mq_open() |
sem_open() |
shm_open() + mmap() |
| Close | mq_close() |
sem_close() |
munmap() |
| Remove (unlink) | mq_unlink() |
sem_unlink() |
shm_unlink() |
| Perform IPC | mq_send(), mq_receive() |
sem_post(), sem_wait(), sem_getvalue() |
Read/write via mapped pointer |
| Extras | mq_setattr(), mq_getattr(), mq_notify() |
sem_init(), sem_destroy() (unnamed) |
— |
-lrt for message queues and shared memory on older glibc versions. On modern Linux, semaphores need -lpthread.IPC Object Names
Unlike System V IPC (which uses integer keys from ftok()), POSIX IPC uses human-readable name strings to identify objects.
/, no more slashes, followed by 1+ non-slash charactersmyobject
Semaphores: 251 chars (kernel prepends
sem.)How Linux Implements IPC Names
On Linux, POSIX IPC objects are backed by the virtual filesystem:
| IPC Type | Name Example | Linux Path |
|---|---|---|
| Shared Memory | /mymem |
/dev/shm/mymem |
| Message Queue | /myqueue |
/dev/mqueue/myqueue |
| Semaphore | /mysem |
/dev/shm/sem.mysem |
/* Checking IPC objects on Linux */
/* List POSIX shared memory and semaphores */
// ls /dev/shm/
/* List POSIX message queues (needs mqueue fs mounted) */
// ls /dev/mqueue/
// cat /proc/sys/fs/mqueue/queues_max (max allowed queues)
/* Mount the mqueue filesystem if not already mounted */
// mount -t mqueue none /dev/mqueue
/* You can also view message queue info via /proc */
// cat /dev/mqueue/my_queue (shows attributes)
Portable Name Generation
Since naming rules differ across implementations (e.g., Tru64 treats the name as a file path in the real filesystem), you should isolate name generation in a helper function or header:
/* ipc_names.h — portable IPC name helper */
#ifndef IPC_NAMES_H
#define IPC_NAMES_H
/* On Linux: simply use the SUSv3 portable form /name */
#define MQ_NAME "/ep_message_queue"
#define SEM_NAME "/ep_semaphore"
#define SHM_NAME "/ep_shared_mem"
#endif
/* Usage in code */
#include "ipc_names.h"
#include <mqueue.h>
mqd_t mq = mq_open(MQ_NAME, O_CREAT | O_RDWR, 0644, NULL);
/* By isolating the name, you only change ipc_names.h
when porting to another OS */
Understanding Handle Types
Each POSIX IPC open call returns a different type of handle. Understanding these helps you avoid common bugs.
int file descriptor internally, but treat it as opaque. Check for error with == (mqd_t)-1.mqd_t mq = mq_open("/q", O_RDWR);
if (mq == (mqd_t)-1) {
perror("error");
}
== SEM_FAILED (which is (sem_t *)-1). Do NOT check against NULL.sem_t *s = sem_open("/s", O_CREAT, 0644, 1);
if (s == SEM_FAILED) {
perror("error");
}
-1. Close with close(fd) after mmap(). The mapping persists.int fd = shm_open("/m", O_RDWR, 0644);
if (fd == -1) {
perror("error");
}
/* After mmap(), close fd — mapping stays */
close(fd);
Interview Questions & Answers
/ followed by one or more non-slash characters, for example /myobject. This is specified by SUSv3. Multiple slashes or no leading slash may work on some systems but are not portable.sem. to the semaphore name internally, consuming 4 characters.mq_open() → mqd_t (message queue descriptor, opaque type)sem_open() → sem_t * (pointer to semaphore object; error = SEM_FAILED)shm_open() → int (regular file descriptor; error = -1)sem_open() and removed with sem_unlink(). An unnamed semaphore has no name; it lives in a shared memory region and is initialized with sem_init(). It is typically used by threads within the same process, or by processes that share the memory region containing it./dev/shm/Message queues appear under
/dev/mqueue/ (requires mqueue filesystem mounted)Semaphores appear as
sem.name files under /dev/shm/