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
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.
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.
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
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 */
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. */
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
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.
A: There are three levels:
- Each process has a per-process descriptor table. A
mqd_tindexes into this table. - Each entry points to a system-wide open message queue description, which holds the
O_NONBLOCKflag and a reference count. - 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.
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.
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.
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.
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.
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).
