1. Intro 2. Open/Close 3. Attributes 4. Send/Receive 5. Notify 6. Linux Specifics 7. Interview Q&A →
On Linux, POSIX message queues are exposed through a virtual filesystem called mqueue, typically mounted at /dev/mqueue. Each queue appears as a file. You can inspect and interact with queues using ordinary shell tools.
# Check if mqueue fs is mounted
mount | grep mqueue
# Mount it manually if needed (usually done automatically by systemd)
mount -t mqueue none /dev/mqueue
# Create a queue from C and inspect it from shell
# (after running a program that calls mq_open("/myqueue", O_CREAT|O_RDWR, ...))
ls -la /dev/mqueue/
# -rwxrwxrwx 1 ravi ravi 80 Jun 15 10:00 myqueue
# Read queue attributes directly from the filesystem
cat /dev/mqueue/myqueue
# QSIZE:0 NOTIFY:0 SIGNO:0 NOTIFY_PID:0
# QSIZE = total bytes of data currently in the queue
# NOTIFY = 1 if a process is registered via mq_notify, 0 otherwise
# SIGNO = signal number to be sent (if SIGEV_SIGNAL)
# NOTIFY_PID= PID of the registered process (0 if none)
You can even delete a queue from the shell using rm:
# Remove a queue from shell (equivalent to mq_unlink)
rm /dev/mqueue/myqueue
Five tunable parameters under /proc/sys/fs/mqueue/ control the behaviour of POSIX MQs system-wide:
| File | Typical Default | What it controls |
|---|---|---|
msg_default |
10 | Default mq_maxmsg when NULL attr passed to mq_open() |
msgsize_default |
8192 | Default mq_msgsize when NULL attr passed to mq_open() |
msg_max |
10 | Hard ceiling for mq_maxmsg (unprivileged processes). Root can exceed. |
msgsize_max |
8192 | Hard ceiling for mq_msgsize (unprivileged processes). Root can exceed. |
queues_max |
256 | Maximum number of message queues system-wide |
# View current limits
cat /proc/sys/fs/mqueue/msg_max
cat /proc/sys/fs/mqueue/msgsize_max
cat /proc/sys/fs/mqueue/queues_max
# Temporarily increase msg_max to allow larger queues (needs root)
echo 50 > /proc/sys/fs/mqueue/msg_max
# Permanently increase via sysctl.conf
echo "fs.mqueue.msg_max = 50" >> /etc/sysctl.conf
sysctl -p
# RLIMIT_MSGQUEUE also applies: per-user limit on total bytes in all queues
# Check with:
ulimit -q # typical: 819200 bytes
/* Show the RLIMIT_MSGQUEUE limit from C */
#include <stdio.h>
#include <sys/resource.h>
int main(void)
{
struct rlimit rl;
getrlimit(RLIMIT_MSGQUEUE, &rl);
printf("RLIMIT_MSGQUEUE: soft=%lu hard=%lu bytes\n",
(unsigned long)rl.rlim_cur,
(unsigned long)rl.rlim_max);
return 0;
}
/* Typical output:
RLIMIT_MSGQUEUE: soft=819200 hard=819200 bytes
*/
Memory accounting: Each queue consumes: mq_maxmsg * (mq_msgsize + sizeof(struct msg_msg)) bytes reserved from the user’s RLIMIT_MSGQUEUE. mq_open() fails with EMFILE or ENOMEM when this limit is hit, even if the queue is empty.
On Linux, mqd_t is implemented as a true file descriptor — it appears in /proc/<pid>/fd/ and can be used with I/O multiplexing functions like select(), poll(), and epoll(). This is a Linux-specific extension beyond the POSIX standard.
This means you can monitor multiple message queues (and regular file descriptors) in a single poll() or epoll_wait() call — very useful for event-driven servers.
#include <stdio.h>
#include <string.h>
#include <mqueue.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <poll.h>
#include <unistd.h>
#define Q1NAME "/poll_q1"
#define Q2NAME "/poll_q2"
#define MSGSIZE 64
int main(void)
{
struct mq_attr attr = { .mq_maxmsg=4, .mq_msgsize=MSGSIZE };
/* Open two queues in O_NONBLOCK for polling */
mqd_t mqd1 = mq_open(Q1NAME, O_CREAT|O_RDONLY|O_NONBLOCK, 0600, &attr);
mqd_t mqd2 = mq_open(Q2NAME, O_CREAT|O_RDONLY|O_NONBLOCK, 0600, &attr);
if (mqd1 == (mqd_t)-1 || mqd2 == (mqd_t)-1) {
perror("mq_open"); return 1;
}
/* Seed queue 2 with a message */
mqd_t wr2 = mq_open(Q2NAME, O_WRONLY);
mq_send(wr2, "data from q2", 13, 3);
mq_close(wr2);
/* Poll both queues — POLLIN becomes ready when messages available */
struct pollfd fds[2];
fds[0].fd = (int)mqd1;
fds[0].events = POLLIN;
fds[1].fd = (int)mqd2;
fds[1].events = POLLIN;
printf("Polling two queues (2 second timeout)...\n");
int ready = poll(fds, 2, 2000); /* 2000ms timeout */
if (ready == -1) {
perror("poll"); goto cleanup;
} else if (ready == 0) {
printf("poll: timeout — no messages\n");
} else {
if (fds[0].revents & POLLIN) {
char buf[MSGSIZE]; unsigned int prio;
mq_receive(mqd1, buf, MSGSIZE, &prio);
printf("Q1 readable: \"%s\"\n", buf);
}
if (fds[1].revents & POLLIN) {
char buf[MSGSIZE]; unsigned int prio;
mq_receive(mqd2, buf, MSGSIZE, &prio);
printf("Q2 readable: \"%s\"\n", buf);
}
}
cleanup:
mq_close(mqd1); mq_unlink(Q1NAME);
mq_close(mqd2); mq_unlink(Q2NAME);
return 0;
}
Output:
Polling two queues (2 second timeout)...
Q2 readable: "data from q2"
For high-performance servers monitoring many queues, epoll is more efficient than poll since it uses O(1) event notification rather than O(n) scanning.
#include <stdio.h>
#include <string.h>
#include <mqueue.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/epoll.h>
#include <unistd.h>
#define QNAME "/epoll_q"
#define MSGSIZE 64
int main(void)
{
struct mq_attr attr = { .mq_maxmsg=4, .mq_msgsize=MSGSIZE };
mqd_t mqd = mq_open(QNAME, O_CREAT|O_RDONLY|O_NONBLOCK, 0600, &attr);
if (mqd == (mqd_t)-1) { perror("mq_open"); return 1; }
/* Send a test message via separate open */
mqd_t wr = mq_open(QNAME, O_WRONLY);
mq_send(wr, "epoll test msg", 15, 5);
mq_close(wr);
/* Set up epoll */
int epfd = epoll_create1(0);
if (epfd == -1) { perror("epoll_create1"); goto cleanup; }
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = (int)mqd;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, (int)mqd, &ev) == -1) {
perror("epoll_ctl"); goto cleanup;
}
printf("Waiting on epoll (2000ms timeout)...\n");
struct epoll_event events[4];
int nready = epoll_wait(epfd, events, 4, 2000);
if (nready > 0 && (events[0].events & EPOLLIN)) {
char buf[MSGSIZE]; unsigned int prio;
ssize_t n = mq_receive(mqd, buf, MSGSIZE, &prio);
printf("epoll: received \"%.*s\" (prio=%u, bytes=%zd)\n",
(int)n, buf, prio, n);
} else if (nready == 0) {
printf("epoll: timed out\n");
} else {
perror("epoll_wait");
}
cleanup:
if (epfd != -1) close(epfd);
mq_close(mqd);
mq_unlink(QNAME);
return 0;
}
Linux supports IPC namespaces (since kernel 3.0), which give each namespace its own isolated set of POSIX message queues and System V IPC objects. This is the mechanism used by containers (Docker, LXC, etc.) for IPC isolation.
When a new IPC namespace is created (e.g., with clone(CLONE_NEWIPC) or unshare(CLONE_NEWIPC)), the process gets its own empty mqueue filesystem. Queues created inside the namespace are invisible to processes outside it and vice versa.
/* Demonstrate IPC namespace isolation */
#include <stdio.h>
#include <string.h>
#include <mqueue.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sched.h>
#include <unistd.h>
#include <sys/wait.h>
#define QNAME "/ns_test_q"
/* Child stack for clone() */
static char child_stack[65536];
static int child_func(void *arg)
{
/* This child is in a NEW IPC namespace */
printf("[child] in new IPC namespace\n");
/* Create a queue in child's namespace */
struct mq_attr attr = { .mq_maxmsg=2, .mq_msgsize=32 };
mqd_t mqd = mq_open(QNAME, O_CREAT|O_RDWR, 0600, &attr);
if (mqd == (mqd_t)-1) {
perror("[child] mq_open"); return 1;
}
printf("[child] created '%s' in new namespace\n", QNAME);
mq_send(mqd, "hello", 6, 1);
mq_close(mqd);
sleep(2); /* Keep namespace alive briefly */
return 0;
}
int main(void)
{
/* Create child in new IPC namespace */
pid_t pid = clone(child_func, child_stack + sizeof(child_stack),
CLONE_NEWIPC | SIGCHLD, NULL);
if (pid == -1) {
perror("clone (need root or CAP_SYS_ADMIN)"); return 1;
}
sleep(1);
/* Try to open same queue from parent (different IPC namespace) */
mqd_t mqd = mq_open(QNAME, O_RDONLY);
if (mqd == (mqd_t)-1) {
printf("[parent] cannot see child's queue '%s' — namespaces isolated!\n",
QNAME);
} else {
printf("[parent] opened queue (same namespace)\n");
mq_close(mqd);
}
waitpid(pid, NULL, 0);
return 0;
}
/* Note: CLONE_NEWIPC requires CAP_SYS_ADMIN or running as root */
In practice, containers create a new IPC namespace so queue names don’t clash between containers, even if they use the same name like /myqueue.
POSIX message queues are implemented entirely in the Linux kernel (not user-space). Here is what happens under the hood:
|
mq_open()
Allocates a |
→ |
mq_send()
Allocates a kernel |
→ |
mq_receive()
Removes the highest-priority message from the tree, copies data to user-space, frees kernel memory. Wakes up waiting senders if queue was full. |
→ |
mq_unlink()
Removes the dentry from the mqueue VFS. The inode (and all messages) is freed when the reference count drops to zero — i.e., when all fds are closed. |
Why kernel implementation vs user-space?
A user-space implementation using shared memory + semaphores is insecure because any process with access to the shared memory region could corrupt the queue structure. The kernel implementation ensures all access goes through controlled system calls, providing proper isolation and security.
# List all POSIX message queues (if mqueue fs is mounted at /dev/mqueue)
ls -la /dev/mqueue/
# Read queue info (QSIZE, NOTIFY, SIGNO, NOTIFY_PID)
cat /dev/mqueue/myqueue
# Monitor queue in real-time (watch for new messages appearing)
watch -n 1 "ls -la /dev/mqueue/ && cat /dev/mqueue/myqueue 2>/dev/null"
# Show file descriptors of a running process
ls -la /proc/$(pgrep myprogram)/fd | grep mqueue
# Show IPC namespace info
ls -la /proc/self/ns/ipc
# Check per-user RLIMIT_MSGQUEUE
ulimit -q
# Increase RLIMIT_MSGQUEUE for current shell session
ulimit -q unlimited # (may require root)
# Check which process holds notification on a queue
cat /dev/mqueue/myqueue
# QSIZE:0 NOTIFY:1 SIGNO:10 NOTIFY_PID:12345
# Clean up all queues (use carefully!)
ls /dev/mqueue/ | xargs -I{} rm /dev/mqueue/{}
Linux kernel version requirements:
• POSIX message queue basic API: Linux 2.6.6 (May 2004)
• IPC namespace support: Linux 2.6.19
• mqueue VFS / /dev/mqueue: Linux 2.6.6
• Using mqd_t with poll/epoll: Linux 2.6.6 (because mqd_t is a real fd on Linux)
Portability across UNIX systems:
The POSIX MQ API (mq_open, mq_send, etc.) is standard. However, the implementation details differ significantly:
• Linux: Kernel implementation, mqd_t is a real fd, exposed via mqueue VFS.
• Solaris 10: User-space implementation using mapped files. Manual page warns it may not be secure.
• macOS/iOS: POSIX MQs are not available (not implemented).
• FreeBSD: Kernel implementation available.
• QNX: Full implementation, widely used in embedded/RTOS contexts.
/* Portability check at compile time */
#include <mqueue.h>
#include <stdio.h>
int main(void)
{
/* MQ_PRIO_MAX: minimum 32 on POSIX, 32768 on Linux */
printf("MQ_PRIO_MAX = %d\n", MQ_PRIO_MAX);
/* On Linux, mqd_t is int (file descriptor) */
mqd_t mqd;
printf("sizeof(mqd_t) = %zu\n", sizeof(mqd));
return 0;
}
/* Linux output:
MQ_PRIO_MAX = 32768
sizeof(mqd_t) = 4 (it's an int/fd)
*/
Q1: Where are POSIX message queues visible in the Linux filesystem?
Under /dev/mqueue/, which is a virtual filesystem of type mqueue. Each queue appears as a file. You can read queue attributes with cat and delete queues with rm.
Q2: What is RLIMIT_MSGQUEUE and what does it limit?
It is a per-user resource limit (set with ulimit -q) that caps the total number of bytes that can be allocated for POSIX message queues belonging to that user. This is checked at mq_open() time based on the queue capacity (mq_maxmsg * mq_msgsize), not the current fill level.
Q3: Can you use select()/poll()/epoll() with POSIX MQ descriptors on Linux?
Yes. On Linux, mqd_t is implemented as a true file descriptor, so it works with all I/O multiplexing interfaces. A descriptor becomes POLLIN-ready when messages are available, and POLLOUT-ready when space is available. This is a Linux-specific extension beyond the POSIX standard.
Q4: What does the NOTIFY field in /dev/mqueue/<name> mean?
It is 1 if a process has registered for asynchronous notification via mq_notify(), and 0 otherwise. The NOTIFY_PID field shows the PID of the registered process, and SIGNO shows the signal number to be sent.
Q5: Why did Linux choose a kernel-space implementation for POSIX MQs instead of user-space?
A user-space implementation using shared memory could not be made secure — any process with access to the shared memory region could corrupt internal queue structures. A kernel implementation ensures all queue access goes through controlled system calls, providing security and proper isolation.
Q6: What is the minimum kernel version for POSIX message queue support on Linux?
Kernel 2.6.6.
Q7: How do IPC namespaces affect POSIX message queues?
Each IPC namespace has its own isolated mqueue filesystem. Queues created in one namespace are completely invisible to processes in another namespace, even if they use the same queue name. Docker containers use this for isolation.
Q8: How does mq_open() pre-account memory and why does it matter?
When you call mq_open(), the kernel pre-accounts the maximum possible memory the queue could use (mq_maxmsg * (mq_msgsize + overhead)) against your RLIMIT_MSGQUEUE. This means even an empty queue uses quota. If this reservation would exceed your limit, mq_open() fails with EMFILE or ENOMEM.
All interview questions in one place plus a complete comparison with System V MQs.
