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_openmq_sendmq_receive, and mq_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 (msggetmsgsndmsgrcvmsgctl) 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.

Feature POSIX Message Queues System V Message Queues
Identifier Human-readable names (e.g., /my_queue) Integer keys, often generated with ftok()
API Paradigm File descriptor-based (like open, read, write) Unique API (msgget, msgsnd, msgrcv)
Message Priority Integrated directly into mq_send() Part of the message structure itself (mtype)
Blocking Behavior Controlled per-descriptor with O_NONBLOCK flag Controlled per-call with IPC_NOWAIT flag
Asynchronous Notification Built-in via signals or thread creation (mq_notify) Not directly supported; requires extra mechanisms
Linker Flag -lrt (Real-time library) None (Part of standard C library)
Persistence Kernel-persistent until explicitly unlinked with mq_unlink() Kernel-persistent until explicitly removed with msgctl()
Recommendation Preferred for all new development on Linux Legacy; used for maintaining older applications

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:

C
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 are O_RDONLY (open for receiving only), O_WRONLY (open for sending only), or O_RDWR (open for sending and receiving). Additionally, you can bitwise-OR this with other flags:
    • O_CREAT: If the queue named name does not already exist, create it. If it does exist, this flag has no effect except when combined with O_EXCL.
    • O_EXCL: When used with O_CREATmq_open() will fail if the queue already exists, returning an error and setting errno to EEXIST. 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 subsequent mq_send() and mq_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 with errno set to EAGAIN instead of blocking the process.
  • mode: This argument is of type mode_t and specifies the permissions for the new queue, just like with files. It’s only required when O_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’s umask.
  • attr: This is a pointer to a struct 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 pass NULL. The mq_attr structure is key to tuning the queue’s behavior:
C
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 and mq_msgsize. The kernel will ignore mq_flags and mq_curmsgs in the attr argument passed to mq_open(). The mq_curmsgs field is only ever an output value, used when retrieving attributes with mq_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:

C
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 successful mq_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 by msg_ptr. This value must be less than or equal to the mq_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:

C
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 by msg_ptr. This must be greater than or equal to the mq_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 pass NULL.

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 to close() for a file descriptor. It closes the connection between the process and the message queue associated with the descriptor mqdes. 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 to unlink() or remove() 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 called mq_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:

Bash
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.

Plaintext
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

Plaintext
# 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.

C
// 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.

C
// 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.

Bash
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.
Bash
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.
Bash
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):

Bash
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.

Mistake / Issue Symptom(s) Troubleshooting / Solution
Forgetting -lrt Linker errors like:
undefined reference to ‘mq_open’
The message queue functions are in the real-time library. Always add the -lrt flag to the end of your GCC command.
Permission Denied mq_open() fails and perror() prints “Permission denied”. Check the mode argument in mq_open() (e.g., 0660). Use ls -l /dev/mqueue/ to inspect the queue file’s permissions. The user running the process must have access.
Invalid Argument mq_open() or other calls fail with “Invalid argument”. – Queue name must start with a single /.
– For mq_send(), check that message size ≤ mq_msgsize.
– For mq_receive(), ensure your buffer size ≥ mq_msgsize.
Leaking Queues System runs out of memory over time. /dev/mqueue is full of old queues. A process must call mq_unlink() to mark a queue for deletion. Ensure your application has a cleanup path, especially on exit or crash.
Mismatched Attributes Receiver fails to open the queue, or messages appear truncated or corrupted. All processes must agree on the queue’s attributes, especially mq_msgsize. Define attributes in a shared header file.
Blocking Indefinitely A receiver process hangs and becomes unresponsive. The process is likely blocked on an mq_receive() call to an empty queue. Ensure a sender is running. For polling, open the queue with the O_NONBLOCK flag.

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.

  1. Echo Service with Two Queues
    • Objective: Create a client/server application using two message queues for bidirectional communication.
    • Task:
      1. Write a “server” program that creates two queues: /client_to_server_q and /server_to_client_q.
      2. The server should wait for a message on /client_to_server_q.
      3. Write a “client” program that opens both queues. It should send a string message (e.g., “Hello from client!”) to /client_to_server_q.
      4. 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.
      5. The client should then wait to receive the response on /server_to_client_q and print it.
    • Verification: The client should successfully print the uppercase version of the message it sent.
  2. Message Priority Triage
    • Objective: Understand and utilize message priorities to process urgent data first.
    • Task:
      1. Modify the chapter’s sender.c program. Have it send three “NORMAL” priority (e.g., priority 1) messages.
      2. Immediately after, have it send one “CRITICAL” priority message (e.g., priority 10).
      3. Finally, have it send two more “NORMAL” priority messages.
      4. Modify receiver.c to print the priority of each message it receives, which it already does.
    • 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.
  3. Non-Blocking Queue Polling
    • Objective: Implement a non-blocking receive to allow a process to check for messages without getting stuck.
    • Task:
      1. Modify receiver.c. When opening the message queue with mq_open(), add the O_NONBLOCK flag.
      2. Instead of a single blocking mq_receive() call, create a while loop.
      3. Inside the loop, call mq_receive(). If it returns -1, check if errno is EAGAIN. If it is, this means the queue is empty. Print a message like “Queue empty, doing other work…” and sleep() for a second before trying again.
      4. If mq_receive() is successful, process the message as before.
    • 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.
  4. Queue Inspector Utility
    • Objective: Use the mq_getattr() function to create a command-line tool to inspect the state of a queue.
    • Task:
      1. Write a new program called mq_inspector.
      2. It should take one command-line argument: the name of a message queue (e.g., ./mq_inspector /temp_sensor_q).
      3. The program should mq_open() the specified queue in O_RDONLY mode.
      4. It should then call mq_getattr() to retrieve the queue’s attributes into a struct mq_attr.
      5. 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.
      6. The program must then mq_close() the queue.
    • Verification: While the sender and receiver 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.

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 (openreadwriteclose), 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), and mq_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

  1. The Linux Programming Interface by Michael Kerrisk. Chapters 52-54 provide an exhaustive and authoritative reference on POSIX message queues, semaphores, and shared memory.
  2. 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)
  3. mq_overview(7) Linux Manual Pageman 7 mq_overviewThis man page gives a fantastic high-level overview of the POSIX message queue implementation in Linux, including details about the /dev/mqueue filesystem.
  4. 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.
  5. Beej’s Guide to Unix Interprocess CommunicationAn accessible, practical online guide that covers message queues alongside other IPC methods in a friendly tone. (https://beej.us/guide/bgipc/)
  6. “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.
  7. Raspberry Pi DocumentationWhile 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/)

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top