Chapter 76: Inter-Process Communication: POSIX Message Queues
Chapter Objectives
By the end of this chapter, you will be able to:
- Understand the fundamental principles of message queues as an Inter-Process Communication (IPC) mechanism.
- Explain the advantages of POSIX message queues over their older System V counterparts, particularly in the context of embedded systems.
- Implement robust communication between independent processes on an embedded Linux system using the core POSIX message queue API functions:
mq_open
,mq_send
,mq_receive
, andmq_close
. - Configure and manage message queue attributes, including priority and blocking behavior, to suit different application requirements.
- Debug and troubleshoot common issues related to permissions, library linking, and resource management in message queue applications.
- Design and build modular, multi-process applications on a Raspberry Pi 5 that leverage message queues for reliable data exchange.
Introduction
In the world of embedded Linux, systems are rarely monolithic. They are often composed of multiple, independent processes working in concert to achieve a common goal. A typical embedded device, such as an industrial controller or a smart home hub, might have one process dedicated to reading sensor data, another managing a user interface, a third handling network communication, and a fourth controlling actuators. The critical question then becomes: how do these disparate processes communicate reliably and efficiently? This is the domain of Inter-Process Communication (IPC).
While the Linux kernel offers a rich variety of IPC mechanisms—pipes, FIFOs, signals, and shared memory—message queues hold a special place due to their unique blend of features. They allow processes to exchange data in the form of discrete packets, or messages, without needing to be synchronized or even running at the same time. This asynchronous, message-passing paradigm is exceptionally well-suited to the event-driven nature of many embedded applications.
This chapter focuses on POSIX message queues, a modern, standardized, and more intuitive alternative to the older System V message queue API. We will explore why this specific IPC mechanism is so valuable for embedded developers. Unlike simple pipes, message queues are not just unstructured byte streams; they preserve message boundaries, ensuring that data sent as a distinct packet is received as one. Furthermore, they introduce the concept of message priority, allowing high-priority data (like a critical system alert) to be processed before less urgent information (like a routine log entry). As we will see, POSIX message queues, with their file-like interface and cleaner semantics, provide a powerful tool for building decoupled, scalable, and robust embedded software architectures on platforms like the Raspberry Pi 5.
Technical Background
At its core, a message queue is a kernel-persistent list of messages, a data structure maintained within the operating system’s memory space that acts as a sort of “mailbox” for processes. One or more processes can write messages to the queue, and one or more other processes can read messages from it. This architecture decouples the sender from the receiver. The sending process can post a message and continue with its work, confident that the kernel will hold onto that message until the receiving process is ready to retrieve it. This persistence is a key differentiator; unlike pipes, the queue and its contents can persist even if no process currently has it open.

The Evolution from System V to POSIX
Before the advent of the POSIX standard for message queues, UNIX systems provided a different implementation known as System V message queues. While functional, the System V API is often considered clunky and less intuitive by modern programming standards. It uses a key-based system (ftok
) for identifying queues, which can be cumbersome to manage, and its API functions (msgget
, msgsnd
, msgrcv
, msgctl
) are less consistent with the familiar file I/O paradigm of “open, read, write, close” that is a hallmark of UNIX-like systems.
The POSIX standard (specifically, IEEE 1003.1b, for real-time extensions) sought to remedy these shortcomings. POSIX message queues were designed from the ground up to be more developer-friendly. They are identified by simple human-readable names (e.g., /my_app_queue
), much like files in a filesystem. In fact, on Linux, they are often implemented using a dedicated virtual filesystem called mqfs
. This design choice means that familiar tools and concepts, such as file permissions, can be applied to manage access to the queues. The API is cleaner, more consistent, and integrates features like message priority and asynchronous notification in a more direct and powerful way. For these reasons, POSIX message queues are now the preferred choice for new development on Linux and other POSIX-compliant systems.
The POSIX Message Queue API
The heart of using POSIX message queues lies in a handful of key functions, which must be linked by passing the -lrt
(real-time library) flag to the compiler. Let’s explore the lifecycle of a message queue through its API.
1. Creating and Opening a Queue: mq_open()
The journey begins with mq_open()
. This single function handles both creating a new queue and opening an existing one, much like the standard open()
system call for files.
Its prototype is:
mqd_t mq_open(const char *name, int oflag, mode_t mode, struct mq_attr *attr);
name
: This is a null-terminated string that identifies the queue. It must start with a forward slash (/
) and should not contain any other slashes. For example,/sensor_data_q
is a valid name. This naming convention makes it easy to create unique, well-defined endpoints for communication.oflag
: This integer argument is a bitmask that specifies the access mode and controls the function’s behavior. The access modes areO_RDONLY
(open for receiving only),O_WRONLY
(open for sending only), orO_RDWR
(open for sending and receiving). Additionally, you can bitwise-OR this with other flags:O_CREAT
: If the queue namedname
does not already exist, create it. If it does exist, this flag has no effect except when combined withO_EXCL
.O_EXCL
: When used withO_CREAT
,mq_open()
will fail if the queue already exists, returning an error and settingerrno
toEEXIST
. This is a crucial atomic operation for ensuring that a process is the exclusive creator of a queue, preventing race conditions.O_NONBLOCK
: This flag affects the behavior of subsequentmq_send()
andmq_receive()
calls on the returned descriptor. In non-blocking mode, if a send cannot proceed because the queue is full, or a receive cannot proceed because the queue is empty, the call will fail immediately witherrno
set toEAGAIN
instead of blocking the process.
mode
: This argument is of typemode_t
and specifies the permissions for the new queue, just like with files. It’s only required whenO_CREAT
is specified. For example,0666
would grant read and write permissions to the owner, group, and others. These permissions are masked by the process’sumask
.attr
: This is a pointer to astruct mq_attr
structure, which allows you to specify attributes for a new queue. If you are opening an existing queue or are content with the system’s default attributes, you can passNULL
. Themq_attr
structure is key to tuning the queue’s behavior:
struct mq_attr {
long mq_flags; /* Flags (0 or O_NONBLOCK) */
long mq_maxmsg; /* Max. number of messages on queue */
long mq_msgsize; /* Max. message size (in bytes) */
long mq_curmsgs; /* No. of messages currently in queue */
};
- When creating a queue, you only need to set
mq_maxmsg
andmq_msgsize
. The kernel will ignoremq_flags
andmq_curmsgs
in theattr
argument passed tomq_open()
. Themq_curmsgs
field is only ever an output value, used when retrieving attributes withmq_getattr()
.
Upon success, mq_open()
returns a message queue descriptor of type mqd_t
, which is analogous to a file descriptor. This descriptor is then used in all subsequent operations on the queue. On failure, it returns (mqd_t) -1
and sets errno
to indicate the error.
2. Sending a Message: mq_send()
Once a queue is open for writing, a process can send messages to it using mq_send()
.
The prototype is:
int mq_send(mqd_t mqdes, const char *msg_ptr, size_t msg_len, unsigned int msg_prio);
mqdes
: The message queue descriptor returned by a successfulmq_open()
call.msg_ptr
: A pointer to the buffer containing the message to be sent. This is the actual data payload.msg_len
: The size, in bytes, of the message pointed to bymsg_ptr
. This value must be less than or equal to themq_msgsize
attribute of the queue.msg_prio
: An unsigned integer representing the priority of the message. Priorities are non-negative, with higher numbers denoting higher priority. The Linux implementation supports priorities from 0 up to a system-defined limit (MQ_PRIO_MAX - 1
), which is typically 32767.
The mq_send()
function adds the message specified by msg_ptr
and msg_len
to the queue identified by mqdes
. Messages are placed in the queue in decreasing order of priority. For messages of the same priority, the new message is enqueued after existing messages of that same priority (First-In, First-Out behavior).
If the queue is full (i.e., mq_curmsgs
equals mq_maxmsg
), the behavior of mq_send()
depends on whether the O_NONBLOCK
flag was set for the descriptor. If O_NONBLOCK
is not set (the default), the call will block until space becomes available in the queue. If O_NONBLOCK
is set, the call fails immediately with errno
set to EAGAIN
.
3. Receiving a Message: mq_receive()
To retrieve a message, a process with a queue open for reading uses mq_receive()
.
The prototype is:
ssize_t mq_receive(mqd_t mqdes, char *msg_ptr, size_t msg_len, unsigned int *msg_prio);
mqdes
: The message queue descriptor.msg_ptr
: A pointer to a buffer where the received message will be stored.msg_len
: The size of the buffer pointed to bymsg_ptr
. This must be greater than or equal to themq_msgsize
of the queue to ensure it can accommodate the largest possible message.msg_prio
: A pointer to an unsigned integer where the priority of the received message will be stored. If you are not interested in the priority, you can passNULL
.
mq_receive()
retrieves the highest-priority, oldest message from the queue. If the queue is empty, its behavior, like mq_send()
, is governed by the O_NONBLOCK
flag. By default (blocking mode), the call will wait until a message arrives. In non-blocking mode, it will fail immediately with errno
set to EAGAIN
.
Upon success, mq_receive()
returns the number of bytes in the received message. On failure, it returns -1 and sets errno
. A common mistake is to assume the size of the received message is msg_len
; the return value is the authoritative source for the actual message length.
4. Closing and Unlinking a Queue
Proper resource management is crucial in any system, and embedded systems are no exception. POSIX message queues have a two-stage cleanup process: closing and unlinking.
mq_close(mqd_t mqdes)
: This function is analogous toclose()
for a file descriptor. It closes the connection between the process and the message queue associated with the descriptormqdes
. If the calling process is the last one to have this particular queue open, this call does not destroy the queue itself. The queue and any messages within it persist in the kernel. This is a deliberate design feature that supports the decoupled nature of the communication.mq_unlink(const char *name)
: This function is what actually removes a message queue from the system. It’s analogous tounlink()
orremove()
for a file. Once a queue is unlinked, its name is removed. The queue’s resources (the memory holding its messages) are only deallocated once all processes that have it open have calledmq_close()
. This means a process can unlink a queue immediately after creating it and continue to use its descriptor. This is a common pattern to ensure that the queue is automatically cleaned up when the process exits, even if it crashes.
graph TD subgraph "Initialization" A[Start: Process A] A --> B{"mq_open(\<i>/my_q\</i>, O_CREAT)"}; style A fill:#1e3a8a,stroke:#1e3a8a,stroke-width:2px,color:#ffffff B --> C[Queue <i>/my_q</i> created in Kernel]; style C fill:#8b5cf6,stroke:#8b5cf6,stroke-width:1px,color:#ffffff end subgraph "Communication Phase" C --> D{Processes A & B use descriptor}; D --> E["mq_send()"]; style E fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff D --> F["mq_receive()"]; style F fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff end subgraph "Cleanup Phase" E --> G{"Process A calls<br>mq_unlink(\<i>/my_q\</i>)"}; F --> G; style G fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff G --> H["Name '/my_q' is removed.<br><i>Queue still exists in memory.</i>"]; style H fill:#eab308,stroke:#eab308,stroke-width:1px,color:#1f2937 H --> I{"Processes A & B call<br>mq_close()"}; style I fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff I --> J[Last open descriptor is closed]; J --> K[Kernel deallocates queue & messages]; style K fill:#8b5cf6,stroke:#8b5cf6,stroke-width:1px,color:#ffffff end K --> L[End: Resources Freed]; style L fill:#10b981,stroke:#10b981,stroke-width:2px,color:#ffffff
Message Queue Filesystem (mqfs
)
On Linux, POSIX message queues are implemented as a virtual filesystem. You can typically mount and inspect it. This provides a tangible way to see the queues that exist on your system.
To mount it (if not already mounted), you can use the command:
sudo mount -t mqueue none /dev/mqueue
Once mounted, you can list the contents of /dev/mqueue
. Each file in this directory corresponds to a POSIX message queue. The file’s name will be the queue’s name (without the leading slash). You can use standard commands like ls -l
to see permissions and cat
to view attributes like the queue size, message count, and notification settings. This is an invaluable tool for debugging.
Tip: Inspecting
/dev/mqueue/
is a great first step when troubleshooting. If your queue doesn’t appear there, it was likely never created successfully. If it’s there but your program can’t open it, it’s probably a permissions issue.
Practical Examples
Theory is essential, but the best way to understand a concept is to apply it. In this section, we will build a practical, two-process application on the Raspberry Pi 5. One process, the sender, will simulate a temperature sensor, periodically sending readings to a message queue. The second process, the receiver, will read these temperature readings and print a warning if they exceed a certain threshold.
This example demonstrates a common embedded systems pattern: a dedicated data-acquisition process communicating with a data-processing or logic-handling process.
graph LR subgraph "Receiver Process (receiver.c)" R1[Start] --> R2{"mq_open(QUEUE_NAME, O_RDONLY)"}; style R1 fill:#1e3a8a,stroke:#1e3a8a,stroke-width:2px,color:#ffffff style R2 fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff R2 --> R3[Loop indefinitely]; R3 --> R4{"mq_receive() <br><i>Blocks here...</i>"}; style R4 fill:#eab308,stroke:#eab308,stroke-width:1px,color:#1f2937 R4 --> R5{Is it shutdown message?}; style R5 fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff R5 -- Yes --> R9{break loop}; R5 -- No --> R6{Temp > Threshold?}; style R6 fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff R6 -- Yes --> R7[Print WARNING]; style R7 fill:#ef4444,stroke:#ef4444,stroke-width:1px,color:#ffffff R7 --> R8[Print reading]; R6 -- No --> R8; R8 --> R3; R9 --> R10{"mq_close()"}; R10 --> R11{"mq_unlink()"}; style R11 fill:#ef4444,stroke:#ef4444,stroke-width:1px,color:#ffffff R11 --> R12[End]; style R12 fill:#10b981,stroke:#10b981,stroke-width:2px,color:#ffffff end subgraph "Sender Process (sender.c)" S1[Start] --> S2{Set mq_attr}; style S1 fill:#1e3a8a,stroke:#1e3a8a,stroke-width:2px,color:#ffffff S2 --> S3{"mq_open(QUEUE_NAME, O_CREAT | O_WRONLY)"}; style S3 fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff S3 --> S4[Loop 20 times]; S4 --> S5[Generate random temperature]; style S5 fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff S5 --> S6{"mq_send(temp_reading, prio=1)"}; style S6 fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff S6 --> S7["sleep(2)"]; S7 --> S4; S4 --> S8{"mq_send(shutdown_msg, prio=10)"}; style S8 fill:#ef4444,stroke:#ef4444,stroke-width:1px,color:#ffffff S8 --> S9{"mq_close()"}; S9 --> S10[End]; style S10 fill:#10b981,stroke:#10b981,stroke-width:2px,color:#ffffff end
Hardware and Software Setup
- Hardware: Raspberry Pi 5
- Operating System: Raspberry Pi OS (or any other modern Linux distribution)
- Compiler: GCC
- Library: Real-time library (
librt
)
No special hardware connections are needed, as this is a software-only IPC example. We will run both programs from the command line in separate terminal windows.
File Structure
Let’s organize our project in a simple directory.
rpi_mq_example/
├── sender.c
├── receiver.c
└── Makefile
The Code
First, we need a simple Makefile
to streamline compilation. It’s important to remember to link against the real-time library with -lrt
.
Makefile
# Makefile for POSIX Message Queue Example
# Compiler
CC = gcc
# Compiler flags
# -g: Add debug information
# -Wall: Turn on all warnings
# -lrt: Link with the real-time library for POSIX MQ functions
CFLAGS = -g -Wall
LDFLAGS = -lrt
# Target executables
TARGETS = sender receiver
all: $(TARGETS)
sender: sender.c
$(CC) $(CFLAGS) -o sender sender.c $(LDFLAGS)
receiver: receiver.c
$(CC) $(CFLAGS) -o receiver receiver.c $(LDFLAGS)
clean:
rm -f $(TARGETS)
Now, let’s write the C code for our two processes.
sender.c
– The Temperature Sensor Simulator
This program will create a message queue, then enter a loop where it generates a random temperature reading every two seconds and sends it to the queue with a specific priority.
// sender.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <mqueue.h>
// Define the name of the message queue
#define QUEUE_NAME "/temp_sensor_q"
// Define the maximum number of messages
#define MAX_MESSAGES 10
// Define the maximum message size
#define MAX_MSG_SIZE 256
// A simple structure for our message
typedef struct {
int sensor_id;
float temperature;
} temp_reading_t;
int main() {
mqd_t mq;
struct mq_attr attr;
temp_reading_t reading;
int counter = 0;
// Set the attributes of the queue
attr.mq_flags = 0;
attr.mq_maxmsg = MAX_MESSAGES;
attr.mq_msgsize = sizeof(temp_reading_t);
attr.mq_curmsgs = 0;
// Create the message queue with read-write permissions for owner and group
// O_CREAT | O_WRONLY: Create if it doesn't exist, open for writing only
printf("Sender: Opening message queue...\n");
mq = mq_open(QUEUE_NAME, O_CREAT | O_WRONLY, 0660, &attr);
if (mq == (mqd_t)-1) {
perror("mq_open");
exit(1);
}
// Seed the random number generator
srand(time(NULL));
printf("Sender: Starting to send temperature readings.\n");
while (counter < 20) {
// Prepare the message
reading.sensor_id = 1;
// Generate a random temperature between 15.0 and 45.0
reading.temperature = 15.0 + (float)rand() / ((float)RAND_MAX / 30.0);
// Send the message with priority 1
// A higher number means higher priority
if (mq_send(mq, (const char *)&reading, sizeof(temp_reading_t), 1) == -1) {
perror("mq_send");
} else {
printf("Sender: Sent reading #%d - Temp: %.2f°C\n", counter + 1, reading.temperature);
}
counter++;
sleep(2); // Wait for 2 seconds
}
// Send a final "shutdown" message with a higher priority
reading.sensor_id = -1; // Special value to indicate shutdown
reading.temperature = 0.0;
printf("Sender: Sending shutdown message.\n");
if (mq_send(mq, (const char *)&reading, sizeof(temp_reading_t), 10) == -1) {
perror("mq_send");
}
// Close the message queue
printf("Sender: Closing message queue.\n");
if (mq_close(mq) == -1) {
perror("mq_close");
exit(1);
}
return 0;
}
receiver.c
– The Monitoring Process
This program opens the same message queue and waits to receive messages. It processes each temperature reading, printing a warning for high temperatures. It stops when it receives a special shutdown message.
// receiver.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <mqueue.h>
// Use the same definitions as the sender
#define QUEUE_NAME "/temp_sensor_q"
#define MAX_MESSAGES 10
#define MAX_MSG_SIZE 256 // Should be >= sender's message size
#define TEMP_THRESHOLD 38.0
typedef struct {
int sensor_id;
float temperature;
} temp_reading_t;
int main() {
mqd_t mq;
struct mq_attr attr;
temp_reading_t received_reading;
ssize_t bytes_read;
unsigned int priority;
// Set attributes to match queue creation
// These are used by mq_open to verify, but are more critical for creation
attr.mq_maxmsg = MAX_MESSAGES;
attr.mq_msgsize = sizeof(temp_reading_t);
// Open the message queue for reading only
printf("Receiver: Opening message queue...\n");
mq = mq_open(QUEUE_NAME, O_RDONLY);
if (mq == (mqd_t)-1) {
perror("mq_open");
exit(1);
}
printf("Receiver: Waiting for temperature readings...\n");
while (1) {
// Receive a message. This call will block until a message is available.
bytes_read = mq_receive(mq, (char *)&received_reading, sizeof(temp_reading_t), &priority);
if (bytes_read == -1) {
perror("mq_receive");
exit(1);
}
// Check if it's the shutdown message
if (received_reading.sensor_id == -1) {
printf("Receiver: Shutdown message received. Exiting.\n");
break;
}
// Process the received message
printf("Receiver: Got reading from Sensor ID %d, Temp: %.2f°C (Priority: %u)\n",
received_reading.sensor_id, received_reading.temperature, priority);
if (received_reading.temperature > TEMP_THRESHOLD) {
printf("!!! WARNING: High temperature detected: %.2f°C !!!\n", received_reading.temperature);
}
}
// Close the message queue
printf("Receiver: Closing message queue.\n");
if (mq_close(mq) == -1) {
perror("mq_close");
}
// The receiver should be the one to unlink the queue to clean it up
printf("Receiver: Unlinking message queue.\n");
if (mq_unlink(QUEUE_NAME) == -1) {
perror("mq_unlink");
// This might fail if the sender exits first and already unlinked it,
// which is not an issue in this specific design.
}
return 0;
}
Build, Flash, and Boot Procedures
Since we are developing directly on the Raspberry Pi 5, the process is straightforward compilation and execution, not cross-compilation and flashing.
Step 1: Build the Code
Open a terminal on your Raspberry Pi 5, navigate to the rpi_mq_example
directory, and run make
.
pi@raspberrypi:~/rpi_mq_example $ make
gcc -g -Wall -o sender sender.c -lrt
gcc -g -Wall -o receiver receiver.c -lrt
This will create two executable files: sender
and receiver
.
Step 2: Run the Application
You will need two separate terminal windows.
- In Terminal 1 (Receiver): Start the receiver process first. It will open the queue and then block, waiting for messages.
pi@raspberrypi:~/rpi_mq_example $ ./receiver
Receiver: Opening message queue...
Receiver: Waiting for temperature readings...
- In Terminal 2 (Sender): Now, start the sender process. It will create the queue (or open the existing one if the receiver was started with create permissions, though our code is designed for the sender to create it) and begin sending messages.
pi@raspberrypi:~/rpi_mq_example $ ./sender
Sender: Opening message queue...
Sender: Starting to send temperature readings.
Sender: Sent reading #1 - Temp: 23.45°C
Sender: Sent reading #2 - Temp: 39.12°C
Sender: Sent reading #3 - Temp: 18.99°C
...
Step 3: Observe the Output
As the sender runs, you will see corresponding output appear in the receiver’s terminal window.
Expected Output in Terminal 1 (Receiver):
Receiver: Opening message queue...
Receiver: Waiting for temperature readings...
Receiver: Got reading from Sensor ID 1, Temp: 23.45°C (Priority: 1)
Receiver: Got reading from Sensor ID 1, Temp: 39.12°C (Priority: 1)
!!! WARNING: High temperature detected: 39.12°C !!!
Receiver: Got reading from Sensor ID 1, Temp: 18.99°C (Priority: 1)
...
// After sender finishes
Receiver: Shutdown message received. Exiting.
Receiver: Closing message queue.
Receiver: Unlinking message queue.
This simple yet powerful example demonstrates the decoupled nature of message queues. The receiver doesn’t need to know anything about the sender’s internal logic, only the name of the queue and the format of the messages. The sender can post messages and move on, trusting the kernel to deliver them. The final mq_unlink
call by the receiver ensures the system resources are freed, which is a critical aspect of robust embedded software design.
Common Mistakes & Troubleshooting
While the POSIX message queue API is more straightforward than its predecessors, developers new to it can still encounter a few common pitfalls. Understanding these can save hours of debugging.
Exercises
These exercises are designed to reinforce the concepts covered in this chapter and encourage you to explore the nuances of the POSIX message queue API.
- Echo Service with Two Queues
- Objective: Create a client/server application using two message queues for bidirectional communication.
- Task:
- Write a “server” program that creates two queues:
/client_to_server_q
and/server_to_client_q
. - The server should wait for a message on
/client_to_server_q
. - Write a “client” program that opens both queues. It should send a string message (e.g., “Hello from client!”) to
/client_to_server_q
. - When the server receives the message, it should print it, convert it to uppercase, and send the modified string back to the client via
/server_to_client_q
. - The client should then wait to receive the response on
/server_to_client_q
and print it.
- Write a “server” program that creates two queues:
- Verification: The client should successfully print the uppercase version of the message it sent.
- Message Priority Triage
- Objective: Understand and utilize message priorities to process urgent data first.
- Task:
- Modify the chapter’s
sender.c
program. Have it send three “NORMAL” priority (e.g., priority 1) messages. - Immediately after, have it send one “CRITICAL” priority message (e.g., priority 10).
- Finally, have it send two more “NORMAL” priority messages.
- Modify
receiver.c
to print the priority of each message it receives, which it already does.
- Modify the chapter’s
- Verification: Run the receiver, then the sender. Observe the output on the receiver’s terminal. The “CRITICAL” message should be received before the two “NORMAL” messages that were sent after it, demonstrating that the higher-priority message jumped the queue.
- Non-Blocking Queue Polling
- Objective: Implement a non-blocking receive to allow a process to check for messages without getting stuck.
- Task:
- Modify
receiver.c
. When opening the message queue withmq_open()
, add theO_NONBLOCK
flag. - Instead of a single blocking
mq_receive()
call, create awhile
loop. - Inside the loop, call
mq_receive()
. If it returns -1, check iferrno
isEAGAIN
. If it is, this means the queue is empty. Print a message like “Queue empty, doing other work…” andsleep()
for a second before trying again. - If
mq_receive()
is successful, process the message as before.
- Modify
- Verification: Run the modified receiver. It should repeatedly print its “Queue empty” message. Then, run the original sender. The receiver should now start processing the incoming messages as they arrive, interspersed with its polling messages when the queue is momentarily empty.
- Queue Inspector Utility
- Objective: Use the
mq_getattr()
function to create a command-line tool to inspect the state of a queue. - Task:
- Write a new program called
mq_inspector
. - It should take one command-line argument: the name of a message queue (e.g.,
./mq_inspector /temp_sensor_q
). - The program should
mq_open()
the specified queue inO_RDONLY
mode. - It should then call
mq_getattr()
to retrieve the queue’s attributes into astruct mq_attr
. - Finally, it should print out all the attributes in a clean, human-readable format: max messages, max message size, current flags, and current number of messages.
- The program must then
mq_close()
the queue.
- Write a new program called
- Verification: While the
sender
andreceiver
from the main example are running, execute your inspector:./mq_inspector /temp_sensor_q
. The output should show the correct attributes and the number of messages currently waiting in the queue.
- Objective: Use the
Summary
This chapter provided a comprehensive introduction to POSIX message queues, a powerful and modern IPC mechanism essential for building modular embedded Linux applications.
- Core Concept: Message queues provide a kernel-managed, persistent “mailbox” for processes to exchange discrete, prioritized packets of data.
- POSIX vs. System V: POSIX message queues offer a superior, more intuitive API based on the familiar file I/O model (
open
,read
,write
,close
), using human-readable names instead of complex keys. - Key API Functions: We learned the complete lifecycle of a queue through
mq_open
(create/open),mq_send
(send data with priority),mq_receive
(receive highest-priority message),mq_close
(close descriptor), andmq_unlink
(remove queue from system). - Essential Compiler Flag: All programs using the POSIX MQ API must be linked with the real-time library by specifying
-lrt
. - Queue Attributes: The
struct mq_attr
allows fine-grained control over a queue’s capacity (mq_maxmsg
) and message size (mq_msgsize
), which are critical for resource management. - Blocking vs. Non-Blocking: The
O_NONBLOCK
flag fundamentally changes the behavior of send and receive operations on full or empty queues, enabling polling and preventing processes from getting stuck. - Cleanup is Critical: Proper use of
mq_unlink
is necessary to prevent resource leaks by ensuring queues are removed from the system after use.
By mastering POSIX message queues, you have gained a vital skill for designing robust, decoupled, and scalable software architectures on the Raspberry Pi 5 and other embedded Linux platforms.
Further Reading
- The Linux Programming Interface by Michael Kerrisk. Chapters 52-54 provide an exhaustive and authoritative reference on POSIX message queues, semaphores, and shared memory.
- Official POSIX Standard for
mqueue.h
: The Open Group Base Specifications. This is the primary source for the standard itself, defining the precise behavior of each function. (https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/mqueue.h.html) mq_overview(7)
Linux Manual Page:man 7 mq_overview
. This man page gives a fantastic high-level overview of the POSIX message queue implementation in Linux, including details about the/dev/mqueue
filesystem.- Advanced Programming in the UNIX Environment by W. Richard Stevens and Stephen A. Rago. While it covers many IPC mechanisms, its treatment of the principles behind message passing is foundational.
- Beej’s Guide to Unix Interprocess Communication: An accessible, practical online guide that covers message queues alongside other IPC methods in a friendly tone. (https://beej.us/guide/bgipc/)
- “POSIX vs. System V IPC: A Developer’s Guide”: A technical blog or article comparing the two systems. Many high-quality articles on sites like LWN.net or personal engineering blogs delve into the practical differences and historical context.
- Raspberry Pi Documentation: While not specific to message queues, the official documentation provides the context for the hardware platform and operating system environment where these concepts are applied. (https://www.raspberrypi.com/documentation/)