Descriptors & Message Queues How mqd_t maps to kernel data structures

 

52.3 – Descriptors & Message Queues
How mqd_t maps to kernel data structures  |  Chapter 52 – POSIX Message Queues

What Is a Message Queue Descriptor?

When a process calls mq_open(), it gets back a message queue descriptor of type mqd_t. This is a per-process handle — similar to how open() returns a file descriptor (int fd).

Just as file descriptors point into a system-wide table of open file descriptions, message queue descriptors point into a system-wide table of open message queue descriptions. Each description in that table points to the actual message queue object.

This three-level structure — descriptor → description → queue object — is the same mental model you already know from file I/O. Understanding it is essential to reasoning about what happens after fork() or when two processes independently open the same queue.

Kernel Data Structure Layout

The diagram below shows how two processes (A and B) relate to open message queue descriptions and the final queue objects. This mirrors Figure 52-1 from TLPI.

Process A
MQ Descriptor Table
Open MQ Description Table
(system-wide)
Message Queue Table
(system-wide)
Queue Names
x
other info | ptr→desc
flags: O_NONBLOCK?
ptr → MQ object
← shared by A.x and B.x
(same description, from fork)
MQ Attributes
UID & GID
Notification settings
Message data
/mq-p
Process B – x
inherited via fork()
z (Process A)
independent open
flags: O_NONBLOCK?
ptr → /mq-r
Separate from B’s description
MQ Attributes
UID & GID
Notification settings
Message data
/mq-r
(same queue,
two descriptions)
y (Process B)
independent open
flags: O_NONBLOCK?
ptr → /mq-r
Separate from A’s description

Three Key Observations

1. The O_NONBLOCK Flag Lives in the Description, Not the Queue

Each open message queue description carries its own copy of the O_NONBLOCK flag. This flag controls whether mq_send() and mq_receive() block when the queue is full or empty, respectively.

Changing O_NONBLOCK via mq_setattr() on one descriptor affects only that descriptor’s description — not other descriptors pointing to different descriptions of the same queue.

2. fork() Produces Shared Descriptions (Like dup())

When a process opens a queue and then calls fork(), the child inherits a copy of the parent’s descriptor table. Both parent and child now have descriptors pointing to the same open message queue description. This is exactly like what happens with file descriptors after fork().

Because they share the same description, they share the same O_NONBLOCK state. If either one changes it via mq_setattr(), the other sees the change.

3. Two Independent Opens → Two Separate Descriptions → Same Queue

If Process A and Process B each call mq_open("/mq-r", ...) independently, each gets a descriptor pointing to its own open message queue description. Both descriptions ultimately refer to the same /mq-r queue object — but the descriptions are independent.

This means Process A and Process B have independent O_NONBLOCK flags. Changing the flag in one process does not affect the other.

Linux Implementation Note

On Linux, POSIX message queues are implemented using a special virtual filesystem (usually mounted at /dev/mqueue). Each queue is an i-node in that filesystem. Message queue descriptors are implemented internally as file descriptors, and open message queue descriptions are implemented as open file descriptions. This is why all the file descriptor rules (fork sharing, dup(), etc.) apply to message queue descriptors too. However, this is a Linux-specific detail — SUSv3 does not require this, and other UNIX systems may implement it differently.

You can actually list your open queues from the shell:

$ ls /dev/mqueue
/mq-hello   /mq-data

Code Examples

Example 1: Open a Queue and Unlink It

mq_open() creates or opens a queue. mq_unlink() removes the queue name from the system (like unlink() for files). The queue is destroyed only after all descriptors referring to it are closed.

#include <mqueue.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>

int main(void)
{
    mqd_t mqd;

    /* Create a new queue with read/write access.
       O_CREAT | O_EXCL: fail if already exists. */
    mqd = mq_open("/my_queue",
                  O_CREAT | O_EXCL | O_RDWR,
                  S_IRUSR | S_IWUSR,   /* owner read/write */
                  NULL);               /* use default attributes */

    if (mqd == (mqd_t)-1) {
        perror("mq_open");
        exit(EXIT_FAILURE);
    }

    printf("Queue opened, descriptor value = %d\n", (int)mqd);

    /* Close our descriptor */
    if (mq_close(mqd) == -1) {
        perror("mq_close");
        exit(EXIT_FAILURE);
    }

    /* Remove the queue name from the system.
       The queue is destroyed once all descriptors are closed. */
    if (mq_unlink("/my_queue") == -1) {
        perror("mq_unlink");
        exit(EXIT_FAILURE);
    }

    printf("Queue unlinked successfully.\n");
    return 0;
}

/* Compile: gcc -o mq_demo mq_demo.c -lrt */

Example 2: fork() and Shared Message Queue Descriptor

After fork(), parent and child share the same open message queue description. They share the O_NONBLOCK flag state, just like shared file descriptions.

#include <mqueue.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/wait.h>

int main(void)
{
    mqd_t mqd;
    pid_t pid;
    char buf[256];
    unsigned int prio;

    /* Parent opens the queue BEFORE fork */
    mqd = mq_open("/shared_q", O_CREAT | O_RDWR,
                  S_IRUSR | S_IWUSR, NULL);
    if (mqd == (mqd_t)-1) { perror("mq_open"); exit(1); }

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

    if (pid == 0) {
        /* --- CHILD: receives a message --- */
        printf("[Child] waiting to receive...\n");
        ssize_t n = mq_receive(mqd, buf, 256, &prio);
        if (n == -1) { perror("mq_receive"); exit(1); }
        buf[n] = '\0';
        printf("[Child] received: '%s' (prio=%u)\n", buf, prio);
        mq_close(mqd);
        exit(0);
    } else {
        /* --- PARENT: sends a message --- */
        const char *msg = "Hello from parent";
        printf("[Parent] sending message...\n");
        if (mq_send(mqd, msg, 17, 1) == -1) {
            perror("mq_send"); exit(1);
        }
        wait(NULL);           /* wait for child */
        mq_close(mqd);
        mq_unlink("/shared_q");
        printf("[Parent] done.\n");
    }

    return 0;
}
/* NOTE: Both parent and child share the SAME open description.
   Descriptor mqd in child is a copy pointing to same description. */

Example 3: Independent Opens → Separate Descriptions

Two separate calls to mq_open() for the same queue name produce two distinct open descriptions. Each has its own O_NONBLOCK flag.

#include <mqueue.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>

int main(void)
{
    mqd_t mqd1, mqd2;
    struct mq_attr attr1, attr2;

    /* First independent open */
    mqd1 = mq_open("/same_queue", O_CREAT | O_RDWR,
                   S_IRUSR | S_IWUSR, NULL);
    if (mqd1 == (mqd_t)-1) { perror("mq_open #1"); exit(1); }

    /* Second independent open of the SAME queue name */
    mqd2 = mq_open("/same_queue", O_RDWR, 0, NULL);
    if (mqd2 == (mqd_t)-1) { perror("mq_open #2"); exit(1); }

    /* Set O_NONBLOCK on mqd1's description only */
    mq_getattr(mqd1, &attr1);
    attr1.mq_flags |= O_NONBLOCK;
    mq_setattr(mqd1, &attr1, NULL);

    /* Check mqd2 — it still has its OWN description, flag unchanged */
    mq_getattr(mqd2, &attr2);
    printf("mqd1 O_NONBLOCK: %s\n",
           (attr1.mq_flags & O_NONBLOCK) ? "SET" : "NOT SET");
    printf("mqd2 O_NONBLOCK: %s\n",
           (attr2.mq_flags & O_NONBLOCK) ? "SET" : "NOT SET");
    /* Output:
       mqd1 O_NONBLOCK: SET
       mqd2 O_NONBLOCK: NOT SET
       --> Two independent descriptions, same queue */

    mq_close(mqd1);
    mq_close(mqd2);
    mq_unlink("/same_queue");
    return 0;
}

Quick Reference: Descriptor vs Description vs Queue

Concept Scope What It Holds Analogy
mqd_t (descriptor) Per-process Pointer into open description table File descriptor (int fd)
Open MQ description System-wide mq_flags (O_NONBLOCK), ptr to queue Open file description
Message queue object System-wide Attributes, UID/GID, messages, notify i-node / file on disk

Interview Questions & Answers

Q1. What does mq_open() return and what does it represent?

A: mq_open() returns a value of type mqd_t, called a message queue descriptor. It is a per-process handle that refers to an entry in the system-wide table of open message queue descriptions. It is analogous to a file descriptor returned by open(). On Linux it is implemented as a file descriptor internally, but this is implementation-specific.

Q2. How does the relationship between mqd_t, open descriptions, and queue objects work?

A: There are three levels:

  1. Each process has a per-process descriptor table. A mqd_t indexes into this table.
  2. Each entry points to a system-wide open message queue description, which holds the O_NONBLOCK flag and a reference count.
  3. Each description points to a message queue object (the actual queue with its messages and attributes).

This is identical to the three-level model used for file descriptors in POSIX.

Q3. What happens to message queue descriptors after fork()?

A: After fork(), the child inherits copies of all the parent’s message queue descriptors. Both parent and child descriptors point to the same open message queue description — just like inherited file descriptors share the same open file description. Therefore they share the same O_NONBLOCK flag; if one changes it with mq_setattr(), the other sees the change.

Q4. If two processes independently call mq_open() on the same queue name, do they share an open description?

A: No. Each independent call to mq_open() creates a new open message queue description. Both descriptions point to the same underlying queue object, but each has its own copy of mq_flags (O_NONBLOCK). Changing the flag through one descriptor does not affect the other. This is analogous to two processes independently calling open() on the same file — they get separate open file descriptions.

Q5. Where is O_NONBLOCK stored — in the descriptor, in the description, or in the queue?

A: O_NONBLOCK is stored in the open message queue description (the system-wide entry), via the mq_flags field. It is not in the per-process descriptor itself, and not in the queue object. This means descriptors that share the same description (e.g., after fork()) share the same flag, while descriptors with separate descriptions for the same queue have independent flags.

Q6. How is the Linux POSIX message queue implementation different from other UNIX systems?

A: On Linux, POSIX message queues are implemented as i-nodes in a virtual filesystem (mounted at /dev/mqueue). Message queue descriptors are real file descriptors internally, and open descriptions are open file descriptions. This allows Linux-specific features like using select(), poll(), and epoll() on message queue descriptors (covered in Section 52.7). This implementation detail is not required by SUSv3, and other UNIX systems may not support it.

Q7. What is the difference between mq_close() and mq_unlink()?

A: mq_close() closes a message queue descriptor in the calling process, releasing the per-process reference. It does not destroy the queue — other processes can still use it, and messages remain. mq_unlink() removes the queue’s name from the system (like unlink() for files). The queue is actually destroyed only after all processes have closed all their descriptors for it (reference count reaches zero).

Leave a Reply

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