
Easy IPC with POSIX message queues
Hey there! Today, I’m waking up that old blog with another post about C development! What? You’re not happy? Of course you are. In this article, I’ll make a quick overview on on particular IPC technique: the POSIX message queue. First of all, for those who don’t know:
- On UNIX systems, each process is given a specific memory and address space, which cannot be accessed by other processes.
- Because of 1., processes can’t communicate using their respective memory space.
- Several threads of the same process share the same memory and address space, therefore they can communicate naturally.
- Techniques and tools to allow two or more processes to exchange information are referred to as IPC (Inter-Process Communication).
On most UNIX systems, you’ll find the following IPC tools:
- Signals
- Pipes
- Shared memory
- Memory-mapped files
- Sockets (UNIX and TCP/IP)
- Message queues
Now as I said earlier, in the article, we will focus on the latter: message queues.
What is a message queue?
Well… the name is rather self-explanatory: a message queue is a structure in which several processes can send and retrieve data. This is how it usually happens:
- Process A create the queue, and gives it a particular identifier: the key.
- Process B gains access to the queue using the key.
- Process A and B send and receive messages to and from the queue.
- Process A destroys the queue.
Resources necessary to use message queues can be included through the following header files:
1 2 3 |
#include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> |
What are these queue keys?
Each message queue registered by the kernel is associated to a key, a unique identifier thanks to which processes can access and alter it. This key is represented by a variable of type key_t (which looks just like an int, of course). In most scenarios, this key can either be:
- Set to IPC_PRIVATE, a constant which creates a private message queue. Queues created this way can only be accessed by the creating process, and its children.
- Generated automatically from a filename, using ftok.
To use ftok, your processes need to know what filename to use in order to generate the key. Let’s say your programs know they need to use /etc/debian_version (which would be disastrous for portability). The ftok call would be:
1 2 |
key_t queue_key; queue_key = ftok("/etc/debian_version", 'a'); |
The second parameter, referred to as “the ID” is another value use for key generation. Your programs will get the same key value if and only if they use the same file and ID value to generate a key. In fact, ftok uses both the file name and the least significant 8 bits of the ID value to generate a key.
Accessing a queue by its IPC key
Once both programs have generated the queue’s key, they can gain access to it using msgget. This function will return a queue ID, which will be used later to designate the key while the process uses the queue.
Note: if you’re using a private queue, there will be no key to use (since you’ll set it to IPC_PRIVATE). For this reason, once your main (father) process has gained access to the queue, the children process will need to use the ID returned by msgget to manipulate the queue.
1 2 3 |
int queue_id; int flags; queue_id = msgget(queue_key, flags); |
The flags argument is used to specify the way the queue has to be created/accessed, and what permissions should be applied to it. These flags can be made up from the following values:
- IPC_CREAT, which will have the queue created if it does not exist yet.
- IPC_EXCL, which will cause msgget to fail if a queue already exists with the given key.
- An octal representation of permission bits.
1 2 |
// Will create the queue if necessary. Read and write permissions for the owner. queue_id = msgget(queue_key, 0600 | IPC_CREAT); |
Note: unlike files, message queues don’t need to be opened and closed. Once the queue ID is known, the program can access it directly. This ID can be shared between a process and its children.
Sending and receiving messages
The first thing you need to do is to define a structure for the queue’s messages. The first member of your structure must be a long variable. For instance, all the following structures are correct:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
struct mymessage{ long mtype; char text[32]; }; struct mymessage{ long mtype; long myvalue; }; struct mymessage{ long mtype; struct another_struct mystruct; }; struct mymessage{ long mtype; struct mymessagedata{ int somevalue; char someothervalue[32]; } data; }; |
In a queue, all messages are ordered according to their mtype value. This value is usually described as the message priority: a message of type 1 is more important than a message of type 2. However, you may use this value as a message classifier. Some implementations actually give a specific number n to each process, and all messages of type n are considered to be destined to process n.
Once you have defined your structure, you can use msgsnd and msgrcv to send and receive messages through the queue:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
struct mymessage{ long mtype; char text[32]; }; struct mymessage msg; msg.mtype = 1; strcpy(msg.text, "Hello World!"); if(msgsnd(queue_id, &msg, sizeof(msg) - sizeof(long), 0) < 0){ perror("msgsnd"); exit(1); } |
A few things to note here:
- msgsnd uses the queue ID provided by msgget, not the key generated with ftok.
- The second argument is a pointer to the message structure defined earlier.
- The third argument is the size of this structure, not including the size of mtype. For this reason, it is important to subtract sizeof(long) to the value!
- The last argument is another set of flags, usually set to 0. You can set it to IPC_NOWAIT if you want msgsnd to return immediately (non-blocking operation).
Now, receiving happens pretty much the same way:
1 2 3 4 5 6 |
struct mymessage msg; if(msgrcv(queue_id, &msg, sizeof(msg) - sizeof(long), 0, 0) < 0) perror("msgrecv"); exit(2); } |
Now, this call is a little trickier:
- The first three arguments are the same as msgsnd.
- The second argument is the message type.
- The third argument are the reception flags, which can be IPC_NOWAIT (non-blocking operation), MSG_NOERROR (will truncate the message if it does not fit in the structure), and/or MSG_EXCEPT.
The type argument and the MSG_EXCEPT flag actually work together:
- If the message type is strictly superior to zero, then msgrcv will look for a message with a matching mtype. The MSG_EXCEPT flag will cause this behaviour to reverse, and have msgrcv find a message which does not have a matching mtype. If several messages match the criteria, one of them will be picked on a first-in-first-out basis.
- If the message type is zero, msgrcv will return the oldest message in the queue (first in), regardless of their types.
- If the message type is strictly inferior to zero, msgrcv will pick the message with the lowest priority inferior to the absolute value of the argument. For instance, passing -2 will retrieve a message of type 1 (prioritarily) or 2.
Destroying a queue
In order to destroy a queue, you must send it a IPC_RMID command. This can be done using the msgctl function:
1 2 3 4 |
if(msgctl(queue_id, IPC_RMID, NULL) < 0){ perror("msgctl"); exit(3); } |
IPC_RMID isn’t the only command you can send to a queue. The third argument is used as a parameter for two other commands, but in this case, it will be ignored (which is why I set it to NULL).
Manipulating queue metadata
Each queue has a specific set of parameters that define it. Just like the stat call for files, two msgctl commands allow you to retrieve and edit these parameters: IPC_STAT and IPC_SET. However, these two commands require you to pass a pointer to a msgid_ds structure as the third parameter of msgctl. IPC_STAT will write to this structure, while IPC_SET will read from it. Here is its definition:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
struct msqid_ds { struct ipc_perm msg_perm; struct msg *msg_first; /* first message on queue */ struct msg *msg_last; /* last message in queue */ time_t msg_stime; /* last msgsnd time */ time_t msg_rtime; /* last msgrcv time */ time_t msg_ctime; /* last change time */ struct wait_queue *wwait; /* pointer into the kernel's wait queue. */ struct wait_queue *rwait; /* pointer into the kernel's wait queue. */ ushort msg_cbytes; /* total number of bytes residing on the queue (sum of the sizes of all messages). */ ushort msg_qnum; /* number of messages currently in the queue. */ ushort msg_qbytes; /* max number of bytes on queue */ ushort msg_lspid; /* pid of last msgsnd */ ushort msg_lrpid; /* last receive pid */ }; |
The definition of the ipc_perm structure can be found here. Now, you can use msgctl to retrieve this structure, edit it, and set it again:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
struct msqid_ds queue_meta; // Get the queue metadata. if(msgctl(queue_id, IPC_STAT, &queue_meta) < 0){ perror("msgctl"); exit(4); } // Give the queue to user ID 1000. queue_meta.msg_perm.uid = 1000; // Set the information. if(msgctl(queue_id, IPC_SET, &queue_meta) < 0){ perror("msgctl"); exit(5); } |
And here you go, your queue now belongs to user 1000!
Using message queues
Message queues are a very powerful tool for inter-process communications, but it is important to note that it comes with no security whatsoever. For this reason, you must use separate components such as semaphores or mutexes to protect your queue from concurrency and corruption problems. With this in mind, you’re good to go!
I’ll try to post more articles concerning IPC (or just… more articles in general) in the future.
Bye!
Great article, very well explained!
If I’ve understood correctly, any process that knows a message queue’s key can send and receive any message from the queue. Is it possible to establish a private message queue between 2 processes (not for a parent-child fork, but for 2 distinct programs started independently)?
Hey, thanks for your comment 😉 The only way to have two non-related processes share a queue is to find a way to have them share the key. Since you’re writing the programs, there’s actually a rather easy (and common) way to do that : all programs must agree on a file and a character to use when calling
ftok
. In the article, I’m using/etc/debian_version
and a, and if all my programs do the same, thenftok
will provide them with the same key.Now of course, that file isn’t a very good choice, since it only exists on Debian distributions. Your programs should probably agree on something like
/etc/passwd
. Of course, they could also be programmed to dynamically find a file they are related to (a shared configuration file, a socket, a PID file, …).