The Queue Lifecycle
Before you can send or receive any message, you must open a queue using mq_open(). This gives you a descriptor — a handle — to work with. When done, you close that handle with mq_close(), and when you want to permanently remove the queue, you unlink it with mq_unlink(). These three functions mirror the familiar open(), close(), and unlink() calls for regular files.
Key Terms
The function signature looks like this:
#include <fcntl.h> /* O_* flags */
#include <sys/stat.h> /* mode constants */
#include <mqueue.h>
mqd_t mq_open(const char *name, int oflag, ...
/* mode_t mode, struct mq_attr *attr */);
/* Returns: mqd_t descriptor on success, (mqd_t)-1 on error */
mq_open() is a variadic function. The first two arguments are always required. The third (mode) and fourth (attr) arguments are only needed when you pass O_CREAT in oflag.
/, e.g. "/my_queue". Identifies the queue system-wide.mq_attr struct for queue size settings. Pass NULL for system defaults.The oflag Bit Values
| Flag | Meaning | Notes |
|---|---|---|
O_CREAT |
Create queue if it doesn’t exist | Requires mode and attr arguments |
O_EXCL |
With O_CREAT: fail if queue already exists | Returns EEXIST if queue exists |
O_RDONLY |
Open for receiving only | Can only call mq_receive() |
O_WRONLY |
Open for sending only | Can only call mq_send() |
O_RDWR |
Open for both send and receive | Most flexible option |
O_NONBLOCK |
Non-blocking mode | mq_send/receive fails with EAGAIN instead of blocking |
Use:
O_RDONLY, O_WRONLY, or O_RDWR aloneUse:
O_CREAT | O_RDWR (plus mode and attr args)Use:
O_CREAT | O_EXCL | O_RDWRAdd:
| O_NONBLOCK to any of the aboveExample 1: Create a new queue with custom attributes
#include <stdio.h>
#include <mqueue.h>
#include <fcntl.h>
#include <sys/stat.h>
int main(void)
{
mqd_t mqd;
struct mq_attr attr;
/* Configure queue: hold up to 8 messages, each max 512 bytes */
attr.mq_flags = 0; /* not used by mq_open */
attr.mq_maxmsg = 8; /* max number of messages in queue */
attr.mq_msgsize = 512; /* max size of a single message */
attr.mq_curmsgs = 0; /* not used by mq_open */
/*
* O_CREAT | O_RDWR : create if not exists, open for read+write
* 0644 : owner rw, group r, others r
* &attr : use our custom attributes
*/
mqd = mq_open("/custom_queue", O_CREAT | O_RDWR, 0644, &attr);
if (mqd == (mqd_t)-1) {
perror("mq_open");
return 1;
}
printf("Queue created/opened. Descriptor = %d\n", (int)mqd);
mq_close(mqd);
mq_unlink("/custom_queue");
return 0;
}
Example 2: Open with default attributes (pass NULL for attr)
#include <stdio.h>
#include <mqueue.h>
#include <fcntl.h>
#include <sys/stat.h>
int main(void)
{
mqd_t mqd;
/*
* Pass NULL for attr — kernel uses default values.
* On Linux defaults: mq_maxmsg=10, mq_msgsize=8192
*/
mqd = mq_open("/default_queue", O_CREAT | O_RDWR, 0600, NULL);
if (mqd == (mqd_t)-1) {
perror("mq_open");
return 1;
}
printf("Queue with default attributes created.\n");
mq_close(mqd);
mq_unlink("/default_queue");
return 0;
}
Example 3: Open existing queue for read-only (no O_CREAT)
#include <stdio.h>
#include <mqueue.h>
#include <fcntl.h>
int main(void)
{
mqd_t mqd;
/*
* No O_CREAT — queue must already exist.
* Only two arguments needed when not creating.
*/
mqd = mq_open("/existing_queue", O_RDONLY);
if (mqd == (mqd_t)-1) {
perror("mq_open: queue may not exist");
return 1;
}
printf("Opened existing queue for reading.\n");
mq_close(mqd);
return 0;
}
Example 4: Exclusive creation — fail if queue already exists
#include <stdio.h>
#include <errno.h>
#include <mqueue.h>
#include <fcntl.h>
#include <sys/stat.h>
int main(void)
{
mqd_t mqd;
struct mq_attr attr = { .mq_maxmsg = 5, .mq_msgsize = 256 };
/*
* O_CREAT | O_EXCL: atomically create-or-fail.
* Useful when you need to guarantee you are the creator.
*/
mqd = mq_open("/unique_queue", O_CREAT | O_EXCL | O_RDWR, 0600, &attr);
if (mqd == (mqd_t)-1) {
if (errno == EEXIST) {
printf("Queue already exists — another process created it.\n");
} else {
perror("mq_open");
}
return 1;
}
printf("We are the creator of /unique_queue\n");
mq_close(mqd);
mq_unlink("/unique_queue");
return 0;
}
Example 5: Non-blocking open
#include <stdio.h>
#include <errno.h>
#include <mqueue.h>
#include <fcntl.h>
#include <sys/stat.h>
int main(void)
{
mqd_t mqd;
char buf[256];
struct mq_attr attr = { .mq_maxmsg = 5, .mq_msgsize = 256 };
mqd = mq_open("/nb_queue", O_CREAT | O_RDWR | O_NONBLOCK, 0600, &attr);
if (mqd == (mqd_t)-1) {
perror("mq_open");
return 1;
}
/* Immediate return with EAGAIN if queue is empty — no blocking */
if (mq_receive(mqd, buf, sizeof(buf), NULL) == -1) {
if (errno == EAGAIN) {
printf("Queue is empty — no message available right now.\n");
} else {
perror("mq_receive");
}
}
mq_close(mqd);
mq_unlink("/nb_queue");
return 0;
}
#include <mqueue.h>
int mq_close(mqd_t mqdes);
/* Returns: 0 on success, -1 on error */
mq_close() closes the message queue descriptor mqdes. It is analogous to close() for file descriptors. Important points:
mq_notify() on this descriptor, that registration is automatically cancelled on close.- Closes your descriptor
- Queue still exists in kernel
- Messages are preserved
- Other processes unaffected
- Like closing a file handle
- Removes queue name
- No new opens allowed
- Messages survive until last close
- Queue destroyed at last close
- Like deleting a file
Code Example: Proper open, use, close pattern
#include <stdio.h>
#include <string.h>
#include <mqueue.h>
#include <fcntl.h>
#include <sys/stat.h>
int main(void)
{
mqd_t mqd;
struct mq_attr attr = { .mq_maxmsg = 5, .mq_msgsize = 128 };
mqd = mq_open("/close_demo", O_CREAT | O_RDWR, 0600, &attr);
if (mqd == (mqd_t)-1) {
perror("mq_open");
return 1;
}
mq_send(mqd, "test message", 13, 1);
printf("Message sent. Now closing descriptor.\n");
/* Close descriptor — queue still alive in kernel */
if (mq_close(mqd) == -1) {
perror("mq_close");
return 1;
}
printf("Descriptor closed. Queue still exists.\n");
/* Re-open the same queue — still accessible */
mqd = mq_open("/close_demo", O_RDONLY);
if (mqd == (mqd_t)-1) {
perror("re-open failed");
return 1;
}
char buf[128];
unsigned int prio;
ssize_t n = mq_receive(mqd, buf, sizeof(buf), &prio);
if (n > 0) {
buf[n] = '\0';
printf("Re-opened and received: %s (priority %u)\n", buf, prio);
}
mq_close(mqd);
mq_unlink("/close_demo"); /* NOW remove the queue */
return 0;
}
#include <mqueue.h>
int mq_unlink(const char *name);
/* Returns: 0 on success, -1 on error */
mq_unlink() removes the queue’s name from the system and schedules the queue for destruction. The actual destruction is deferred until the last open descriptor is closed — this is the reference counting mechanism at work.
After mq_unlink(), new calls to mq_open() with the same name will not find the old queue. If a process tries to open it, a fresh queue will be created (or the open will fail if O_CREAT is not specified).
Code Example: mq_unlink basic usage
#include <stdio.h>
#include <mqueue.h>
#include <fcntl.h>
#include <sys/stat.h>
int main(void)
{
struct mq_attr attr = { .mq_maxmsg = 5, .mq_msgsize = 64 };
/* Create queue */
mqd_t mqd = mq_open("/temp_queue", O_CREAT | O_RDWR, 0600, &attr);
if (mqd == (mqd_t)-1) { perror("mq_open"); return 1; }
mq_send(mqd, "data", 5, 1);
/*
* Unlink BEFORE close — safe pattern.
* The name disappears immediately but the queue lives until close.
*/
if (mq_unlink("/temp_queue") == -1) {
perror("mq_unlink");
mq_close(mqd);
return 1;
}
printf("Queue name removed. Data still readable until close.\n");
/* We can still receive because mqd is still open */
char buf[64];
ssize_t n = mq_receive(mqd, buf, sizeof(buf), NULL);
if (n > 0) {
buf[n] = '\0';
printf("Received after unlink: %s\n", buf);
}
/* Last close — queue is now fully destroyed */
mq_close(mqd);
printf("Queue destroyed (last close after unlink).\n");
return 0;
}
Code Example: mq_unlink cleanup in a server process
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <mqueue.h>
#include <fcntl.h>
#include <sys/stat.h>
#define QUEUE_NAME "/server_queue"
static mqd_t g_mqd = (mqd_t)-1;
/* Signal handler: clean up queue on Ctrl+C */
static void cleanup_handler(int sig)
{
if (g_mqd != (mqd_t)-1)
mq_close(g_mqd);
mq_unlink(QUEUE_NAME);
printf("\nQueue cleaned up. Exiting.\n");
exit(0);
}
int main(void)
{
struct mq_attr attr = { .mq_maxmsg = 10, .mq_msgsize = 256 };
signal(SIGINT, cleanup_handler);
/* Remove any stale queue from a previous run */
mq_unlink(QUEUE_NAME); /* Ignore error if it doesn't exist */
g_mqd = mq_open(QUEUE_NAME, O_CREAT | O_RDONLY, 0666, &attr);
if (g_mqd == (mqd_t)-1) { perror("mq_open"); return 1; }
printf("Server listening on %s (press Ctrl+C to quit)...\n", QUEUE_NAME);
char buf[256];
unsigned int prio;
while (1) {
ssize_t n = mq_receive(g_mqd, buf, sizeof(buf), &prio);
if (n > 0) {
buf[n] = '\0';
printf("Received [prio=%u]: %s\n", prio, buf);
}
}
return 0; /* never reached */
}
Understanding what happens to message queue descriptors across fork() and exec() is important for multi-process programs.
- Child gets copies of all parent’s message queue descriptors
- Both parent and child can send/receive on the queue
- Child does NOT inherit the parent’s
mq_notify()registrations - Closing in child does not close parent’s descriptor (they are independent copies)
- All message queue descriptors are closed
- All notification registrations are deregistered
- The queue itself still exists — only descriptors in this process are gone
- New program can re-open the queue by name
- All descriptors closed automatically (same effect as exec)
- All notification registrations removed
- Queue lives on in kernel until unlinked and last descriptor closed
Code Example: fork() with message queues
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <mqueue.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/wait.h>
#define QUEUE_NAME "/fork_demo"
#define MSG_SIZE 128
int main(void)
{
struct mq_attr attr = { .mq_maxmsg = 5, .mq_msgsize = MSG_SIZE };
mqd_t mqd;
pid_t pid;
/* Parent creates queue */
mqd = mq_open(QUEUE_NAME, O_CREAT | O_RDWR, 0666, &attr);
if (mqd == (mqd_t)-1) { perror("mq_open"); return 1; }
pid = fork();
if (pid == -1) { perror("fork"); return 1; }
if (pid == 0) {
/* ---- CHILD: has a copy of mqd ---- */
const char *msg = "Hello from child!";
mq_send(mqd, msg, strlen(msg) + 1, 5);
printf("[Child %d] Sent message\n", getpid());
mq_close(mqd); /* close child's copy */
return 0;
} else {
/* ---- PARENT: wait then read ---- */
wait(NULL); /* wait for child to finish */
char buf[MSG_SIZE];
unsigned int prio;
ssize_t n = mq_receive(mqd, buf, sizeof(buf), &prio);
if (n > 0) {
buf[n] = '\0';
printf("[Parent] Received from child: %s (prio=%u)\n", buf, prio);
}
mq_close(mqd);
mq_unlink(QUEUE_NAME);
}
return 0;
}
Just like file descriptors and file descriptions (open file table entries), POSIX message queues have two levels:
| Term | What It Is | Per-process or Shared? |
|---|---|---|
Message queue descriptor (mqd_t) |
The handle returned by mq_open(). Like a file descriptor number. | Per-process. Duplicated by fork(). |
| Message queue description | Kernel’s internal open queue entry — holds the open flags (O_NONBLOCK etc.). | Shared between parent and child after fork(). |
Practical implication: After fork(), parent and child share the same underlying queue description. If the parent changes the O_NONBLOCK flag via mq_setattr(), the child sees the change too — because they share the same queue description object.
mq_close() closes the calling process’s descriptor for the queue — like closing a file handle. The queue and its messages remain. mq_unlink() removes the queue’s name from the filesystem and marks it for deletion. The actual deletion happens when the last process that has it open calls mq_close() (or terminates). This is the reference counting mechanism.O_CREAT | O_EXCL | O_RDWR. The O_EXCL flag causes mq_open() to fail with EEXIST if a queue with that name already exists. This is an atomic check-and-create operation.mq_notify() registrations. The descriptors point to the same underlying queue description, so changes to open flags (e.g., setting O_NONBLOCK) affect both.EAGAIN. In non-blocking mode, if mq_send() would need to block (queue full) or mq_receive() would need to block (queue empty), they return -1 immediately with errno set to EAGAIN.mqd_t) for it. mq_unlink() only removes the name and marks the queue for deletion — it does not immediately destroy the queue data. Processes holding open descriptors can continue to send and receive until they close their descriptors.mqd_t is the type returned by mq_open() — it is a message queue descriptor. On Linux it is an int, similar to a file descriptor. It identifies an open message queue within the process. SUSv3 only guarantees it is not an array type (can be assigned and passed by value). On Solaris it is a void *.O_CREAT | O_EXCL which would fail in this case. If you want to guarantee creation fails when queue exists, always add O_EXCL.mq_close() were called on each). As a side effect of closing each descriptor, any mq_notify() registrations made through those descriptors are automatically deregistered. Other processes can then register for notification on those queues.