System V Message Queues – Introduction

 

System V Message Queues – Introduction
Chapter 45 (Page 17) → Chapter 46 Bridge | Linux System Programming | EmbeddedPathashala
msgget()
Create / Open
Integer
Message Type
FIFO + Type
Retrieval Modes

Key Terms in This Tutorial
Message Queue msgget() msgsnd() msgrcv() msgctl() Message Type IPC Identifier Message-Oriented FIFO Order msqid_ds Pipe vs MQ

What Are System V Message Queues?

A System V message queue is a kernel-maintained linked list of messages. Each message is a discrete unit of data with an associated integer type. Processes can send messages to the queue and receive them — either in first-in-first-out (FIFO) order or selectively by type.

Message queues were one of the original System V IPC mechanisms introduced into mainstream UNIX around 1983. They solve a simple but important problem: how do two unrelated processes on the same machine exchange structured, typed data without sharing memory or using file-based communication?

Bridge to Chapter 46: This page is the doorway between the Chapter 45 overview (common IPC concepts) and the full Chapter 46 deep-dive into message queue system calls, attributes, and limits. Understanding the three key differences from pipes is the most important takeaway here.

1. The Handle: IPC Identifier, Not a File Descriptor
msgget() IPC Identifier open() vs msgget()

When you open a regular file, the kernel gives you back a file descriptor — a small integer that is local to your process. File descriptors live in a per-process table and have no meaning outside that process.

Message queues work differently. The handle used to refer to a message queue is the IPC identifier returned by msgget(). This identifier is a property of the object itself, not of any particular process. Any process that knows the identifier can use it directly — no need to call msgget() again if you already know the ID.

File Descriptor vs IPC Identifier
Property File Descriptor (open) IPC Identifier (msgget)
Scope Per-process only System-wide, visible to all
Inherited by fork? Yes (copy) Not needed — all share same ID
Closed on exit? Yes, automatically No — persists until IPC_RMID
Uniqueness Unique per process Unique across entire system
Kernel persistence File data persists; fd does not Queue + ID persist after process exits
/* Opening a file — fd is per-process */
int fd = open("/tmp/myfile.txt", O_RDONLY);
/* fd is only meaningful in THIS process */

/* Opening a message queue — msqid is system-wide */
key_t key = ftok("/tmp/myfile.txt", 'A');
int msqid = msgget(key, IPC_CREAT | 0660);
/* msqid can be used by ANY process that knows it */

/* Another process can look up the same queue */
int msqid2 = msgget(key, 0);  /* same key → same msqid */
/* msqid == msqid2 — the same object */
Key Point: Because the identifier is system-wide, a process can write the identifier to a file, environment variable, or shared memory, and other unrelated processes can use it directly — much like sharing a file’s inode number rather than a file descriptor.

2. Message-Oriented: Whole Messages, Not a Byte Stream
Message Boundaries msgsnd() msgrcv() Pipe vs Queue

A pipe is a byte stream — data written at one end flows out the other end as a continuous sequence of bytes. The reader can read any number of bytes at a time. There are no boundaries between writes.

A message queue is message-oriented. Each call to msgsnd() writes one complete, self-contained message. Each call to msgrcv() reads exactly one complete message. You cannot read half a message and leave the rest, and you cannot read two messages in one call.

Pipe (Byte Stream) vs Message Queue (Message-Oriented)

Pipe — Byte Stream

Writer

H
e
l
l
o
W
r
No boundaries — continuous bytes
Reader reads N bytes at a time

Message Queue — Message-Oriented
Writer

type=1″Login req”
type=2″Data block”
type=1″Logout req”
Clear boundaries — each message is atomic
Reader gets one complete message
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#include <stdio.h>

/* Define a message structure — always starts with a long mtype */
struct my_message {
    long mtype;       /* Message type — MUST be first, MUST be > 0 */
    char text[128];   /* Message data payload */
};

/* --- WRITER process --- */
void writer_example(int msqid) {
    struct my_message msg;

    /* Send message type 1 */
    msg.mtype = 1;
    strncpy(msg.text, "Hello from writer!", sizeof(msg.text));
    msgsnd(msqid, &msg, sizeof(msg.text), 0);
    /* One msgsnd() = one complete, atomic message on the queue */

    /* Send another message type 2 */
    msg.mtype = 2;
    strncpy(msg.text, "Data block contents", sizeof(msg.text));
    msgsnd(msqid, &msg, sizeof(msg.text), 0);
}

/* --- READER process --- */
void reader_example(int msqid) {
    struct my_message msg;

    /* Read ANY message (mtype=0 means take the first one in FIFO order) */
    msgrcv(msqid, &msg, sizeof(msg.text), 0, 0);
    printf("Got: [type=%ld] %s\n", msg.mtype, msg.text);
    /*
     * msgrcv() ALWAYS returns exactly one complete message.
     * You cannot read half a message or skip part of it.
     * The message is REMOVED from the queue upon successful read.
     */
}
Important Rule: If you call msgrcv() with a buffer that is smaller than the message, the default behavior is to return an error (E2BIG). The message is NOT partially returned. You can use the MSG_NOERROR flag to silently truncate — but this loses data.

3. Integer Message Types — Selective Retrieval
mtype field FIFO Order Type-Based Selection Priority Queues

The most powerful feature distinguishing message queues from pipes is the integer type field. Every message carries a long integer type value (must be > 0). When calling msgrcv(), you can control which message you want:

msgrcv() mtype Argument Behavior
mtype value in msgrcv() What is returned Typical use case
0 First message in the queue (FIFO order, regardless of type) Simple FIFO queue behavior — like a pipe
> 0 (e.g., 3) First message with exactly that type Dedicated channels per message type (e.g., requests vs responses)
< 0 (e.g., -5) First message whose type is ≤ |mtype| (i.e., lowest type first) Priority queues — low type number = high priority
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <string.h>

#define MSG_TYPE_REQUEST  1
#define MSG_TYPE_RESPONSE 2
#define MSG_TYPE_ERROR    3

struct mymsg {
    long mtype;
    char data[256];
};

/*
 * Demonstrates all three retrieval modes.
 * Assume msqid is a valid message queue with several messages.
 */
void retrieval_modes(int msqid) {
    struct mymsg msg;
    ssize_t bytes;

    /* Mode 1: FIFO — get whatever came first */
    bytes = msgrcv(msqid, &msg, sizeof(msg.data), 0, 0);
    printf("FIFO mode: got type=%ld, data='%s'\n", msg.mtype, msg.data);

    /* Mode 2: Exact type — only get a response message */
    bytes = msgrcv(msqid, &msg, sizeof(msg.data), MSG_TYPE_RESPONSE, 0);
    printf("Exact type: got response: '%s'\n", msg.data);

    /* Mode 3: Priority — get the lowest-numbered type first */
    /* With mtype=-5, returns first msg with type in {1,2,3,4,5}, lowest first */
    bytes = msgrcv(msqid, &msg, sizeof(msg.data), -5, 0);
    printf("Priority mode: got type=%ld (highest priority)\n", msg.mtype);
}

/*
 * Classic client-server pattern using types:
 *   - Clients send type=1 (request)
 *   - Server replies with type=client_pid (so each client gets its own reply)
 */
void client_server_pattern(int msqid) {
    struct mymsg msg;

    /* CLIENT: send a request */
    msg.mtype = MSG_TYPE_REQUEST;
    snprintf(msg.data, sizeof(msg.data), "PID=%d: please process this", (int)getpid());
    msgsnd(msqid, &msg, strlen(msg.data) + 1, 0);

    /* CLIENT: wait for a reply addressed specifically to this PID */
    msgrcv(msqid, &msg, sizeof(msg.data), (long)getpid(), 0);
    printf("Client got reply: %s\n", msg.data);

    /* SERVER (in another process):
       reads type=1, processes it, replies with type=sender_pid */
}
Classic Pattern: In a multi-client server, the server sends each reply with mtype = client_pid. Each client then calls msgrcv(msqid, &msg, len, getpid(), 0) to receive only its own reply. This lets many clients share a single message queue without seeing each other’s responses.

4. Message Queues vs Pipes and FIFOs
Pipe Limitations FIFO Limitations Queue Advantages When to Prefer Each

The TLPI text explicitly notes that message queues have limitations (covered fully in Section 46.9), and that new applications should prefer FIFOs, POSIX message queues, or sockets where possible. But understanding what message queues do better than pipes is equally important.

Comparison: Pipes / FIFOs vs System V Message Queues
Feature Pipe / FIFO System V Message Queue
Communication model Byte stream (no boundaries) Message-oriented (discrete units)
Message framing Must be done by application (e.g., length prefix) Built-in — each write is one complete message
Selective read Not possible — strictly sequential Yes — read by type value
Kernel persistence Destroyed when no process has it open Persists until IPC_RMID or reboot
Access control File permissions on the FIFO file ipc_perm (owner/group/other, read/write)
Handle type File descriptor (per-process) IPC identifier (system-wide)
Unrelated processes FIFOs yes (via filesystem path); pipes no Yes (via key or known identifier)
Portability POSIX standard — highly portable SUSv3 XSI — widely available but less modern
Limits PIPE_BUF, pipe capacity msgmax, msgmnb, msgmni (system-wide)
/*
 * EXAMPLE: Why message boundaries matter
 *
 * With a PIPE, the writer must encode message length manually.
 * With a message queue, boundaries are automatic.
 */

/* ---- PIPE approach: manual framing ---- */
#include <unistd.h>
#include <stdint.h>
#include <string.h>

void pipe_send(int fd, const char *data, size_t len) {
    /* Must send the length first, then data */
    uint32_t nlen = (uint32_t)len;
    write(fd, &nlen, sizeof(nlen));  /* 4-byte length prefix */
    write(fd, data, len);            /* actual data */
}

void pipe_recv(int fd, char *buf, size_t bufsize) {
    uint32_t nlen;
    read(fd, &nlen, sizeof(nlen));   /* read length first */
    read(fd, buf, nlen);             /* then read exactly that many bytes */
    /* This is fragile — partial reads, blocking, etc. */
}

/* ---- Message Queue approach: automatic framing ---- */
#include <sys/msg.h>

struct packet {
    long mtype;
    char data[256];
};

void mq_send(int msqid, const char *data, long type) {
    struct packet pkt;
    pkt.mtype = type;
    strncpy(pkt.data, data, sizeof(pkt.data));
    msgsnd(msqid, &pkt, strlen(pkt.data) + 1, 0);
    /* No length prefix needed — msgrcv always returns one complete message */
}

void mq_recv(int msqid, char *buf, long type) {
    struct packet pkt;
    msgrcv(msqid, &pkt, sizeof(pkt.data), type, 0);
    strncpy(buf, pkt.data, sizeof(pkt.data));
    /* Automatically got one complete message, nothing more, nothing less */
}
TLPI Advice: The text explicitly states that new applications should avoid System V message queues in favor of FIFOs, POSIX message queues (mq_open), or sockets, because these alternatives offer better integration with I/O multiplexing (select/poll/epoll), better POSIX compliance, and cleaner semantics. System V message queues exist primarily for legacy compatibility.

5. Creating and Opening a Message Queue: msgget()
msgget() IPC_CREAT IPC_EXCL Permissions

The msgget() system call is the entry point for working with message queues. It either creates a new queue or returns the identifier of an existing one, depending on the key and flags.

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgget(key_t key, int msgflg);
/* Returns: message queue identifier on success, -1 on error */
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

int main(void) {
    key_t key;
    int msqid;

    /* Step 1: Generate a key from an existing file */
    key = ftok("/tmp", 'M');
    if (key == -1) { perror("ftok"); exit(EXIT_FAILURE); }

    /* Step 2: Create a new message queue */
    /* S_IRUSR | S_IWUSR = owner can read and write (0600) */
    msqid = msgget(key, IPC_CREAT | IPC_EXCL | S_IRUSR | S_IWUSR);
    if (msqid == -1) {
        if (errno == EEXIST) {
            printf("Queue already exists. Opening existing queue...\n");
            /* Open existing queue (no IPC_CREAT, no IPC_EXCL) */
            msqid = msgget(key, 0);
            if (msqid == -1) { perror("msgget open"); exit(EXIT_FAILURE); }
        } else {
            perror("msgget create");
            exit(EXIT_FAILURE);
        }
    }

    printf("Message queue ID: %d\n", msqid);

    /*
     * Verify with shell:
     *   ipcs -q
     * Should show a new entry with the msqid above.
     *
     * Delete the queue when done:
     *   ipcrm -q 
     */

    return 0;
}
Flags Summary:

  • IPC_CREAT — create queue if it does not exist
  • IPC_EXCL — fail with EEXIST if queue already exists (used with IPC_CREAT)
  • Permission bits — lower 9 bits are the read/write/execute mask (execute is ignored for IPC)
  • No flags / 0 — open existing queue; fail with ENOENT if it does not exist

6. Complete Example: Sending and Receiving Messages
msgsnd() msgrcv() msgctl IPC_RMID End-to-End Flow

The following end-to-end example shows a parent creating a queue, a child sending messages of different types, and the parent receiving them selectively by type — demonstrating the core message queue workflow.

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <wait.h>

#define MSG_TYPE_PING  1
#define MSG_TYPE_DATA  2
#define MSG_TYPE_DONE  3

struct msg_buf {
    long mtype;
    char text[128];
};

int main(void) {
    int msqid;
    struct msg_buf msg;
    pid_t pid;

    /* Create a private message queue (IPC_PRIVATE = always new, unique) */
    msqid = msgget(IPC_PRIVATE, IPC_CREAT | 0600);
    if (msqid == -1) { perror("msgget"); exit(1); }
    printf("Created queue, msqid = %d\n", msqid);

    pid = fork();
    if (pid == -1) { perror("fork"); exit(1); }

    if (pid == 0) {
        /* ===== CHILD: sends messages ===== */

        msg.mtype = MSG_TYPE_PING;
        strcpy(msg.text, "Are you alive?");
        msgsnd(msqid, &msg, strlen(msg.text)+1, 0);

        msg.mtype = MSG_TYPE_DATA;
        strcpy(msg.text, "Temperature=36.5");
        msgsnd(msqid, &msg, strlen(msg.text)+1, 0);

        msg.mtype = MSG_TYPE_DATA;
        strcpy(msg.text, "Humidity=72%");
        msgsnd(msqid, &msg, strlen(msg.text)+1, 0);

        msg.mtype = MSG_TYPE_DONE;
        strcpy(msg.text, "All done.");
        msgsnd(msqid, &msg, strlen(msg.text)+1, 0);

        printf("Child: sent 4 messages\n");
        exit(0);
    }

    /* ===== PARENT: receives messages selectively ===== */
    wait(NULL);  /* wait for child to finish sending */

    /* Read only DATA messages first */
    printf("\n--- Receiving DATA messages ---\n");
    while (msgrcv(msqid, &msg, sizeof(msg.text), MSG_TYPE_DATA, IPC_NOWAIT) != -1)
        printf("DATA: %s\n", msg.text);

    /* Now read the PING */
    printf("\n--- Receiving PING ---\n");
    if (msgrcv(msqid, &msg, sizeof(msg.text), MSG_TYPE_PING, 0) != -1)
        printf("PING: %s\n", msg.text);

    /* Read the DONE signal */
    printf("\n--- Receiving DONE ---\n");
    if (msgrcv(msqid, &msg, sizeof(msg.text), MSG_TYPE_DONE, 0) != -1)
        printf("DONE: %s\n", msg.text);

    /* Delete the queue */
    if (msgctl(msqid, IPC_RMID, NULL) == -1)
        perror("msgctl IPC_RMID");
    else
        printf("\nQueue deleted.\n");

    /*
     * Expected output:
     * Created queue, msqid = 
     * Child: sent 4 messages
     *
     * --- Receiving DATA messages ---
     * DATA: Temperature=36.5
     * DATA: Humidity=72%
     *
     * --- Receiving PING ---
     * PING: Are you alive?
     *
     * --- Receiving DONE ---
     * DONE: All done.
     *
     * Queue deleted.
     *
     * Notice: DATA messages were read BEFORE PING even though PING
     * was sent first — that is type-based selective retrieval.
     */
    return 0;
}
IPC_NOWAIT flag: When passed to msgrcv(), if no message of the requested type exists, the call returns immediately with errno = ENOMSG instead of blocking. This is used in the loop above to drain all DATA messages without hanging.

7. Limitations — Why New Code Should Prefer Alternatives
No select/poll/epoll POSIX MQ Sockets FIFOs

TLPI (Section 46.9, previewed here) lists several reasons why System V message queues are considered a legacy mechanism:

System V MQ Limitations vs Modern Alternatives
Limitation Impact Alternative
Cannot use with select / poll / epoll Cannot multiplex queue I/O with other fds in an event loop POSIX MQ (supports poll), sockets, FIFOs
IPC identifier, not a file descriptor Incompatible with standard UNIX I/O tools and libraries POSIX MQ returns mqd_t (file-descriptor-like)
Kernel persistence (no auto-cleanup) Leaked queues consume kernel resources even after all processes exit Pipes / sockets are auto-cleaned; POSIX MQ can be unlinked
Fixed system-wide limits msgmni / msgmax / msgmnb require tuning via /proc for high-throughput apps Sockets have more flexible buffering
Complex API msgsnd/msgrcv need a struct with long mtype as first field — error-prone POSIX MQ has cleaner mq_send / mq_receive interface
/*
 * POSIX message queue (modern alternative) — for comparison
 * Compile with: gcc -o posix_mq posix_mq.c -lrt
 */
#include <mqueue.h>
#include <stdio.h>
#include <string.h>
#include <fcntl.h>

int main(void) {
    mqd_t mq;
    char buf[256];

    /* Create or open a POSIX message queue */
    mq = mq_open("/my_queue", O_CREAT | O_RDWR, 0600, NULL);
    /* mq is a file-descriptor-like handle — works with poll/select! */

    /* Send a message (no struct, no mtype boilerplate) */
    mq_send(mq, "hello world", 11, 1 /* priority */);

    /* Receive a message */
    unsigned prio;
    mq_receive(mq, buf, sizeof(buf), &prio);
    printf("Got: %s (priority %u)\n", buf, prio);

    mq_close(mq);
    mq_unlink("/my_queue");  /* Remove from filesystem — auto-cleanup */
    return 0;
}
/*
 * Key differences from System V:
 *   - Named by path (/my_queue) not by integer key
 *   - mq_unlink() removes it like a file (no lingering IPC objects)
 *   - mqd_t works with poll()/select() for event-driven I/O
 *   - Priority replaces type — always highest priority first
 */

Interview Questions — System V Message Queues Introduction

Q1. What is the fundamental difference between a pipe and a System V message queue in terms of communication model?
A pipe is a byte stream — there are no message boundaries and the reader can read any number of bytes at a time. A System V message queue is message-oriented — each msgsnd() writes one complete, atomic message and each msgrcv() reads exactly one complete message. Boundaries are preserved automatically by the kernel.
Q2. The handle for a message queue is called an IPC identifier. How does it differ from a file descriptor?
A file descriptor is a per-process attribute — it exists only within the process that opened the file. An IPC identifier is a property of the object itself and is visible system-wide. All processes accessing the same message queue use the same identifier. Also, a file descriptor is closed automatically when the process exits, whereas an IPC identifier persists until msgctl(IPC_RMID) is called or the system reboots.
Q3. Explain the three ways the mtype argument to msgrcv() can be used to select messages.
(1) mtype = 0: returns the first message in the queue in FIFO order, ignoring type. (2) mtype > 0: returns the first message with exactly that type value. (3) mtype < 0: returns the first message whose type is less than or equal to the absolute value of mtype (lowest type number first — useful for priority queues).
Q4. Why must the mtype field be the first member of the message structure passed to msgsnd()/msgrcv(), and what constraint applies to its value?
The kernel expects the first field of the buffer to be a long integer representing the type. This is a fixed interface contract — the kernel reads the type from offset 0 of the buffer. The mtype value must be strictly greater than 0; a value of 0 or negative in msgsnd() causes an EINVAL error.
Q5. What does the IPC_NOWAIT flag do when passed to msgrcv()?
Normally, if no message of the requested type is present, msgrcv() blocks until one arrives. With IPC_NOWAIT, the call returns immediately with -1 and errno = ENOMSG if no matching message exists. This enables non-blocking polling of a message queue.
Q6. Why does TLPI recommend against using System V message queues in new applications?
The primary reasons are: (1) IPC identifiers are not file descriptors, so they cannot be used with select()/poll()/epoll() for event-driven I/O; (2) kernel persistence means leaked queues consume system resources; (3) fixed system-wide limits require manual tuning; (4) the struct-with-long-mtype API is error-prone. POSIX message queues, FIFOs, and sockets are cleaner, more portable alternatives.
Q7. How can multiple clients share one message queue and still receive only their own replies from a server?
The server reads requests with mtype = MSG_TYPE_REQUEST. Each client includes its PID in the request payload. The server then sends the reply with mtype = client_pid. Each client calls msgrcv(msqid, &msg, len, getpid(), 0) to receive only the reply addressed to its own PID, ignoring all other messages on the queue.
Q8. What happens if you call msgrcv() with a buffer smaller than the message waiting on the queue?
By default, msgrcv() returns -1 with errno = E2BIG. The message remains on the queue untouched. If the MSG_NOERROR flag is passed, msgrcv() silently truncates the message to fit the buffer and removes the (truncated) message from the queue — the remaining data is permanently lost.

Chapter 45 Complete — Continue to Chapter 46

You have now covered all of Chapter 45: System V IPC overview, keys, permissions, client-server patterns, identifier algorithm, ipcs/ipcrm tools, /proc/sysvipc, IPC limits, and this message queue introduction. Chapter 46 dives deep into msgsnd(), msgrcv(), msgctl(), queue attributes, and limits.

← Previous: /proc/sysvipc & IPC Limits Next: Ch46 Message Queues Deep Dive →

Leave a Reply

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