System V Message Queues Receiving Messages with msgrcv()

 

System V Message Queues
Part 3 — Receiving Messages with msgrcv() | TLPI Chapter 46
46.2.2
Section
msgrcv()
System Call
3
Code Examples
18+
Interview Q&A

Key Concepts in This Tutorial
msgrcv() msgtyp maxmsgsz IPC_NOWAIT MSG_NOERROR MSG_EXCEPT E2BIG ENOMSG Priority Queue Selective Receive Message Type Filtering Read Permission

What Is msgrcv()?

msgrcv() is the system call used to read a message from a System V message queue. When a message is read, it is automatically removed from the queue. This is different from reading a shared memory segment where data persists after reading.

What makes msgrcv() powerful is selective receive — you can choose which message to read based on its type. You don’t have to read messages in the order they were sent. This gives message queues an advantage over pipes, where you must always read bytes in FIFO (first-in, first-out) order.

For example, in a server that handles both urgent and normal requests, it can always read urgent messages first, regardless of when they were sent. This is not possible with pipes.

The msgrcv() System Call — Signature

Here is the official function signature:

#include <sys/types.h>   /* For portability */
#include <sys/msg.h>

ssize_t msgrcv(int msqid, void *msgp, size_t maxmsgsz, long msgtyp, int msgflg);

/* Returns: number of bytes in mtext on success, -1 on error */

Every argument explained:

Argument Type Meaning
msqid int Message queue identifier from msgget().
msgp void * Pointer to a buffer where the received message will be stored. Must start with a long mtype field.
maxmsgsz size_t Maximum number of bytes that can fit in the mtext field of your buffer. Protects against buffer overflow.
msgtyp long Controls which message to receive (0 = any, >0 = specific type, <0 = priority queue). See details below.
msgflg int Flags: 0 (blocking), IPC_NOWAIT (non-blocking), MSG_NOERROR (truncate), MSG_EXCEPT (invert type match).
Return Value: On success, returns the number of bytes actually copied into the mtext field. On error, returns -1 and sets errno. Note: the return value does NOT include the mtype field.

The msgtyp Argument — Three Different Behaviors

This is the most important and conceptually rich part of msgrcv(). The msgtyp argument gives you three different ways to select which message to receive:

msgtyp Behavior Summary
msgtyp == 0
Receive the first message in the queue, regardless of its type.

This is simple FIFO order — just like a pipe.

Use when: order matters, type doesn’t
msgtyp > 0
Receive the first message whose mtype == msgtyp.

Skips all messages of different types. Blocks if no matching message exists.

Use when: you want a specific message type
msgtyp < 0
Priority queue mode: Receive the message with the lowest mtype value that is ≤ |msgtyp|.

Lower type number = higher priority.

Use when: priority-based processing needed

Understanding msgtyp < 0 — Priority Queue Example

This is the trickiest part. Let’s trace through a concrete example from the textbook. Suppose the message queue contains these messages in this order:

Queue State — Messages in Arrival Order
Queue Position mtype mtext
1 (oldest) 300 Message A
2 100 Message B
3 200 Message C
4 400 Message D
5 (newest) 100 Message E
Call: msgrcv(id, &msg, maxsz, -300, 0)
This means: “give me the message with the lowest mtype that is ≤ 300”

Retrieval order:
Call 1 → mtype 100 (position 2, Message B) — lowest mtype ≤ 300
Call 2 → mtype 100 (position 5, Message E) — next lowest mtype ≤ 300
Call 3 → mtype 200 (position 3, Message C) — next lowest mtype ≤ 300
Call 4 → mtype 300 (position 1, Message A) — next lowest mtype ≤ 300
Call 5 → BLOCKS — only Message D (type 400) remains, and 400 > 300

Key Insight: When msgtyp < 0, the queue behaves like a priority queue where lower mtype value = higher priority. This allows you to implement priority-based message processing without any additional data structures — the kernel handles the ordering for you.

The msgflg Argument — All Three Flags Explained

The msgflg argument to msgrcv() supports three flags. These can be ORed together:

1. IPC_NOWAIT — Non-Blocking Receive

By default, if no message matching msgtyp is in the queue, msgrcv() blocks the calling process until such a message arrives. IPC_NOWAIT changes this: instead of blocking, the call returns immediately with -1 and errno = ENOMSG.

Why ENOMSG and not EAGAIN? With non-blocking writes (pipes, msgsnd), the error is EAGAIN. But for msgrcv with IPC_NOWAIT, the error is ENOMSG (“no message of desired type”). This is historical behavior required by SUSv3. This inconsistency is a classic exam/interview trap.
2. MSG_NOERROR — Truncate Oversized Messages

If the message body (mtext) is larger than maxmsgsz bytes, the default behavior is to fail with errno = E2BIG and leave the message in the queue. When MSG_NOERROR is specified, the excess bytes are silently truncated and the (truncated) message IS removed from the queue. The truncated data is permanently lost.

Data Loss Warning: MSG_NOERROR silently discards data. Only use it when you explicitly know that message truncation is acceptable for your application.
3. MSG_EXCEPT — Inverse Type Match (Linux-Specific)

This flag is Linux-specific (not in POSIX). It only has effect when msgtyp > 0. It inverts the usual type match — instead of receiving the first message whose mtype == msgtyp, it receives the first message whose mtype is NOT equal to msgtyp.

To use this flag, you must define _GNU_SOURCE before including <sys/msg.h>:

#define _GNU_SOURCE
#include <sys/msg.h>
MSG_EXCEPT Example: Using the example queue from before, calling msgrcv(id, &msg, maxsz, 100, MSG_EXCEPT) repeatedly would retrieve messages in the order: type 300 (pos 1), type 200 (pos 3), type 400 (pos 4) — then block. Messages of type 100 are excluded.

Flag Effect Error When Condition Not Met Portability
0 (no flag) Block until matching message arrives EINTR (signal) All UNIX
IPC_NOWAIT Return immediately if no match ENOMSG POSIX
MSG_NOERROR Truncate if message too large — (data silently lost) POSIX
MSG_EXCEPT Receive first message NOT matching msgtyp ENOMSG (with IPC_NOWAIT) Linux only

The E2BIG Error — What Happens When Message Is Too Large?

When you call msgrcv(), you provide a maxmsgsz argument telling the kernel how many bytes your buffer can hold. If the queued message’s mtext is larger than this:

Without MSG_NOERROR (default)
• msgrcv() returns -1
• errno = E2BIG
• Message STAYS in the queue
• No data is copied to your buffer
• You must use a larger buffer to read it
With MSG_NOERROR
• msgrcv() succeeds (returns > 0)
• Message IS removed from queue
• Only maxmsgsz bytes are copied
• Excess bytes are DISCARDED forever
• Returns maxmsgsz (truncated count)

Signal Interruption of Blocked msgrcv()

Just like msgsnd(), a blocked msgrcv() call can be interrupted by a signal. When this happens, the call returns -1 with errno = EINTR, regardless of whether SA_RESTART was set in the signal handler.

This is consistent with msgsnd(). Both System V message queue calls are never auto-restarted. The programmer must always check for EINTR and retry.

Reading Permission: Receiving a message requires read permission on the queue. If the process lacks read permission, msgrcv() fails with errno = EACCES.

Coding Example 1 — Basic Message Receive (All Types)

This example receives any message from the queue (msgtyp = 0) and prints its content. Must be run after the sender from the previous tutorial has placed messages.

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <errno.h>

#define MAX_MTEXT 256

struct my_msg {
    long mtype;
    char mtext[MAX_MTEXT];
};

int main(void)
{
    int msqid;
    struct my_msg msg;
    key_t key;
    ssize_t bytes_received;

    /* Get the same key as the sender */
    key = ftok("/tmp", 'A');
    if (key == -1) { perror("ftok"); exit(EXIT_FAILURE); }

    /* Access the existing queue — no IPC_CREAT since sender created it */
    msqid = msgget(key, 0666);
    if (msqid == -1) {
        perror("msgget — make sure sender created the queue first");
        exit(EXIT_FAILURE);
    }
    printf("Connected to queue ID: %d\n", msqid);

    /*
     * msgtyp = 0 means receive the FIRST message regardless of type.
     * This is simple FIFO order, just like a pipe.
     * msgflg = 0 means blocking — wait if queue is empty.
     */
    printf("Waiting for message (msgtyp=0, blocking)...\n");

    bytes_received = msgrcv(msqid, &msg, MAX_MTEXT, 0, 0);
    if (bytes_received == -1) {
        perror("msgrcv");
        exit(EXIT_FAILURE);
    }

    printf("Received %zd bytes\n", bytes_received);
    printf("  mtype = %ld\n", msg.mtype);
    printf("  mtext = \"%s\"\n", msg.mtext);

    return 0;
}

/* Compile: gcc -o basic_recv basic_recv.c
   Run sender first, then: ./basic_recv */

Coding Example 2 — Selective Receive by Message Type

This example demonstrates all three msgtyp behaviors in one program — receiving specific types, any type, and priority-based reception.

#define _GNU_SOURCE  /* Required for MSG_EXCEPT */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <errno.h>

#define MAX_MTEXT 128

struct my_msg {
    long mtype;
    char mtext[MAX_MTEXT];
};

/* Helper: send a message */
void send_msg(int msqid, long type, const char *text)
{
    struct my_msg m;
    m.mtype = type;
    strncpy(m.mtext, text, MAX_MTEXT - 1);
    if (msgsnd(msqid, &m, strlen(m.mtext) + 1, 0) == -1)
        perror("msgsnd");
    else
        printf("  [SENT] type=%ld text=\"%s\"\n", type, text);
}

/* Helper: receive a message with given msgtyp and flags */
void recv_msg(int msqid, long msgtyp, int flags, const char *label)
{
    struct my_msg m;
    ssize_t n;

    n = msgrcv(msqid, &m, MAX_MTEXT, msgtyp, flags | IPC_NOWAIT);
    if (n == -1) {
        if (errno == ENOMSG)
            printf("  [%s] No matching message (ENOMSG)\n", label);
        else
            perror("msgrcv");
    } else {
        printf("  [%s] Got: type=%ld text=\"%s\" (%zd bytes)\n",
               label, m.mtype, m.mtext, n);
    }
}

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

    key = ftok("/tmp", 'D');
    if (key == -1) { perror("ftok"); exit(EXIT_FAILURE); }

    msqid = msgget(key, IPC_CREAT | 0666);
    if (msqid == -1) { perror("msgget"); exit(EXIT_FAILURE); }

    printf("=== Populating queue with messages ===\n");
    send_msg(msqid, 300, "Priority-LOW");
    send_msg(msqid, 100, "Priority-HIGH (first)");
    send_msg(msqid, 200, "Priority-MED");
    send_msg(msqid, 400, "Priority-VERY-LOW");
    send_msg(msqid, 100, "Priority-HIGH (second)");

    printf("\n=== Test 1: msgtyp=0 (receive first in queue, FIFO) ===\n");
    recv_msg(msqid, 0, 0, "FIFO");

    printf("\n=== Rebuild queue ===\n");
    send_msg(msqid, 300, "Priority-LOW");
    send_msg(msqid, 100, "Priority-HIGH (A)");
    send_msg(msqid, 200, "Priority-MED");
    send_msg(msqid, 400, "Priority-VERY-LOW");
    send_msg(msqid, 100, "Priority-HIGH (B)");

    printf("\n=== Test 2: msgtyp=100 (receive only type 100) ===\n");
    recv_msg(msqid, 100, 0, "TYPE=100");
    recv_msg(msqid, 100, 0, "TYPE=100");
    recv_msg(msqid, 100, 0, "TYPE=100"); /* Should get ENOMSG */

    printf("\n=== Test 3: msgtyp=-300 (priority queue, lowest type ≤ 300) ===\n");
    /* Remaining: type 300, 200, 400 */
    recv_msg(msqid, -300, 0, "PRIO -300 #1");
    recv_msg(msqid, -300, 0, "PRIO -300 #2");
    recv_msg(msqid, -300, 0, "PRIO -300 #3"); /* 400 > 300, so ENOMSG */

    printf("\n=== Test 4: MSG_EXCEPT with msgtyp=400 (all except type 400) ===\n");
    recv_msg(msqid, 400, MSG_EXCEPT, "EXCEPT-400");

    printf("\n=== Cleanup ===\n");
    msgctl(msqid, IPC_RMID, NULL);
    printf("Queue removed.\n");

    return 0;
}

/* Compile: gcc -o selective_recv selective_recv.c
   Run:    ./selective_recv */

Coding Example 3 — Handling Oversized Messages (E2BIG vs MSG_NOERROR)

This example sends a large message and demonstrates the difference between the default behavior (E2BIG, message stays in queue) and MSG_NOERROR (truncate and remove).

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

/* Sender uses a large buffer */
#define SEND_SIZE 200

/* Receiver uses a small buffer — intentionally smaller */
#define RECV_SIZE 50

struct large_msg {
    long mtype;
    char mtext[SEND_SIZE];
};

struct small_buf {
    long mtype;
    char mtext[RECV_SIZE];
};

int main(void)
{
    int msqid;
    key_t key;
    struct large_msg sm;
    struct small_buf rb;
    ssize_t n;

    key = ftok("/tmp", 'E');
    if (key == -1) { perror("ftok"); exit(EXIT_FAILURE); }

    msqid = msgget(key, IPC_CREAT | 0666);
    if (msqid == -1) { perror("msgget"); exit(EXIT_FAILURE); }

    /* Send a message larger than our receive buffer */
    sm.mtype = 1;
    memset(sm.mtext, 'X', SEND_SIZE - 1);
    sm.mtext[SEND_SIZE - 1] = '\0';
    printf("Sending %d-byte message: \"%.*s...\"\n",
           SEND_SIZE, 20, sm.mtext);

    if (msgsnd(msqid, &sm, SEND_SIZE, 0) == -1) {
        perror("msgsnd"); exit(EXIT_FAILURE);
    }

    /* --- Attempt 1: Default (no MSG_NOERROR) --- */
    printf("\n--- Receive attempt 1: small buffer (%d bytes), no MSG_NOERROR ---\n",
           RECV_SIZE);
    n = msgrcv(msqid, &rb, RECV_SIZE, 0, IPC_NOWAIT);
    if (n == -1) {
        if (errno == E2BIG) {
            printf("ERROR: E2BIG — message too large for buffer!\n");
            printf("Message is still in the queue (not removed).\n");
        } else {
            perror("msgrcv");
        }
    }

    /* --- Attempt 2: With MSG_NOERROR — truncate and remove --- */
    printf("\n--- Receive attempt 2: same small buffer, WITH MSG_NOERROR ---\n");
    n = msgrcv(msqid, &rb, RECV_SIZE, 0, IPC_NOWAIT | MSG_NOERROR);
    if (n == -1) {
        perror("msgrcv");
    } else {
        printf("Success: received %zd bytes (truncated from %d)\n", n, SEND_SIZE);
        printf("mtype = %ld\n", rb.mtype);
        printf("mtext (first %d bytes) = \"%.*s\"\n",
               RECV_SIZE, RECV_SIZE - 1, rb.mtext);
        printf("Remaining %d bytes were DISCARDED.\n",
               SEND_SIZE - RECV_SIZE);
    }

    /* Verify queue is now empty */
    n = msgrcv(msqid, &rb, RECV_SIZE, 0, IPC_NOWAIT);
    if (n == -1 && errno == ENOMSG)
        printf("\nQueue is now empty — message was removed by MSG_NOERROR call.\n");

    msgctl(msqid, IPC_RMID, NULL);
    return 0;
}

/* Compile: gcc -o noerror_recv noerror_recv.c
   Run:    ./noerror_recv */

🎓 Interview Questions — msgrcv() and Receiving Messages

These cover the most frequently tested concepts in interviews and university exams.

Q1. What is the return value of msgrcv() and what does it represent?
msgrcv() returns the number of bytes copied into the mtext field of the message buffer on success. It returns -1 on error. Importantly, the return value does NOT include the size of the mtype field — it counts only the message body bytes. This is symmetric with msgsz in msgsnd().
Q2. Explain the three different behaviors of the msgtyp argument in msgrcv().
msgtyp == 0: Receive the first message in the queue regardless of type (FIFO order).
msgtyp > 0: Receive the first message whose mtype field equals msgtyp. Messages of other types are skipped.
msgtyp < 0: Priority queue mode. Receive the message with the lowest mtype value that is ≤ |msgtyp|. If multiple messages have the same lowest type, the earliest one is returned. This allows implementing message priorities where lower type number = higher priority.
Q3. What error does msgrcv() return when no matching message exists and IPC_NOWAIT is set? Why is this surprising?
It returns errno = ENOMSG (“no message of desired type”). This is surprising because the analogous non-blocking operation — msgsnd() with a full queue and IPC_NOWAIT — returns EAGAIN. Similarly, non-blocking reads from pipes and FIFOs return EAGAIN. Using ENOMSG for msgrcv() is historical Unix behavior preserved for compatibility by SUSv3, not a logical design choice.
Q4. What is the E2BIG error in msgrcv() and how does MSG_NOERROR change the behavior?
E2BIG occurs when the message body (mtext) is larger than the receiver’s maxmsgsz buffer. By default (without MSG_NOERROR), msgrcv() fails with E2BIG and the message remains in the queue — nothing is removed or copied.

With MSG_NOERROR, the message IS removed from the queue, but only maxmsgsz bytes are copied to the buffer. The excess bytes beyond maxmsgsz are permanently discarded. The call succeeds and returns maxmsgsz.

Q5. What is MSG_EXCEPT and for which platform is it available?
MSG_EXCEPT is a Linux-specific flag (not in POSIX). It inverts the type matching when msgtyp > 0: instead of receiving the first message with mtype == msgtyp, it receives the first message with mtype ≠ msgtyp. To use it, you must define _GNU_SOURCE before including <sys/msg.h>. It has no effect when msgtyp == 0 or msgtyp < 0.
Q6. How can multiple processes read from the same message queue without interfering with each other?
By using different message types. Each process calls msgrcv() with a different msgtyp value. For example, process A only reads type 1 messages, process B only reads type 2 messages. The sender assigns the appropriate mtype when calling msgsnd(). A useful pattern is to use the receiver’s process ID as the message type — the receiver calls msgrcv(msqid, &msg, maxsz, getpid(), 0) to receive only messages addressed specifically to it.
Q7. How does msgrcv() differ from reading a pipe in terms of message ordering?
A pipe is a strict byte stream — data must be read in exactly the order it was written (FIFO). There is no way to skip or reorder data. A message queue, using msgtyp > 0 or msgtyp < 0, allows selective reading. Messages can be retrieved out of order — the receiver can skip messages of the wrong type and read a later-arriving message first. This is one of the key advantages of message queues over pipes for inter-process communication.
Q8. What permission is needed to call msgrcv()? What error occurs if it’s missing?
Read permission on the message queue. If the calling process does not have read permission on the queue, msgrcv() fails with errno = EACCES. Permissions are set during queue creation with msgget() using standard Unix-style permission bits (owner/group/other, read/write).
Q9. Can msgrcv() be automatically restarted after a signal? Explain.
No. Like msgsnd(), a blocked msgrcv() that is interrupted by a signal always fails with errno = EINTR, regardless of whether the signal handler was set up with SA_RESTART. Both System V message queue system calls are excluded from auto-restart. The programmer must explicitly check for EINTR and retry in a loop.
Q10. Trace the msgrcv(-300, 0) priority queue behavior on the queue: [(pos1,type300), (pos2,type100), (pos3,type200), (pos4,type400), (pos5,type100)]
msgtyp = -300 means “lowest mtype ≤ 300”:
Call 1: Lowest mtype ≤ 300 is 100. First occurrence at pos2 → returns type 100, Message B.
Call 2: Lowest mtype ≤ 300 is still 100. Next at pos5 → returns type 100, Message E.
Call 3: Lowest mtype ≤ 300 is now 200 (pos3) → returns type 200, Message C.
Call 4: Lowest mtype ≤ 300 is now 300 (pos1) → returns type 300, Message A.
Call 5: Only type 400 remains. 400 > 300 → BLOCKS (or ENOMSG with IPC_NOWAIT).
Q11. What happens to a process blocked in msgrcv() when the queue is deleted?
If the message queue is deleted (via msgctl(msqid, IPC_RMID, NULL)) while a process is blocked in msgrcv() waiting for a message, the blocked call wakes up immediately and returns -1 with errno = EIDRM (identifier removed). The process should handle this error and not attempt further operations on the queue.
Q12. What is the purpose of the maxmsgsz argument? How does it protect the program?
maxmsgsz specifies the maximum number of bytes that the receiver’s mtext buffer can hold. It protects against buffer overflow — if the queued message is larger, the kernel does NOT copy it into your buffer. Instead it either returns E2BIG (default) or truncates with MSG_NOERROR. This is a safety mechanism: unlike pipes where raw bytes flow in, message queues give you a length check before any copy happens.
Q13. How is a message queue’s message type system useful for implementing a request-reply pattern between processes?
A common pattern: The client sends a request message with mtype = server_pid or a command code. In the reply, the server sends back with mtype = client_pid (obtained from the request). The client then calls msgrcv(msqid, &reply, maxsz, getpid(), 0) to receive only replies addressed to it. Multiple clients can share the same queue without receiving each other’s replies, because each client filters by its own PID as the type.
Q14. What are the key differences between System V message queues and POSIX message queues for receiving?
System V uses msgrcv() with type-based selection; POSIX uses mq_receive() with priority-based selection (highest priority first).
POSIX queues support mq_notify() for asynchronous notification when a message arrives — no need to block or poll. System V has no equivalent.
POSIX queues work with select()/poll()/epoll() because they expose a file descriptor. System V queues do not.
System V queues persist until explicitly deleted or system reboot. POSIX queues have similar persistence but are identified by name (like files) and can be unlinked.
Q15. What is the practical use of msgtyp = getpid() in a multi-process server design?
In a request-reply server using a shared message queue, each client process calls msgrcv(msqid, &reply, maxsz, getpid(), 0). The server includes the client’s PID in the reply mtype field. This way, each client only wakes up when its own reply arrives — no client accidentally reads another client’s reply. This is a classic and elegant use of mtype for process-level addressing using a single shared queue, avoiding the need for per-client queues.
Q16. What is the overall lifecycle of a System V message queue in terms of system calls?
Create: msgget(key, IPC_CREAT | perms) — creates or opens queue, returns msqid.
Send: msgsnd(msqid, &msg, msgsz, flags) — places message on queue.
Receive: msgrcv(msqid, &msg, maxsz, msgtyp, flags) — reads and removes message.
Control/Inspect: msgctl(msqid, IPC_STAT, &msqid_ds) — get queue info.
Delete: msgctl(msqid, IPC_RMID, NULL) — remove queue permanently.
Queues persist across process restarts until explicitly deleted or system reboot. Use ipcs -q and ipcrm -q msqid for manual management.
Q17. What is the difference between a pipe and a message queue when the reader is faster than the writer?
With a pipe, a read on an empty pipe blocks until data is written (or returns 0 if all write ends are closed, indicating EOF).
With a message queue, msgrcv() on an empty queue (or a queue with no matching type) blocks until a matching message arrives. With IPC_NOWAIT, it returns immediately with ENOMSG. The key difference: message queues have no EOF concept — they can remain empty indefinitely without signaling “end of data” to the reader.
Q18. Give a scenario where MSG_NOERROR would be dangerous to use.
Any scenario where complete data integrity is required. For example, if a sender puts a binary struct (sensor reading with 10 fields) on the queue and the receiver’s buffer is too small to hold the whole struct, using MSG_NOERROR would silently deliver a truncated struct. The receiver would read partial data — some fields would have garbage/uninitialized values. Without MSG_NOERROR, the E2BIG error would alert the programmer to a buffer size mismatch. MSG_NOERROR is only safe for text data where partial content is acceptable (e.g., log snippets), never for fixed-size binary structures.

✅ Chapter 46 — msgsnd() & msgrcv() Complete

You now understand how to send and receive messages using System V IPC with full theory, examples, and interview readiness.

← Previous: msgsnd() Back to EmbeddedPathashala

Leave a Reply

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