Create / Open
Message Type
Retrieval Modes
What Are System V Message Queues?
A System V message queue is a kernel-maintained linked list of messages. Each message is a discrete unit of data with an associated integer type. Processes can send messages to the queue and receive them — either in first-in-first-out (FIFO) order or selectively by type.
Message queues were one of the original System V IPC mechanisms introduced into mainstream UNIX around 1983. They solve a simple but important problem: how do two unrelated processes on the same machine exchange structured, typed data without sharing memory or using file-based communication?
When you open a regular file, the kernel gives you back a file descriptor — a small integer that is local to your process. File descriptors live in a per-process table and have no meaning outside that process.
Message queues work differently. The handle used to refer to a message queue is the IPC identifier returned by msgget(). This identifier is a property of the object itself, not of any particular process. Any process that knows the identifier can use it directly — no need to call msgget() again if you already know the ID.
| Property | File Descriptor (open) | IPC Identifier (msgget) |
|---|---|---|
| Scope | Per-process only | System-wide, visible to all |
| Inherited by fork? | Yes (copy) | Not needed — all share same ID |
| Closed on exit? | Yes, automatically | No — persists until IPC_RMID |
| Uniqueness | Unique per process | Unique across entire system |
| Kernel persistence | File data persists; fd does not | Queue + ID persist after process exits |
/* Opening a file — fd is per-process */
int fd = open("/tmp/myfile.txt", O_RDONLY);
/* fd is only meaningful in THIS process */
/* Opening a message queue — msqid is system-wide */
key_t key = ftok("/tmp/myfile.txt", 'A');
int msqid = msgget(key, IPC_CREAT | 0660);
/* msqid can be used by ANY process that knows it */
/* Another process can look up the same queue */
int msqid2 = msgget(key, 0); /* same key → same msqid */
/* msqid == msqid2 — the same object */
A pipe is a byte stream — data written at one end flows out the other end as a continuous sequence of bytes. The reader can read any number of bytes at a time. There are no boundaries between writes.
A message queue is message-oriented. Each call to msgsnd() writes one complete, self-contained message. Each call to msgrcv() reads exactly one complete message. You cannot read half a message and leave the rest, and you cannot read two messages in one call.
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#include <stdio.h>
/* Define a message structure — always starts with a long mtype */
struct my_message {
long mtype; /* Message type — MUST be first, MUST be > 0 */
char text[128]; /* Message data payload */
};
/* --- WRITER process --- */
void writer_example(int msqid) {
struct my_message msg;
/* Send message type 1 */
msg.mtype = 1;
strncpy(msg.text, "Hello from writer!", sizeof(msg.text));
msgsnd(msqid, &msg, sizeof(msg.text), 0);
/* One msgsnd() = one complete, atomic message on the queue */
/* Send another message type 2 */
msg.mtype = 2;
strncpy(msg.text, "Data block contents", sizeof(msg.text));
msgsnd(msqid, &msg, sizeof(msg.text), 0);
}
/* --- READER process --- */
void reader_example(int msqid) {
struct my_message msg;
/* Read ANY message (mtype=0 means take the first one in FIFO order) */
msgrcv(msqid, &msg, sizeof(msg.text), 0, 0);
printf("Got: [type=%ld] %s\n", msg.mtype, msg.text);
/*
* msgrcv() ALWAYS returns exactly one complete message.
* You cannot read half a message or skip part of it.
* The message is REMOVED from the queue upon successful read.
*/
}
msgrcv() with a buffer that is smaller than the message, the default behavior is to return an error (E2BIG). The message is NOT partially returned. You can use the MSG_NOERROR flag to silently truncate — but this loses data.The most powerful feature distinguishing message queues from pipes is the integer type field. Every message carries a long integer type value (must be > 0). When calling msgrcv(), you can control which message you want:
| mtype value in msgrcv() | What is returned | Typical use case |
|---|---|---|
0 |
First message in the queue (FIFO order, regardless of type) | Simple FIFO queue behavior — like a pipe |
> 0 (e.g., 3) |
First message with exactly that type | Dedicated channels per message type (e.g., requests vs responses) |
< 0 (e.g., -5) |
First message whose type is ≤ |mtype| (i.e., lowest type first) | Priority queues — low type number = high priority |
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <string.h>
#define MSG_TYPE_REQUEST 1
#define MSG_TYPE_RESPONSE 2
#define MSG_TYPE_ERROR 3
struct mymsg {
long mtype;
char data[256];
};
/*
* Demonstrates all three retrieval modes.
* Assume msqid is a valid message queue with several messages.
*/
void retrieval_modes(int msqid) {
struct mymsg msg;
ssize_t bytes;
/* Mode 1: FIFO — get whatever came first */
bytes = msgrcv(msqid, &msg, sizeof(msg.data), 0, 0);
printf("FIFO mode: got type=%ld, data='%s'\n", msg.mtype, msg.data);
/* Mode 2: Exact type — only get a response message */
bytes = msgrcv(msqid, &msg, sizeof(msg.data), MSG_TYPE_RESPONSE, 0);
printf("Exact type: got response: '%s'\n", msg.data);
/* Mode 3: Priority — get the lowest-numbered type first */
/* With mtype=-5, returns first msg with type in {1,2,3,4,5}, lowest first */
bytes = msgrcv(msqid, &msg, sizeof(msg.data), -5, 0);
printf("Priority mode: got type=%ld (highest priority)\n", msg.mtype);
}
/*
* Classic client-server pattern using types:
* - Clients send type=1 (request)
* - Server replies with type=client_pid (so each client gets its own reply)
*/
void client_server_pattern(int msqid) {
struct mymsg msg;
/* CLIENT: send a request */
msg.mtype = MSG_TYPE_REQUEST;
snprintf(msg.data, sizeof(msg.data), "PID=%d: please process this", (int)getpid());
msgsnd(msqid, &msg, strlen(msg.data) + 1, 0);
/* CLIENT: wait for a reply addressed specifically to this PID */
msgrcv(msqid, &msg, sizeof(msg.data), (long)getpid(), 0);
printf("Client got reply: %s\n", msg.data);
/* SERVER (in another process):
reads type=1, processes it, replies with type=sender_pid */
}
mtype = client_pid. Each client then calls msgrcv(msqid, &msg, len, getpid(), 0) to receive only its own reply. This lets many clients share a single message queue without seeing each other’s responses.The TLPI text explicitly notes that message queues have limitations (covered fully in Section 46.9), and that new applications should prefer FIFOs, POSIX message queues, or sockets where possible. But understanding what message queues do better than pipes is equally important.
| Feature | Pipe / FIFO | System V Message Queue |
|---|---|---|
| Communication model | Byte stream (no boundaries) | Message-oriented (discrete units) |
| Message framing | Must be done by application (e.g., length prefix) | Built-in — each write is one complete message |
| Selective read | Not possible — strictly sequential | Yes — read by type value |
| Kernel persistence | Destroyed when no process has it open | Persists until IPC_RMID or reboot |
| Access control | File permissions on the FIFO file | ipc_perm (owner/group/other, read/write) |
| Handle type | File descriptor (per-process) | IPC identifier (system-wide) |
| Unrelated processes | FIFOs yes (via filesystem path); pipes no | Yes (via key or known identifier) |
| Portability | POSIX standard — highly portable | SUSv3 XSI — widely available but less modern |
| Limits | PIPE_BUF, pipe capacity | msgmax, msgmnb, msgmni (system-wide) |
/*
* EXAMPLE: Why message boundaries matter
*
* With a PIPE, the writer must encode message length manually.
* With a message queue, boundaries are automatic.
*/
/* ---- PIPE approach: manual framing ---- */
#include <unistd.h>
#include <stdint.h>
#include <string.h>
void pipe_send(int fd, const char *data, size_t len) {
/* Must send the length first, then data */
uint32_t nlen = (uint32_t)len;
write(fd, &nlen, sizeof(nlen)); /* 4-byte length prefix */
write(fd, data, len); /* actual data */
}
void pipe_recv(int fd, char *buf, size_t bufsize) {
uint32_t nlen;
read(fd, &nlen, sizeof(nlen)); /* read length first */
read(fd, buf, nlen); /* then read exactly that many bytes */
/* This is fragile — partial reads, blocking, etc. */
}
/* ---- Message Queue approach: automatic framing ---- */
#include <sys/msg.h>
struct packet {
long mtype;
char data[256];
};
void mq_send(int msqid, const char *data, long type) {
struct packet pkt;
pkt.mtype = type;
strncpy(pkt.data, data, sizeof(pkt.data));
msgsnd(msqid, &pkt, strlen(pkt.data) + 1, 0);
/* No length prefix needed — msgrcv always returns one complete message */
}
void mq_recv(int msqid, char *buf, long type) {
struct packet pkt;
msgrcv(msqid, &pkt, sizeof(pkt.data), type, 0);
strncpy(buf, pkt.data, sizeof(pkt.data));
/* Automatically got one complete message, nothing more, nothing less */
}
mq_open), or sockets, because these alternatives offer better integration with I/O multiplexing (select/poll/epoll), better POSIX compliance, and cleaner semantics. System V message queues exist primarily for legacy compatibility.The msgget() system call is the entry point for working with message queues. It either creates a new queue or returns the identifier of an existing one, depending on the key and flags.
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
/* Returns: message queue identifier on success, -1 on error */
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
int main(void) {
key_t key;
int msqid;
/* Step 1: Generate a key from an existing file */
key = ftok("/tmp", 'M');
if (key == -1) { perror("ftok"); exit(EXIT_FAILURE); }
/* Step 2: Create a new message queue */
/* S_IRUSR | S_IWUSR = owner can read and write (0600) */
msqid = msgget(key, IPC_CREAT | IPC_EXCL | S_IRUSR | S_IWUSR);
if (msqid == -1) {
if (errno == EEXIST) {
printf("Queue already exists. Opening existing queue...\n");
/* Open existing queue (no IPC_CREAT, no IPC_EXCL) */
msqid = msgget(key, 0);
if (msqid == -1) { perror("msgget open"); exit(EXIT_FAILURE); }
} else {
perror("msgget create");
exit(EXIT_FAILURE);
}
}
printf("Message queue ID: %d\n", msqid);
/*
* Verify with shell:
* ipcs -q
* Should show a new entry with the msqid above.
*
* Delete the queue when done:
* ipcrm -q
*/
return 0;
}
IPC_CREAT— create queue if it does not existIPC_EXCL— fail withEEXISTif queue already exists (used with IPC_CREAT)- Permission bits — lower 9 bits are the read/write/execute mask (execute is ignored for IPC)
- No flags /
0— open existing queue; fail withENOENTif it does not exist
The following end-to-end example shows a parent creating a queue, a child sending messages of different types, and the parent receiving them selectively by type — demonstrating the core message queue workflow.
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <wait.h>
#define MSG_TYPE_PING 1
#define MSG_TYPE_DATA 2
#define MSG_TYPE_DONE 3
struct msg_buf {
long mtype;
char text[128];
};
int main(void) {
int msqid;
struct msg_buf msg;
pid_t pid;
/* Create a private message queue (IPC_PRIVATE = always new, unique) */
msqid = msgget(IPC_PRIVATE, IPC_CREAT | 0600);
if (msqid == -1) { perror("msgget"); exit(1); }
printf("Created queue, msqid = %d\n", msqid);
pid = fork();
if (pid == -1) { perror("fork"); exit(1); }
if (pid == 0) {
/* ===== CHILD: sends messages ===== */
msg.mtype = MSG_TYPE_PING;
strcpy(msg.text, "Are you alive?");
msgsnd(msqid, &msg, strlen(msg.text)+1, 0);
msg.mtype = MSG_TYPE_DATA;
strcpy(msg.text, "Temperature=36.5");
msgsnd(msqid, &msg, strlen(msg.text)+1, 0);
msg.mtype = MSG_TYPE_DATA;
strcpy(msg.text, "Humidity=72%");
msgsnd(msqid, &msg, strlen(msg.text)+1, 0);
msg.mtype = MSG_TYPE_DONE;
strcpy(msg.text, "All done.");
msgsnd(msqid, &msg, strlen(msg.text)+1, 0);
printf("Child: sent 4 messages\n");
exit(0);
}
/* ===== PARENT: receives messages selectively ===== */
wait(NULL); /* wait for child to finish sending */
/* Read only DATA messages first */
printf("\n--- Receiving DATA messages ---\n");
while (msgrcv(msqid, &msg, sizeof(msg.text), MSG_TYPE_DATA, IPC_NOWAIT) != -1)
printf("DATA: %s\n", msg.text);
/* Now read the PING */
printf("\n--- Receiving PING ---\n");
if (msgrcv(msqid, &msg, sizeof(msg.text), MSG_TYPE_PING, 0) != -1)
printf("PING: %s\n", msg.text);
/* Read the DONE signal */
printf("\n--- Receiving DONE ---\n");
if (msgrcv(msqid, &msg, sizeof(msg.text), MSG_TYPE_DONE, 0) != -1)
printf("DONE: %s\n", msg.text);
/* Delete the queue */
if (msgctl(msqid, IPC_RMID, NULL) == -1)
perror("msgctl IPC_RMID");
else
printf("\nQueue deleted.\n");
/*
* Expected output:
* Created queue, msqid =
* Child: sent 4 messages
*
* --- Receiving DATA messages ---
* DATA: Temperature=36.5
* DATA: Humidity=72%
*
* --- Receiving PING ---
* PING: Are you alive?
*
* --- Receiving DONE ---
* DONE: All done.
*
* Queue deleted.
*
* Notice: DATA messages were read BEFORE PING even though PING
* was sent first — that is type-based selective retrieval.
*/
return 0;
}
msgrcv(), if no message of the requested type exists, the call returns immediately with errno = ENOMSG instead of blocking. This is used in the loop above to drain all DATA messages without hanging.TLPI (Section 46.9, previewed here) lists several reasons why System V message queues are considered a legacy mechanism:
| Limitation | Impact | Alternative |
|---|---|---|
| Cannot use with select / poll / epoll | Cannot multiplex queue I/O with other fds in an event loop | POSIX MQ (supports poll), sockets, FIFOs |
| IPC identifier, not a file descriptor | Incompatible with standard UNIX I/O tools and libraries | POSIX MQ returns mqd_t (file-descriptor-like) |
| Kernel persistence (no auto-cleanup) | Leaked queues consume kernel resources even after all processes exit | Pipes / sockets are auto-cleaned; POSIX MQ can be unlinked |
| Fixed system-wide limits | msgmni / msgmax / msgmnb require tuning via /proc for high-throughput apps | Sockets have more flexible buffering |
| Complex API | msgsnd/msgrcv need a struct with long mtype as first field — error-prone | POSIX MQ has cleaner mq_send / mq_receive interface |
/*
* POSIX message queue (modern alternative) — for comparison
* Compile with: gcc -o posix_mq posix_mq.c -lrt
*/
#include <mqueue.h>
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
int main(void) {
mqd_t mq;
char buf[256];
/* Create or open a POSIX message queue */
mq = mq_open("/my_queue", O_CREAT | O_RDWR, 0600, NULL);
/* mq is a file-descriptor-like handle — works with poll/select! */
/* Send a message (no struct, no mtype boilerplate) */
mq_send(mq, "hello world", 11, 1 /* priority */);
/* Receive a message */
unsigned prio;
mq_receive(mq, buf, sizeof(buf), &prio);
printf("Got: %s (priority %u)\n", buf, prio);
mq_close(mq);
mq_unlink("/my_queue"); /* Remove from filesystem — auto-cleanup */
return 0;
}
/*
* Key differences from System V:
* - Named by path (/my_queue) not by integer key
* - mq_unlink() removes it like a file (no lingering IPC objects)
* - mqd_t works with poll()/select() for event-driven I/O
* - Priority replaces type — always highest priority first
*/
Interview Questions — System V Message Queues Introduction
msgsnd() writes one complete, atomic message and each msgrcv() reads exactly one complete message. Boundaries are preserved automatically by the kernel.msgctl(IPC_RMID) is called or the system reboots.mtype = 0: returns the first message in the queue in FIFO order, ignoring type. (2) mtype > 0: returns the first message with exactly that type value. (3) mtype < 0: returns the first message whose type is less than or equal to the absolute value of mtype (lowest type number first — useful for priority queues).long integer representing the type. This is a fixed interface contract — the kernel reads the type from offset 0 of the buffer. The mtype value must be strictly greater than 0; a value of 0 or negative in msgsnd() causes an EINVAL error.msgrcv() blocks until one arrives. With IPC_NOWAIT, the call returns immediately with -1 and errno = ENOMSG if no matching message exists. This enables non-blocking polling of a message queue.select()/poll()/epoll() for event-driven I/O; (2) kernel persistence means leaked queues consume system resources; (3) fixed system-wide limits require manual tuning; (4) the struct-with-long-mtype API is error-prone. POSIX message queues, FIFOs, and sockets are cleaner, more portable alternatives.mtype = MSG_TYPE_REQUEST. Each client includes its PID in the request payload. The server then sends the reply with mtype = client_pid. Each client calls msgrcv(msqid, &msg, len, getpid(), 0) to receive only the reply addressed to its own PID, ignoring all other messages on the queue.msgrcv() returns -1 with errno = E2BIG. The message remains on the queue untouched. If the MSG_NOERROR flag is passed, msgrcv() silently truncates the message to fit the buffer and removes the (truncated) message from the queue — the remaining data is permanently lost.You have now covered all of Chapter 45: System V IPC overview, keys, permissions, client-server patterns, identifier algorithm, ipcs/ipcrm tools, /proc/sysvipc, IPC limits, and this message queue introduction. Chapter 46 dives deep into msgsnd(), msgrcv(), msgctl(), queue attributes, and limits.
← Previous: /proc/sysvipc & IPC Limits Next: Ch46 Message Queues Deep Dive →
