POSIX IPC Creating, Opening, Closing & Unlinking IPC Objects

 

POSIX IPC – Part 2
Creating, Opening, Closing & Unlinking IPC Objects · Chapter 51 · TLPI Series

← Index | ← Part 1: API Overview | Part 2: Create/Open/Close/Unlink

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.

Analogy: File open() vs IPC open()
File open()
fd = open("/tmp/data",
          O_CREAT | O_RDWR,
          S_IRUSR | S_IWUSR);
Returns: int fd
POSIX IPC shm_open()
fd = shm_open("/mymem",
              O_CREAT | O_RDWR,
              S_IRUSR | S_IWUSR);
Returns: int fd
The arguments are identical in concept: name, flags (oflag), and permissions (mode). The main difference is where the object lives — in the IPC namespace, not the regular filesystem.

Open 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)

Common Flag Combinations
Create new (fail if exists)
O_CREAT | O_EXCL | O_RDWR
Use this when only ONE process should create the object
Create or open existing
O_CREAT | O_RDWR
Creates if missing, opens if present (idempotent)
Open existing only
O_RDONLY
// or
O_RDWR
For a consumer/reader that expects the object to already exist

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.

Close vs Unlink — Important Difference
mq_close() / sem_close() / munmap() + close()
Releases the handle in this process only.
Object still exists in the kernel.
Other processes can still open it.
Like closing a file: file still on disk.
mq_unlink() / sem_unlink() / shm_unlink()
Removes the name from the namespace.
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
Common Bug: Forgetting to call 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

Full Message Queue Lifecycle
/* 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.
 */
Full Semaphore Lifecycle (Producer/Consumer)
/* 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 with Semaphore Synchronization

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
Tip for debugging: If your program crashes and leaves stale IPC objects, you can 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

Q1. What flags do you use to create a POSIX IPC object that fails if it already exists?
Use 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.
Q2. What is the difference between closing and unlinking a POSIX IPC object?
Closing (mq_close, sem_close, munmap+close) releases the handle in the calling process only. The object remains in the kernel and other processes can still use it. Unlinking (mq_unlink, sem_unlink, shm_unlink) removes the name from the IPC namespace. The object is destroyed once all processes that have it open also close it — same reference-counting model as file unlink.
Q3. What is kernel persistence in POSIX IPC?
POSIX IPC objects persist in the kernel as long as the system is running, even after all processes that created/opened them have exited — until an explicit unlink call removes them. They do not survive a system reboot. This is called kernel persistence, as opposed to process persistence (object disappears when creating process exits) or filesystem persistence (survives reboot).
Q4. Why should you close a shared memory file descriptor after mmap()?
After 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).
Q5. What error do you get when trying to use O_CREAT | O_EXCL on an already-existing POSIX IPC object?
The call fails and sets 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.
Q6. What does O_NONBLOCK do for message queues?
It puts the queue into non-blocking mode. 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.
Q7. What happens to a POSIX shared memory object if you forget to call shm_unlink()?
The object remains in the kernel (in /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.
Q8. Can you perform IPC without synchronization on shared memory?
Technically yes — but it causes race conditions. If two processes write to overlapping regions simultaneously, data gets corrupted. In practice, shared memory is always paired with a synchronization primitive: a POSIX semaphore, POSIX mutex placed in the shared region (using pthread_mutexattr_setpshared()), or a POSIX condition variable. Never use shared memory for IPC without some form of explicit synchronization.
Q9. What are the three arguments common to all POSIX IPC open calls, and what do they do?
name: the IPC object’s name in the portable form /name
oflag: 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

Leave a Reply

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