Every System V message queue that exists in the kernel is represented internally by a data structure called msqid_ds. This structure stores all the metadata about the queue — its ownership, permissions, usage statistics, timestamps, and capacity limits.
Think of it as the “inode” of a message queue. Just as a filesystem inode stores metadata about a file (owner, size, timestamps), the msqid_ds stores metadata about a message queue.
Understanding this structure is essential because:
- It is the return value of
msgctl(IPC_STAT)— you use it to inspect queue state. - It is the input to
msgctl(IPC_SET)— you use it to change queue settings. - Its fields are updated automatically by the kernel when msgsnd() or msgrcv() is called.
- It is a very common interview topic — interviewers often ask what each field means.
/* Defined in <sys/msg.h> */
struct msqid_ds {
struct ipc_perm msg_perm; /* Ownership and permissions */
time_t msg_stime; /* Time of last msgsnd() */
time_t msg_rtime; /* Time of last msgrcv() */
time_t msg_ctime; /* Time of last change (creation or IPC_SET) */
unsigned long __msg_cbytes; /* Current bytes in queue (non-standard) */
msgqnum_t msg_qnum; /* Number of messages currently in queue */
msglen_t msg_qbytes; /* Maximum bytes allowed in queue */
pid_t msg_lspid; /* PID of process that called last msgsnd() */
pid_t msg_lrpid; /* PID of process that called last msgrcv() */
};
msqid_ds. This is the only System V message queue interface that uses this abbreviated spelling — a historical inconsistency that exists purely to confuse programmers (the TLPI author notes this explicitly!).AUTO = updated automatically by the kernel IPC_SET = can be updated via IPC_SET
uid (owner UID), gid (owner GID), cuid (creator UID), cgid (creator GID), and mode (9-bit permission flags like a file). Via IPC_SET, you can change uid, gid, and mode. The creator fields (cuid, cgid) cannot be changed.msgsnd() call on this queue. When a queue is first created, this field is set to 0 (meaning “never”). Each successful send updates it to the current time.msgrcv() call on this queue. Initialised to 0 when the queue is created. Each successful receive updates it to the current time. Useful for detecting queues that have not been read for a long time.msgsnd() and decreases by 1 on each successful msgrcv(). A value of 0 means the queue is empty. You can use this to monitor queue backlog.msgsnd() blocks (or returns EAGAIN if IPC_NOWAIT is set). The system-wide default is in /proc/sys/kernel/msgmnb (typically 16384 bytes). Can be raised by a privileged process using IPC_SET with CAP_SYS_RESOURCE.msgsnd() call. Initialised to 0 at queue creation. Useful for debugging to identify which process last wrote to the queue.msgrcv() call. Initialised to 0 at queue creation. Useful for debugging to identify which process last read from the queue.msg_qnum instead for counting.| Field | Initial Value | Updated by | Settable via IPC_SET? |
|---|---|---|---|
msg_perm |
Set from caller context at creation | Queue creation, IPC_SET | Yes (uid, gid, mode) |
msg_stime |
0 (never) | Each successful msgsnd() | No |
msg_rtime |
0 (never) | Each successful msgrcv() | No |
msg_ctime |
Creation time | Creation, IPC_SET | No (set automatically) |
msg_qnum |
0 | msgsnd() +1, msgrcv() -1 | No |
msg_qbytes |
System default (msgmnb) | IPC_SET only | Yes |
msg_lspid |
0 | Each successful msgsnd() | No |
msg_lrpid |
0 | Each successful msgrcv() | No |
__msg_cbytes |
0 | msgsnd() / msgrcv() | No (Linux-specific) |
The msg_perm field is itself a structure. It is the same ipc_perm sub-structure used across all System V IPC objects (message queues, semaphores, shared memory).
struct ipc_perm {
key_t __key; /* Key supplied to msgget() — non-modifiable */
uid_t uid; /* Effective UID of owner — settable via IPC_SET */
gid_t gid; /* Effective GID of owner — settable via IPC_SET */
uid_t cuid; /* Effective UID of creator — NOT settable */
gid_t cgid; /* Effective GID of creator — NOT settable */
unsigned short mode; /* Permission bits (rwxrwxrwx format) — settable via IPC_SET */
unsigned short __seq; /* Sequence number (used in msqid generation) */
};
msgget(IPC_CREAT). The owner starts as the creator, but can be changed via IPC_SET. This allows a privileged setup process to create a queue and then hand ownership to another UID. The creator fields (cuid, cgid) can never be changed.mode field works exactly like Unix file permissions — 9 bits: 3 for owner (read/write), 3 for group (read/write), 3 for others (read/write). The execute bit has no meaning for IPC objects and is ignored. For example, 0660 allows read+write for owner and group, nothing for others.All three timestamp fields — msg_stime, msg_rtime, msg_ctime — are of type time_t and store time as seconds since the Unix Epoch (midnight, 1 January 1970, UTC). This is the same format as the value returned by time().
A value of 0 in msg_stime or msg_rtime means “this operation has never occurred on this queue.” This is a useful sentinel: if msg_stime is 0, the queue has never had a message sent to it. If msg_rtime is 0, no message has ever been received from it.
msg_ctime is different — it is set to the current time at the moment of queue creation and also updated whenever IPC_SET is called. It records the last “configuration change” to the queue, similar to the st_ctime field in a file’s stat structure.
ctime(): printf("Last send: %s", ctime(&info.msg_stime)); Note that ctime() returns a string with a trailing newline, so you typically don’t need \n in your format string.This field is the most operationally important one for system performance. It controls how much data can accumulate in the queue before msgsnd() is forced to block.
- The kernel tracks the total bytes of all messages currently in the queue via
__msg_cbytes. - When a new
msgsnd()arrives, the kernel checks if adding the new message would push__msg_cbytesovermsg_qbytes. - If so, and
IPC_NOWAITis not set, the sending process blocks until enough messages are consumed to make room. - If
IPC_NOWAITis set,msgsnd()immediately returns-1witherrno = EAGAIN.
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
/* Helper: print time_t field. Shows "Never" if t == 0 */
static void print_time(const char *label, time_t t)
{
if (t == 0)
printf("%-28s: Never\n", label);
else
printf("%-28s: %s", label, ctime(&t)); /* ctime adds \n */
}
int main(void)
{
int msqid;
struct msqid_ds info;
/* Create a private queue with read+write for owner only */
msqid = msgget(IPC_PRIVATE, 0600);
if (msqid == -1) { perror("msgget"); exit(1); }
/* Read queue metadata */
if (msgctl(msqid, IPC_STAT, &info) == -1) {
perror("msgctl IPC_STAT");
exit(1);
}
printf("====== msqid_ds contents (msqid=%d) ======\n\n", msqid);
/* Ownership and permissions */
printf("%-28s: %d\n", "msg_perm.uid (owner)", info.msg_perm.uid);
printf("%-28s: %d\n", "msg_perm.gid (owner)", info.msg_perm.gid);
printf("%-28s: %d\n", "msg_perm.cuid (creator)", info.msg_perm.cuid);
printf("%-28s: %d\n", "msg_perm.cgid (creator)", info.msg_perm.cgid);
printf("%-28s: %04o\n", "msg_perm.mode (perms)", info.msg_perm.mode & 0777);
/* Timestamps */
print_time("msg_stime (last msgsnd)", info.msg_stime);
print_time("msg_rtime (last msgrcv)", info.msg_rtime);
print_time("msg_ctime (last change)", info.msg_ctime);
/* Queue statistics */
printf("%-28s: %lu\n", "msg_qnum (messages now)",
(unsigned long)info.msg_qnum);
printf("%-28s: %lu bytes\n", "msg_qbytes (max capacity)",
(unsigned long)info.msg_qbytes);
printf("%-28s: %d\n", "msg_lspid (last sender PID)", info.msg_lspid);
printf("%-28s: %d\n", "msg_lrpid (last receiver PID)", info.msg_lrpid);
/* Cleanup */
msgctl(msqid, IPC_RMID, NULL);
return 0;
}
/*
* Sample Output (right after creation — no messages sent/received yet):
* ====== msqid_ds contents (msqid=98308) ======
* msg_perm.uid (owner) : 1000
* msg_perm.gid (owner) : 1000
* msg_perm.cuid (creator) : 1000
* msg_perm.cgid (creator) : 1000
* msg_perm.mode (perms) : 0600
* msg_stime (last msgsnd) : Never
* msg_rtime (last msgrcv) : Never
* msg_ctime (last change) : Mon Jun 8 09:00:00 2026
* msg_qnum (messages now) : 0
* msg_qbytes (max capacity) : 16384 bytes
* msg_lspid (last sender PID): 0
* msg_lrpid (last receiver PID): 0
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
struct message {
long mtype;
char mtext[64];
};
static void show_queue_stats(int msqid, const char *when)
{
struct msqid_ds info;
if (msgctl(msqid, IPC_STAT, &info) == -1) {
perror("msgctl IPC_STAT"); return;
}
printf("\n--- State: %-30s ---\n", when);
printf(" msg_qnum (messages in queue): %lu\n",
(unsigned long)info.msg_qnum);
printf(" msg_stime (last send) : %s",
info.msg_stime ? ctime(&info.msg_stime) : "Never\n");
printf(" msg_rtime (last recv) : %s",
info.msg_rtime ? ctime(&info.msg_rtime) : "Never\n");
printf(" msg_lspid (last sender PID) : %d\n", info.msg_lspid);
printf(" msg_lrpid (last receiver PID) : %d\n", info.msg_lrpid);
}
int main(void)
{
int msqid;
struct message msg;
msqid = msgget(IPC_PRIVATE, 0600);
if (msqid == -1) { perror("msgget"); exit(1); }
/* --- State 1: Just after creation --- */
show_queue_stats(msqid, "After creation");
/* --- Send a message --- */
msg.mtype = 1;
strncpy(msg.mtext, "Hello from EmbeddedPathashala!", sizeof(msg.mtext)-1);
if (msgsnd(msqid, &msg, strlen(msg.mtext)+1, 0) == -1) {
perror("msgsnd"); exit(1);
}
/* --- State 2: After sending --- */
show_queue_stats(msqid, "After msgsnd()");
/* --- Receive the message --- */
if (msgrcv(msqid, &msg, sizeof(msg.mtext), 0, 0) == -1) {
perror("msgrcv"); exit(1);
}
/* --- State 3: After receiving --- */
show_queue_stats(msqid, "After msgrcv()");
msgctl(msqid, IPC_RMID, NULL);
return 0;
}
/*
* This example clearly shows:
* - msg_qnum goes 0 → 1 → 0 as a message is sent then received
* - msg_stime is "Never" until the first msgsnd()
* - msg_rtime is "Never" until the first msgrcv()
* - msg_lspid and msg_lrpid are 0 until the first send/receive
*/
msqid_ds is the kernel-maintained data structure associated with every System V message queue. It serves as the “metadata record” for the queue — storing ownership, permissions, usage timestamps, current message count, byte capacity, and PIDs of the last sender and receiver. Every message queue has exactly one such structure. It is accessed via msgctl() with IPC_STAT (to read) or IPC_SET (to modify certain fields).msg_stime and msg_rtime are set to 0 when the message queue is first created. A value of 0 means “this operation has never been performed on this queue.” msg_stime is updated to the current time on each successful msgsnd(), and msg_rtime is updated on each successful msgrcv(). This is a common interview question — the answer is 0, not the creation time.msg_stime tracks the last time a message was sent to the queue via msgsnd(). It is 0 until the first send and then updated on every subsequent send.
msg_ctime tracks the last time the queue’s configuration was changed — either when the queue was first created, or when msgctl(IPC_SET) was called to modify the queue’s metadata. Sending and receiving messages does not update msg_ctime.
msg_qnum stores the number of messages currently in the queue at the time of the last IPC_STAT call. It starts at 0 when the queue is created. Each successful msgsnd() increments it by 1, and each successful msgrcv() decrements it by 1. You can use this to implement a “queue depth monitor” — polling IPC_STAT periodically and alerting if msg_qnum grows too large, which would indicate a consumer is not keeping up with a producer.cuid, cgid) identifies the process that originally called msgget(IPC_CREAT) and is immutable — it can never be changed by any means.
The owner (uid, gid) starts as the same as the creator but can be changed via msgctl(IPC_SET). This is useful when a privileged setup process creates a queue and then transfers ownership to a less-privileged user. Deletion rights are based on matching the owner OR creator UID.
msg_qbytes is the maximum total bytes that all messages combined can occupy in the queue simultaneously. When a process calls msgsnd() and the new message would cause the total to exceed msg_qbytes, the following happens: if IPC_NOWAIT is not set, the calling process blocks until space becomes available. If IPC_NOWAIT is set, msgsnd() returns -1 with errno = EAGAIN. The default system value is in /proc/sys/kernel/msgmnb.msg_lspid holds the PID of the process that made the most recent successful msgsnd() call. msg_lrpid holds the PID of the process that made the most recent successful msgrcv() call. Both are initialised to 0 at queue creation (since no send or receive has occurred yet). These fields are extremely useful for debugging — for example, to identify which process is filling up a queue or which process last consumed a message.__msg_cbytes) is a convention in C to mark fields as implementation-specific / non-standard. This field is Linux-specific and not defined in POSIX or SUSv3. Portable code targeting multiple Unix platforms should avoid depending on it. On Linux, it stores the current total bytes occupied by all messages in the queue, but since it is non-standard, its name and existence cannot be guaranteed on other systems like macOS, Solaris, or AIX.msgctl(msqid, IPC_STAT, &info) to populate your local msqid_ds structure with the current values. (2) Then modify only info.msg_perm.mode to the new permission bits. (3) Finally call msgctl(msqid, IPC_SET, &info) to apply the change. Never fill a msqid_ds from scratch for IPC_SET — you would risk zeroing out uid and gid values and accidentally changing the queue’s ownership.time_t, which stores time as seconds since the Unix Epoch (January 1, 1970, 00:00:00 UTC). A value of 0 is the sentinel meaning “this event has never occurred” — specifically, it means: no message has ever been sent (msg_stime), no message has ever been received (msg_rtime), or (in theory, though this never happens) the queue was never created or modified (msg_ctime). In practice msg_ctime is always nonzero because it is set at creation time.msqid_ds structure stores metadata about a message queue (owner, group, permissions, send/receive/change timestamps, message count, byte capacity, and last-actor PIDs). Neither structure stores the actual data content — the inode doesn’t store file bytes, and msqid_ds doesn’t store message content.msgqnum_t and msglen_t are unsigned integer types used to type the msg_qnum and msg_qbytes fields respectively. They are defined in <sys/msg.h> and their exact width may vary by platform (typically 32-bit or 64-bit). They are specified in SUSv3 to ensure these fields are always unsigned — message counts and byte sizes are inherently non-negative. Using a defined type rather than plain unsigned long improves portability across different Unix implementations.You have completed Sections 46.3 and 46.4 of Chapter 46 — System V Message Queues.
