POSIX IPC API Overview & IPC Object Names

 

POSIX IPC – Part 1
API Overview & IPC Object Names · Chapter 51 · TLPI Series

← Index | Part 1: API Overview & Naming | Part 2: Create/Open/Close →

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

📨 1. Message Queues

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.

Message Queue Flow
Process A
mq_send()
Kernel Queue
prio=9 “ALARM”
prio=5 “DATA”
prio=1 “LOG”
Process B
mq_receive()
Process B always gets the highest-priority message first (prio=9 here)
/* ---- 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 */

🔒 2. Semaphores

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:

Named Semaphore
Has a name like /my_sem
Any process with permission can open it
Created with sem_open()
Persistent until sem_unlink()
Unnamed Semaphore
No name — lives in shared memory
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 */

🗂️ 3. Shared Memory

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.

Shared Memory Layout
Process A
Virtual Address Space
ptr → shared region
↘ ↙
Physical RAM
/dev/shm/myobj
Process B
Virtual Address Space
ptr → shared region
/* ---- 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)
Note: Link with -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.

POSIX IPC Naming Rules
✅ Portable (SUSv3)
/myobject
Starts with /, no more slashes, followed by 1+ non-slash characters
❌ Not portable
/dir/subdir/obj
myobject
Multiple slashes or missing leading slash are implementation-defined
📏 Linux Limits
Shared memory + MQ: NAME_MAX = 255 chars
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.

mqd_t
Message Queue Descriptor. On Linux this is actually an 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_t *
Pointer to a semaphore structure. Check for error with == 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");
}
int (fd)
Shared memory returns a regular file descriptor. Check against -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

Q1. What are the three POSIX IPC mechanisms?
POSIX IPC provides Message Queues (for passing discrete priority-tagged messages), Semaphores (for synchronizing access to shared resources using a kernel counter), and Shared Memory (for mapping the same physical memory into multiple process address spaces for fast data sharing).
Q2. What is the correct format for a portable POSIX IPC object name?
A portable name must start with a single / 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.
Q3. What is the name length limit for POSIX semaphores on Linux, and why?
The limit is 251 characters (instead of NAME_MAX=255 for other types). Linux prepends the string sem. to the semaphore name internally, consuming 4 characters.
Q4. What handle type does each POSIX IPC open call return?
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)
Q5. What is the difference between a named and an unnamed POSIX semaphore?
A named semaphore is identified by a name and accessible by any process that has appropriate permissions. It is created with 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.
Q6. Why is shared memory considered the fastest IPC mechanism?
Because no data copying happens between processes. Both processes map the same physical memory pages. When Process A writes to its mapped pointer, Process B immediately reads the change via its own mapped pointer. There is no kernel buffer copy, unlike pipes or message queues where data is copied into and out of kernel buffers.
Q7. How do POSIX message queues differ from pipes in message delivery?
Pipes provide an undelimited byte stream — there are no message boundaries and the receiver must handle framing. POSIX message queues preserve message boundaries — each send and receive operation works on one complete message. Additionally, POSIX MQs support per-message priority, delivering higher-priority messages first regardless of insertion order.
Q8. Where are POSIX IPC objects stored on Linux?
Shared memory objects appear as files under /dev/shm/
Message queues appear under /dev/mqueue/ (requires mqueue filesystem mounted)
Semaphores appear as sem.name files under /dev/shm/

Leave a Reply

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