The IPC Open Call — Your Entry Point
Each POSIX IPC mechanism has its own “open” function: mq_open(), sem_open(), and shm_open(). These work very much like the traditional open() system call for files. You provide a name, some flags, and permissions — and you get back a handle to work with.
The open call either creates a new object (if it doesn’t exist) or opens an existing one (if it already exists), depending on the flags you pass.
fd = open("/tmp/data",
O_CREAT | O_RDWR,
S_IRUSR | S_IWUSR);
int fdfd = shm_open("/mymem",
O_CREAT | O_RDWR,
S_IRUSR | S_IWUSR);
int fdOpen Flags (oflag)
The oflag argument is a bitmask. You combine flags with |. Here are all the flags supported by POSIX IPC open calls:
| Flag | Meaning | Error if condition not met |
|---|---|---|
O_CREAT |
Create the object if it doesn’t exist yet | ENOENT if omitted and object missing |
O_EXCL |
Fail if object already exists (used with O_CREAT) | EEXIST if object already exists |
O_RDONLY |
Open for reading only | EACCES if no read permission |
O_WRONLY |
Open for writing only | EACCES if no write permission |
O_RDWR |
Open for both reading and writing | — |
O_NONBLOCK |
Non-blocking mode (mainly for message queues — mq_send/mq_receive return EAGAIN instead of blocking) | — |
O_CREAT | O_EXCL | O_RDWR
O_CREAT | O_RDWR
O_RDONLY
// or
O_RDWR
O_CREAT | O_EXCL — Atomicity Guarantee
A critical property: when you use O_CREAT | O_EXCL, the check-for-existence and creation happen as a single atomic operation. There is no race condition where two processes could both think the object doesn’t exist and both try to create it. The kernel guarantees that only one succeeds.
/* O_CREAT | O_EXCL ensures only one process creates the object */
#include <stdio.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <errno.h>
#define SHM_NAME "/ep_exclusive"
#define SHM_SIZE 1024
int main() {
int fd;
fd = shm_open(SHM_NAME, O_CREAT | O_EXCL | O_RDWR,
S_IRUSR | S_IWUSR);
if (fd == -1) {
if (errno == EEXIST) {
/* Another process already created it — open existing */
printf("Already exists, opening...\n");
fd = shm_open(SHM_NAME, O_RDWR, 0);
if (fd == -1) {
perror("shm_open (open existing) failed");
return 1;
}
} else {
perror("shm_open (create exclusive) failed");
return 1;
}
} else {
printf("Created new shared memory object\n");
/* Set size for the newly created object */
if (ftruncate(fd, SHM_SIZE) == -1) {
perror("ftruncate failed");
return 1;
}
}
/* Now use fd for mmap() ... */
printf("fd = %d, ready to use\n", fd);
close(fd);
return 0;
}
/* Compile: gcc -o excl_demo excl_demo.c -lrt */
The mode Argument
The third argument to all IPC open calls sets the permission bits for the new object, just like open() for files. The permissions are masked by the process’s umask.
/* Common permission constants */
S_IRUSR /* owner read = 0400 */
S_IWUSR /* owner write = 0200 */
S_IRGRP /* group read = 0040 */
S_IWGRP /* group write = 0020 */
S_IROTH /* others read = 0004 */
S_IWOTH /* others write = 0002 */
/* Typical single-user app: rw for owner only */
shm_open("/myobj", O_CREAT | O_RDWR, S_IRUSR | S_IWUSR);
/* Multi-process app where group needs access */
shm_open("/myobj", O_CREAT | O_RDWR, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP);
Closing POSIX IPC Objects
Closing a POSIX IPC object releases the handle in the current process but does NOT destroy the object. The object remains in the kernel until explicitly unlinked. This is called kernel persistence.
Object still exists in the kernel.
Other processes can still open it.
Like closing a file: file still on disk.
Object is destroyed when all processes close it.
No new processes can open it by name after unlink.
Like
unlink() for files./* ---- Close calls for each mechanism ---- */
/* Message Queue */
mqd_t mq = mq_open("/myq", O_RDWR);
/* ... use mq ... */
mq_close(mq); /* release handle in this process */
mq_unlink("/myq"); /* remove from kernel (when last ref closes) */
/* Named Semaphore */
sem_t *sem = sem_open("/mysem", O_RDWR);
/* ... use sem ... */
sem_close(sem); /* release handle */
sem_unlink("/mysem"); /* remove from kernel */
/* Shared Memory */
int fd = shm_open("/mymem", O_RDWR, 0);
void *ptr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
/* ... use ptr ... */
munmap(ptr, 4096); /* unmap this process's mapping */
close(fd); /* close the fd */
shm_unlink("/mymem"); /* remove the kernel object */
Kernel Persistence — What It Means
POSIX IPC objects have kernel persistence: they survive until explicitly deleted with an unlink call, even if no process has them open. They do not persist across a system reboot. This is different from System V IPC, which also has kernel persistence.
| Persistence Type | Survives process exit? | Survives reboot? | Examples |
|---|---|---|---|
| Process persistence | ❌ No | ❌ No | Pipes, unnamed semaphores |
| Kernel persistence | ✅ Yes | ❌ No | POSIX IPC, System V IPC |
| Filesystem persistence | ✅ Yes | ✅ Yes | Regular files |
mq_unlink() / sem_unlink() / shm_unlink() in your cleanup code means the objects accumulate in the kernel across program runs. You will get EEXIST errors when trying to create them next time with O_EXCL, or you will attach to stale objects from a previous crashed run.
Always unlink in a cleanup function or signal handler.
Complete Lifecycle Examples
/* mq_lifecycle.c — Create, send, receive, close, unlink */
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <mqueue.h>
#include <fcntl.h>
#include <signal.h>
#include <stdlib.h>
#define MQ_NAME "/ep_mq_lifecycle"
#define MSG_SIZE 128
#define MAX_MSGS 5
static mqd_t mq_fd = (mqd_t)-1;
/* Cleanup handler to prevent stale objects on Ctrl+C */
void cleanup(int sig) {
if (mq_fd != (mqd_t)-1) {
mq_close(mq_fd);
}
mq_unlink(MQ_NAME);
printf("\nCleaned up. Exiting.\n");
exit(0);
}
int main() {
struct mq_attr attr;
char buf[MSG_SIZE];
unsigned int prio;
signal(SIGINT, cleanup); /* handle Ctrl+C gracefully */
/* ---- STEP 1: Create ---- */
attr.mq_flags = 0;
attr.mq_maxmsg = MAX_MSGS;
attr.mq_msgsize = MSG_SIZE;
attr.mq_curmsgs = 0;
mq_fd = mq_open(MQ_NAME, O_CREAT | O_EXCL | O_RDWR, 0644, &attr);
if (mq_fd == (mqd_t)-1) {
if (errno == EEXIST) {
/* Stale from previous run — unlink and retry */
mq_unlink(MQ_NAME);
mq_fd = mq_open(MQ_NAME, O_CREAT | O_EXCL | O_RDWR, 0644, &attr);
}
if (mq_fd == (mqd_t)-1) {
perror("mq_open failed");
return 1;
}
}
printf("Queue created: %s\n", MQ_NAME);
/* ---- STEP 2: Send messages with different priorities ---- */
mq_send(mq_fd, "Low priority task", 18, 1); /* prio 1 */
mq_send(mq_fd, "High priority alert", 21, 9); /* prio 9 */
mq_send(mq_fd, "Medium priority msg", 20, 5); /* prio 5 */
printf("Sent 3 messages\n");
/* ---- STEP 3: Receive (always gets highest priority first) ---- */
printf("\nReceiving (highest priority first):\n");
for (int i = 0; i < 3; i++) {
ssize_t n = mq_receive(mq_fd, buf, MSG_SIZE, &prio);
if (n == -1) {
perror("mq_receive failed");
break;
}
buf[n] = '\0';
printf(" [prio=%u] %s\n", prio, buf);
}
/* ---- STEP 4: Close (handle released, object stays) ---- */
mq_close(mq_fd);
mq_fd = (mqd_t)-1;
printf("\nClosed handle. Queue still in kernel.\n");
/* ---- STEP 5: Unlink (remove from kernel) ---- */
if (mq_unlink(MQ_NAME) == -1) {
perror("mq_unlink failed");
return 1;
}
printf("Queue unlinked. Object destroyed.\n");
return 0;
}
/*
* Compile: gcc -o mq_lifecycle mq_lifecycle.c -lrt
* Expected output:
* Queue created: /ep_mq_lifecycle
* Sent 3 messages
* Receiving (highest priority first):
* [prio=9] High priority alert
* [prio=5] Medium priority msg
* [prio=1] Low priority task
* Closed handle. Queue still in kernel.
* Queue unlinked. Object destroyed.
*/
/* sem_producer_consumer.c
Two processes use a named semaphore to take turns */
#include <stdio.h>
#include <semaphore.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
#define SEM_EMPTY "/ep_sem_empty" /* signals: slot available to write */
#define SEM_FULL "/ep_sem_full" /* signals: data available to read */
#define ROUNDS 4
int main() {
sem_t *sem_empty, *sem_full;
int val;
/* Create two semaphores */
sem_empty = sem_open(SEM_EMPTY, O_CREAT | O_EXCL, 0644, 1); /* 1 = slot free */
sem_full = sem_open(SEM_FULL, O_CREAT | O_EXCL, 0644, 0); /* 0 = no data yet */
if (sem_empty == SEM_FAILED || sem_full == SEM_FAILED) {
perror("sem_open failed");
/* Clean up possibly-created semaphores */
sem_unlink(SEM_EMPTY);
sem_unlink(SEM_FULL);
return 1;
}
pid_t pid = fork();
if (pid == 0) {
/* ---- CHILD = Consumer ---- */
for (int i = 0; i < ROUNDS; i++) {
sem_wait(sem_full); /* wait until data is available */
printf("Consumer: read item %d\n", i + 1);
sem_post(sem_empty); /* signal: slot is free again */
}
sem_close(sem_empty);
sem_close(sem_full);
exit(0);
} else {
/* ---- PARENT = Producer ---- */
for (int i = 0; i < ROUNDS; i++) {
sem_wait(sem_empty); /* wait until slot is free */
printf("Producer: wrote item %d\n", i + 1);
sem_post(sem_full); /* signal: data is ready */
usleep(100000); /* 100ms delay */
}
wait(NULL); /* wait for child */
/* Cleanup */
sem_close(sem_empty);
sem_close(sem_full);
sem_unlink(SEM_EMPTY);
sem_unlink(SEM_FULL);
printf("Done.\n");
}
return 0;
}
/* Compile: gcc -o sem_pc sem_producer_consumer.c -lpthread */
Shared memory alone has a race condition problem — if two processes write at the same time, data gets corrupted. The real-world pattern always pairs shared memory with a semaphore or mutex.
/* shm_with_sem.c — Safe shared memory using a semaphore lock */
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <semaphore.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
#define SHM_NAME "/ep_safe_shm"
#define SEM_NAME "/ep_shm_lock"
#define SHM_SIZE 256
int main() {
int fd;
char *ptr;
sem_t *sem;
/* Create shared memory */
fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR);
ftruncate(fd, SHM_SIZE);
ptr = mmap(NULL, SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
close(fd); /* fd no longer needed after mmap */
/* Create mutex semaphore (initial value = 1 = unlocked) */
sem = sem_open(SEM_NAME, O_CREAT | O_EXCL, 0644, 1);
if (sem == SEM_FAILED) {
perror("sem_open");
return 1;
}
/* Initialize shared data */
snprintf(ptr, SHM_SIZE, "initial data");
pid_t pid = fork();
if (pid == 0) {
/* ---- CHILD ---- */
sleep(1); /* let parent write first */
sem_wait(sem); /* acquire lock */
printf("Child reads: '%s'\n", ptr);
snprintf(ptr, SHM_SIZE, "updated by child");
sem_post(sem); /* release lock */
munmap(ptr, SHM_SIZE);
sem_close(sem);
exit(0);
} else {
/* ---- PARENT ---- */
sem_wait(sem); /* acquire lock */
snprintf(ptr, SHM_SIZE, "written by parent");
printf("Parent wrote: '%s'\n", ptr);
sem_post(sem); /* release lock */
wait(NULL); /* wait for child */
/* Parent does final read */
sem_wait(sem);
printf("Parent final read: '%s'\n", ptr);
sem_post(sem);
/* Cleanup everything */
munmap(ptr, SHM_SIZE);
sem_close(sem);
shm_unlink(SHM_NAME);
sem_unlink(SEM_NAME);
printf("All cleaned up.\n");
}
return 0;
}
/* Compile: gcc -o shm_sem shm_with_sem.c -lrt -lpthread */
Non-Blocking IPC with O_NONBLOCK
By default, mq_send() blocks when the queue is full, and mq_receive() blocks when the queue is empty. Using O_NONBLOCK makes these calls return immediately with EAGAIN instead of blocking.
/* Non-blocking message queue receive */
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <mqueue.h>
#include <fcntl.h>
#define MQ_NAME "/ep_nonblock_mq"
#define MSG_SIZE 128
int main() {
struct mq_attr attr;
char buf[MSG_SIZE];
unsigned int prio;
attr.mq_flags = 0;
attr.mq_maxmsg = 5;
attr.mq_msgsize = MSG_SIZE;
attr.mq_curmsgs = 0;
/* Open with O_NONBLOCK */
mqd_t mq = mq_open(MQ_NAME, O_CREAT | O_RDWR | O_NONBLOCK, 0644, &attr);
if (mq == (mqd_t)-1) {
perror("mq_open");
return 1;
}
/* Try to receive from empty queue — won't block */
ssize_t n = mq_receive(mq, buf, MSG_SIZE, &prio);
if (n == -1) {
if (errno == EAGAIN) {
printf("Queue is empty, no message available (non-blocking)\n");
} else {
perror("mq_receive");
}
}
/* Send one message, then receive successfully */
mq_send(mq, "test message", 13, 1);
n = mq_receive(mq, buf, MSG_SIZE, &prio);
if (n > 0) {
buf[n] = '\0';
printf("Received: '%s'\n", buf);
}
mq_close(mq);
mq_unlink(MQ_NAME);
return 0;
}
/* Compile: gcc -o nb_mq nb_mq.c -lrt */
Inspecting POSIX IPC Objects on Linux
# ---- Shell commands to inspect POSIX IPC objects ----
# List all shared memory objects
ls -la /dev/shm/
# List all semaphores (they appear with sem. prefix)
ls -la /dev/shm/sem.*
# Mount the mqueue filesystem (if not already mounted)
sudo mount -t mqueue none /dev/mqueue
# List all message queues
ls -la /dev/mqueue/
# Inspect a specific message queue
cat /dev/mqueue/my_queue
# Output shows: QSIZE (bytes), NOTIFY (0/1), SIGNO, NOTIFY_PID
# Check system limits for message queues
cat /proc/sys/fs/mqueue/queues_max # max number of queues (default 256)
cat /proc/sys/fs/mqueue/msg_max # max messages per queue (default 10)
cat /proc/sys/fs/mqueue/msgsize_max # max message size (default 8192)
# Remove stale IPC objects manually
rm /dev/shm/my_object # remove shared memory
rm /dev/shm/sem.my_sem # remove semaphore
rm /dev/mqueue/my_queue # remove message queue
rm them directly from /dev/shm/ or /dev/mqueue/. This is one advantage of POSIX IPC over System V — the objects are visible in the filesystem.Interview Questions & Answers
O_CREAT | O_EXCL. The check-and-create is atomic — there is no race condition. If the object already exists, the call fails with errno == EEXIST.mmap() succeeds, the mapping is maintained by the kernel independently of the file descriptor. The fd is no longer needed to keep the mapping alive. Calling close(fd) frees the file descriptor slot in the process without affecting the memory mapping. Keeping unnecessary open fd’s wastes per-process fd slots (limited to RLIMIT_NOFILE).errno to EEXIST. This is the correct way to detect the “object already exists” case. A common pattern is to catch EEXIST, unlink the stale object, and retry creation — or simply open the existing object without O_EXCL.mq_send() returns -1 with errno == EAGAIN if the queue is full (instead of blocking). mq_receive() returns -1 with errno == EAGAIN if the queue is empty (instead of blocking). This is useful in event-driven or polling-based applications./dev/shm/ on Linux) across program restarts until the system reboots. This wastes physical memory and can cause EEXIST errors on the next program run if O_EXCL was used. It can also mislead other programs that open the stale object expecting fresh data. Always call shm_unlink() in a cleanup function, SIGINT/SIGTERM handler, or atexit() handler.pthread_mutexattr_setpshared()), or a POSIX condition variable. Never use shared memory for IPC without some form of explicit synchronization./nameoflag: bitmask of O_CREAT, O_EXCL, O_RDONLY, O_RDWR, O_WRONLY, O_NONBLOCK controlling creation and access mode
mode: permission bits (like file permissions) applied to a newly created object, masked by the process’s umask
