1. Introduction
Message queues provide a buffer where processes can asynchronously send and receive messages in inter-process communication. By decoupling the sender and receiver, message queues enable seamless communication and empower developers to design robust and interconnected systems.
In this tutorial, we’ll delve into the world of message queues in the Linux kernel. We’ll understand the inner workings of these communication mechanisms and learn new possibilities to use the power of message queues in our projects. Let’s get started!
2. Kernel Data Structures
To grasp the implementation of message queues in the Linux kernel, let’s first familiarize ourselves with the underlying data structures involved.
At the core of message queue implementation lies the struct msg_queue data structure. It represents a message queue and includes required fields like the message buffer, metadata, and synchronization primitives. The kernel manages these data structures to form the foundation for message queue operations.
The Linux kernel implements two types of message queues – System V IPC Messages and Portable Operating System Interface (POSIX) Message Queue. Although both facilitate inter-process communication, they differ in features and capabilities.
2.1. System V IPC Messages
Linux implements System V IPC messages as linked lists, deviating from a strict First-In-First-Out (FIFO) principle.
To use System V IPC, a process invokes the msgsnd() function to send a message. It takes the IPC identifier of the receiving message queue, the message size, and a message structure containing the message type and text.
On the receiving side, another process invokes the msgrcv() function to receive a message. This function requires the IPC identifier of the message queue, a buffer to store the message, the buffer size, and a value of t. The value of t determines the message returned from the queue. However, a positive value retrieves the first message with a type equal to t, a negative value retrieves the last message with a type equal to t, and zero retrieves the first message in the queue.
The functions msgsnd() and msgrcv() can be found in include/linux/msg.h according to the ipc/msg.c implementation.
Additionally, there are limitations on the message size, the total number of messages, and the total size of all messages in the queue.
2.2. POSIX Message Queue
Linux also implements POSIX Message Queue for applications, extending message queues’ functionality. The POSIX standard introduces a simple file-based interface that makes it easy for applications to interact with message queues.
The POSIX Message Queue implementation in the Linux kernel is in ipc/mqueue.c and offers several advantages over the System V IPC message queues. One notable feature is the support for message priorities. This allows the receiving process to retrieve them in a specific order based on their importance.
Additionally, the POSIX Message Queue uses asynchronous notifications. Processes can register to receive notifications when new messages arrive in the queue. This feature eliminates the need for continuous polling, enhancing efficiency and responsiveness in message-based communication scenarios.
With these additional functionalities, the POSIX Message Queue offers a more versatile and feature-rich application communication mechanism. Whether we need to prioritize messages, receive asynchronous notifications, or handle timeouts, it provides the tools to meet our application requirements effectively.
2.3. Check Default Message Queue Settings
Let’s check our system’s default message queue settings to be aware of existing configurations. The sysctl command provides a convenient way to access and manipulate kernel parameters via a virtual file system, usually located at /proc/sys/. By specifying the appropriate parameter name, we can query the current value of a specific setting.
In this case, we use the sysctl command to retrieve the default kernel message queue settings:
$ sysctl kernel.msg{max,mni,mnb}
kernel.msgmax = 8192
kernel.msgmni = 32000
kernel.msgmnb = 16384
Let’s understand each parameter and its significance:
- kernel.msgmax – defines the upper limit for the size of individual messages and represents the maximum size (in bytes) that a message in a queue can have
- kernel.msgmni – indicates the maximum number of message queue identifiers we can create system-wide and determines the total number of message queues that can exist simultaneously
- kernel.msgmnb – represents the total amount of memory allocated to hold messages within a single queue by defining the maximum size (in bytes) of a message queue
In short, our output here indicates that the default maximum size of a message is 8192 bytes, the maximum number of message queue identifiers is 32000, and the maximum size of a message queue is 16384 bytes.
3. Implementing Message Queues in the Linux Kernel
Let’s now explore the implementation of these message queues. We’ll focus on creating a message queue, sending messages to a queue, and receiving messages from a queue while seeing detailed, practical examples of each.
3.1. Message Queue Creation
To create a message queue, we can use the ipcmk command. This Linux utility allows us to create and manage various inter-process communication resources, including message queues, by calling underlying C functions like ftok() and msgget().
Adding the -Q option to the ipcmk command indicates the creation of a message queue:
$ ipcmk -Q
Message queue identifier: 12345
If the process is successful, it creates the queue and outputs its identifier. As we can see, our output indicates that the message queue with the identifier 12345 has been created successfully.
After creating the message queue, verifying its information is essential to ensure proper functionality and troubleshooting, if required. To do this, we can read the /proc/sysvipc/msg file with cat or simply use the ipcs command. This command lets us query and manage inter-process communication (IPC) resources, including message queues.
By adding the -q flag to ipcs, the output displays information about all available message queues:
$ ipcs -q
------ Message Queues --------
key msqid owner perms used-bytes messages
0x123456 12345 user 644 1024 0
Our output provides some information for each message queue. This example indicates that our message queue with the key 0x123456, identifier 12345, and owner user has the permissions 644. Furthermore, it shows that the message queue currently uses 1024 bytes and contains 0 messages.
3.2. Sending Messages to a Queue
Now, let’s fill the message queue with some messages. To do this, we’ll write a C program to send messages to a message queue. We create a file with a .c extension and save it. For this example, we use msg_send.c. Then, we proceed to write our program:
#include <string.h>
#include <sys/msg.h>
int main() {
int msqid = 12345; // Message queue ID
struct message {
long type;
char text[20];
} msg;
msg.type = 1;
strcpy(msg.text, "This is message 1");
msgsnd(msqid, (void *)&msg, sizeof(msg.text), IPC_NOWAIT);
strcpy(msg.text, "This is message 2");
msgsnd(msqid, (void *)&msg, sizeof(msg.text), IPC_NOWAIT);
return 0;
}
In this C program, we include the necessary C header files for string operations and message queue functions before defining the main() function. In the function, we declare an integer variable msqid and assign it the ID of the message queue where we want to send the messages.
Next, we define a structure message to represent our message. It consists of two members – type, which represents the message type, and text, which holds the content of the message. We also set the message type to 1 and use strcpy() to assign the content of the first message.
Then, we use msgsnd() to send the message to the message queue identified by msqid. The msg structure is cast to (void *), and the message content size is passed using sizeof(msg.text). The IPC_NOWAIT flag indicates a non-blocking behavior.
After sending the first message, we update the content of msg.text with a new value and call msgsnd() again to send the second message to the message queue. Finally, we return 0 to indicate the successful execution of the program.
3.3. Testing the Message Sender
Now, we understand what our C program does. Let’s go through a step-by-step guide to execute it. Let’s save this C program as msg_send.c, if we haven’t. Then, we open a terminal in the directory where we save the file:
$ ls
Documents Downloads msg_send.c
To compile our C code, we must install the GNU C compiler (GCC), the commonly used C code compiler in Linux, on our system by first updating the package lists for upgrades:
$ sudo apt update
Hit:1 http://ke.archive.ubuntu.com/ubuntu jammy InRelease
...
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
Once we know all of our packages are up-to-date, then, we install GCC with apt:
$ sudo apt install gcc
Reading package lists... Done
Building dependency tree... Done
...
Done...
After installing the GCC compiler into our system, we use it to compile the code:
$ gcc -o msg_send msg_send.c
This command compiles the code and creates an executable named msg_send. Now, we execute the compiled program:
$ ./msg_send
Upon execution, our messages will be added to the queue, and the program will terminate without any visible output.
Lastly, we can use the ipcs -q command to check the message queue and verify the number of messages present:
$ ipcs -q
------ Message Queues --------
key msqid owner perms used-bytes messages
0x123456 12345 user 644 1024 2
As we can see, the messages column now indicates 2 messages in the message queue. This confirms that the messages we sent with the ./msg_send program have been successfully added to the message queue.
3.4. Receiving Messages From a Queue
We’ll also write a C program to receive messages from a queue. Let’s create a new C file and save it. For this example, we use msg_recv.c. Then, we can proceed to write our program:
#include <stdio.h>
#include <sys/msg.h>
int main() {
int msqid = 12345;
struct message {
long type;
char text[20];
} msg;
long msgtyp = 0;
msgrcv(msqid, (void *)&msg, sizeof(msg.text), msgtyp, MSG_NOERROR | IPC_NOWAIT);
printf("%s \n", msg.text);
return 0;
}
Like sending a message, the main() function declares the integer variable msqid and defines the structure message. We also declare a variable msgtyp to specify the desired message type to receive. In this case, we set it to 0 to receive any message type.
Then, we call the msgrcv() function to retrieve a message from the message queue identified by msqid. It takes the message queue identifier, a pointer to the msg structure, the size of the message content, the desired message type, and additional flags MSG_NOERROR | IPC_NOWAIT as arguments. The msgrcv() function reads a message from the queue into the msg structure.
Finally, we use printf() to print the received message and return 0 to indicate the successful execution of the program. We can customize the printf() output format according to our requirements.
3.5. Testing the Receiver
Now that we understand the program better, we save it, navigate to its directory, compile the program, and execute it:
$ gcc -o msg_recv msg_recv.c
$ ./msg_recv
This is message 1
$ ./msg_recv
This is message 2
As we can see, after executing the ./msg_recv command, the program attempts to receive one message at a time from the message queue and prints it to the console.
Our message queue 12345 is empty after successfully sending and receiving the two messages. If we like, we can delete the queue with the ipcrm command by specifying the message queue id with the -q option:
$ ipcrm -q 12345
This successfully deletes our message queue 12345. It’s important to note that deleting a message queue will permanently remove it, and any messages stored in the queue will be lost. Therefore, we should ensure we no longer require the messages before deleting the message queue.
Our examples provide simple instances of using message queues for inter-process communication within the Linux kernel. Many more advanced use cases exist, such as priority-based messaging, real-time systems, and publisher-subscriber models for efficient and reliable communication between processes.
4. Conclusion
In this article, we explored the implementation of message queues in the Linux kernel and learned how they facilitate efficient and reliable inter-process communication.
We looked at practical, executable examples demonstrating the creation, sending, and receiving of messages in a message queue. We can adapt these examples to different projects.
Finally, we saw that message queues are a vital component of Linux inter-process communication, offering flexibility, reliability, and performance benefits. By mastering their usage, we can design robust and efficient systems that use the power of asynchronous communication.