In System V IPC, once a message queue is created, you need a way to manage it — inspect its state, change its settings, or delete it when it is no longer needed. This is exactly what msgctl() (message queue control) does.
Think of msgctl() as the administrative interface to a message queue. Just like you use ioctl() to control device files, you use msgctl() to control message queue objects. It is a multipurpose system call — the cmd argument selects what operation to perform.
This is one of those system calls that every Linux systems programmer must know deeply, and it is a very common interview topic in embedded Linux and systems programming roles.
#include <sys/types.h> /* For portability */
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
/* Returns: 0 on success, -1 on error (errno set) */
Parameters explained:
| Parameter | Type | Purpose |
|---|---|---|
msqid |
int | The message queue identifier returned by msgget(). This uniquely identifies which queue to operate on. |
cmd |
int | The control command — one of IPC_RMID, IPC_STAT, IPC_SET, or other values. This tells the kernel what action to perform. |
buf |
struct msqid_ds * | Pointer to a data structure buffer. Used to pass data in (for IPC_SET) or receive data out (for IPC_STAT). For IPC_RMID, this is ignored and can be NULL. |
buf is interpreted differently depending on cmd. For IPC_RMID it is completely ignored. For IPC_STAT the kernel writes into it. For IPC_SET the caller fills it and the kernel reads from it.The cmd argument controls what operation msgctl() performs. There are three fundamental values:
This command immediately and permanently deletes the message queue from the kernel. When IPC_RMID is used:
- The queue object is destroyed along with its
msqid_dsmetadata structure. - All messages currently sitting in the queue are discarded and lost forever.
- Any process currently blocked on
msgsnd()(because the queue was full) ormsgrcv()(waiting for a message) is immediately unblocked. - Those unblocked calls fail and return
-1witherrnoset to EIDRM (Identifier Removed). - The third argument
bufis completely ignored for this operation.
msgctl(msqid, IPC_RMID, NULL), all unread messages in that queue are gone. Make sure the application logic handles this gracefully, especially when other processes may be using the queue.This command reads the current state and metadata of the message queue into the msqid_ds structure pointed to by buf. After the call returns successfully, buf is filled with information about:
- Ownership and permissions (
msg_perm) - Time of last send, receive, and change
- Number of messages currently in the queue
- Maximum allowed bytes in the queue
- PIDs of the last sender and receiver
This is used for monitoring and debugging — for example, to check if a queue is getting too full, or to find out who last interacted with it.
This command updates selected fields in the queue’s msqid_ds structure. The caller fills in a msqid_ds structure and passes it via buf. The kernel reads from it and updates the queue’s metadata. Only these fields can be changed:
msg_perm.uid— owner user IDmsg_perm.gid— owner group IDmsg_perm.mode— permission bits (lower 9 bits)msg_qbytes— maximum number of bytes in the queue (requiresCAP_SYS_RESOURCEto raise above the system default)
msqid_ds, then modify only the fields you need, then call IPC_SET. This prevents accidentally overwriting fields you didn’t intend to change.Create Queue
Send Messages
Receive Messages
Control/Delete
msgctl() can be called at any stage — not just at the end. For example, you might call it after msgget() to adjust the queue’s byte limit before any messages are sent, or periodically call it with IPC_STAT to monitor queue depth.
This is the most common use of msgctl(). Every queue you create should be cleaned up — otherwise it persists in kernel memory until the system reboots.
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <errno.h>
int main(void)
{
key_t key;
int msqid;
/* Step 1: Generate a unique key for this queue */
key = ftok("/tmp", 'A');
if (key == -1) {
perror("ftok");
exit(EXIT_FAILURE);
}
/* Step 2: Create (or open) the message queue */
msqid = msgget(key, IPC_CREAT | 0660);
if (msqid == -1) {
perror("msgget");
exit(EXIT_FAILURE);
}
printf("Queue created. msqid = %d\n", msqid);
/* Step 3: ... (send/receive messages here) ... */
/* Step 4: Delete the queue when done */
/* IPC_RMID: third argument is ignored — pass NULL */
if (msgctl(msqid, IPC_RMID, NULL) == -1) {
perror("msgctl IPC_RMID");
exit(EXIT_FAILURE);
}
printf("Queue msqid=%d deleted successfully.\n", msqid);
return 0;
}
/*
* Output:
* Queue created. msqid = 32769
* Queue msqid=32769 deleted successfully.
*
* Important: After IPC_RMID, any process blocked on msgsnd()
* or msgrcv() on this queue will be woken up and their call
* will return -1 with errno == EIDRM.
*/
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int main(void)
{
int msqid;
struct msqid_ds info;
/* Create a private queue */
msqid = msgget(IPC_PRIVATE, 0600);
if (msqid == -1) { perror("msgget"); exit(1); }
/* ---- IPC_STAT: Read queue metadata ---- */
if (msgctl(msqid, IPC_STAT, &info) == -1) {
perror("msgctl IPC_STAT");
exit(1);
}
printf("=== IPC_STAT Results ===\n");
printf("Owner UID : %d\n", info.msg_perm.uid);
printf("Owner GID : %d\n", info.msg_perm.gid);
printf("Permissions : %o\n", info.msg_perm.mode & 0777);
printf("Messages in queue: %lu\n", (unsigned long)info.msg_qnum);
printf("Max queue bytes : %lu\n", (unsigned long)info.msg_qbytes);
printf("Last msgsnd() time: %s",
(info.msg_stime == 0) ? "Never\n" : ctime(&info.msg_stime));
printf("Last msgrcv() time: %s",
(info.msg_rtime == 0) ? "Never\n" : ctime(&info.msg_rtime));
printf("PID of last sender : %d\n", info.msg_lspid);
printf("PID of last receiver: %d\n", info.msg_lrpid);
/* ---- IPC_SET: Change permissions ----
* Best practice: start from IPC_STAT result, then modify only
* the fields you need. This avoids accidental overwrites.
*/
info.msg_perm.mode = 0666; /* Allow rw for everyone */
if (msgctl(msqid, IPC_SET, &info) == -1) {
perror("msgctl IPC_SET");
exit(1);
}
printf("\nPermissions updated to 0666.\n");
/* Confirm the change */
if (msgctl(msqid, IPC_STAT, &info) == -1) {
perror("msgctl IPC_STAT (verify)");
exit(1);
}
printf("New permissions : %o\n", info.msg_perm.mode & 0777);
/* Cleanup */
msgctl(msqid, IPC_RMID, NULL);
return 0;
}
/*
* Sample Output:
* === IPC_STAT Results ===
* Owner UID : 1000
* Owner GID : 1000
* Permissions : 600
* Messages in queue: 0
* Max queue bytes : 16384
* Last msgsnd() time: Never
* Last msgrcv() time: Never
* PID of last sender : 0
* PID of last receiver: 0
*
* Permissions updated to 0666.
* New permissions : 666
*/
This is a subtle but important behaviour that comes up in interviews:
When a process calls msgrcv() on an empty queue and no message arrives, it blocks (sleeps) inside the kernel. Similarly, a process calling msgsnd() when the queue is full also blocks.
Now, if another process deletes the queue with msgctl(msqid, IPC_RMID, NULL), the kernel immediately wakes up all blocked processes. Their msgsnd() or msgrcv() call returns -1 and errno is set to EIDRM (“Identifier removed”).
/* Robust msgrcv() that handles queue deletion gracefully */
ssize_t result = msgrcv(msqid, &msg, sizeof(msg.mtext), 0, 0);
if (result == -1) {
if (errno == EIDRM) {
/* The queue was deleted while we were waiting.
This is a normal shutdown scenario — exit cleanly. */
printf("Queue was removed. Exiting...\n");
exit(EXIT_SUCCESS);
} else if (errno == EINTR) {
/* Interrupted by a signal — retry or exit depending on design */
printf("Interrupted by signal. Retrying...\n");
/* ... retry logic ... */
} else {
perror("msgrcv");
exit(EXIT_FAILURE);
}
}
| Operation | Who is allowed? |
|---|---|
| IPC_RMID | The process must have the effective UID matching the queue’s owner or creator UID, or must have the CAP_IPC_OWNER capability (root typically has this). |
| IPC_STAT | The process needs read permission on the queue (as defined in the msg_perm.mode field). Processes without any permission get EACCES. |
| IPC_SET | Same as IPC_RMID — must match owner/creator UID or have CAP_IPC_OWNER. To raise msg_qbytes above the system default, the process additionally needs CAP_SYS_RESOURCE. |
| errno value | When it occurs |
|---|---|
| EINVAL | msqid is not a valid message queue identifier, or cmd is an unrecognised command. |
| EACCES | The calling process does not have permission to perform the requested IPC_STAT operation on this queue. |
| EPERM | Attempted IPC_SET or IPC_RMID but the caller is not the owner, creator, or does not have the required capability. |
| EPERM | Attempted to raise msg_qbytes above the system limit without CAP_SYS_RESOURCE. |
| EFAULT | The buf pointer is an invalid address (for IPC_STAT or IPC_SET). |
You can also inspect and remove System V message queues from the command line using standard Linux tools. This is very useful for debugging.
msgget(IPC_CREAT...), always run ipcs -q to check you haven’t left orphan queues in the kernel. Leaked queues consume kernel memory and count against the system’s IPC object limit.msgctl() is the control interface for System V message queues. It allows a process to perform administrative operations on a queue identified by msqid. The cmd argument selects the specific operation. It returns 0 on success and -1 on failure, with errno set to indicate the error.errno = EIDRM. The buf argument is ignored.
IPC_STAT — Copies the queue’s msqid_ds metadata structure into the buffer pointed to by buf. Used to inspect the queue’s state, size, timestamps, and permissions.
IPC_SET — Updates selected writable fields of the queue’s msqid_ds structure (uid, gid, mode, and msg_qbytes) using values from the buffer pointed to by buf.
-1 with errno set to EIDRM (Identifier Removed). A well-written application must check for this error and handle it gracefully — typically by cleaning up and exiting rather than retrying, since the queue no longer exists.msqid_ds, modify only the specific fields you want to change, and then call IPC_SET. This avoids accidentally overwriting fields you did not intend to change. For example, if you only want to change msg_qbytes, you must still provide correct values for uid, gid, and mode — so reading them first with IPC_STAT is essential.msg_perm.uid), (b) its effective UID matches the queue’s creator UID (msg_perm.cuid), or (c) it has the CAP_IPC_OWNER capability (which the root user possesses by default). Without one of these, msgctl() fails with errno = EPERM.CAP_SYS_RESOURCE capability is required to raise msg_qbytes (the maximum byte capacity of the queue) above the system-wide default limit. An ordinary user can reduce msg_qbytes below the current value without any special capability, but raising it requires privilege. The system default is typically visible in /proc/sys/kernel/msgmnb.msgctl() returns -1 and sets errno to EINVAL if the msqid does not refer to a valid, existing message queue. This can happen if the queue was deleted by another process, or if the wrong ID was used. This is why robust programs always check the return value of every IPC system call.ipcs -q lists all existing System V message queues visible to the current user, showing their key, ID, owner, permissions, number of messages, and byte size. ipcrm -q <msqid> deletes a specific queue by its numeric ID — equivalent to calling msgctl(msqid, IPC_RMID, NULL) in C code. These tools are essential for debugging and cleaning up leaked IPC objects during development./proc/sys/kernel/msgmni), and can cause msgget() to fail with ENOSPC in long-running systems.msqid_ds data into the buffer you provide. Your buffer must be large enough to hold a struct msqid_ds.
For IPC_SET, buf is an input parameter — the kernel reads the values you have placed in the buffer and uses them to update the queue’s metadata. The direction is reversed: caller → kernel for IPC_SET, kernel → caller for IPC_STAT.
msg_perm.uid (owner user ID), msg_perm.gid (owner group ID), msg_perm.mode (permission bits), and msg_qbytes (maximum queue byte capacity). Other fields like timestamps, message count, and PID fields are maintained automatically by the kernel and cannot be set directly by userspace.if (msgctl(msqid, IPC_RMID, NULL) == -1) {
if (errno == EINVAL) {
/* Queue does not exist — already deleted, not an error */
fprintf(stderr, "Queue %d already removed.\n", msqid);
} else {
perror("msgctl IPC_RMID");
exit(EXIT_FAILURE);
}
}
The key is checking for EINVAL separately — in a multi-process system, another process may have already called IPC_RMID before you did. Treating EINVAL as a non-fatal condition here is good defensive programming.
Next: msqid_ds Data Structure — Understanding Every Field
