Monday, January 9, 2012

POSIX Message Queues and Arrival Notifications

Recently I was working on my dissertation project and came up with the need for message passing among local processes on Linux. I am quite familiar with the System V IPC message passing library. However, It does not offer the capability of sending signals when messages arrive, which is crucial for my work.

While reading the sigaction(2) man page, I found that there's an API that allows notification when a message arrives. It's part of the POSIX Message Queues library, which is fully implemented on Linux. From now on, I'll be referring to this library as MQ.

Before I actually tried to use this library in my project, I came up with a little test program which is notified through a signal whenever a message arrives. The test is comprised of two programs: a server program, which listens for a message and a client program, which is used to send a message to the server program.

The Server Program (server.c)

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <mqueue.h>

mqd_t msgqdes;

int main(int argc, char **argv)
    struct sigevent se;
    struct sigaction sa;
    
    printf("(SERVER) My PID: %d\n\n", getpid());
    
    msgqdes = mq_open("/mqueuetest", O_RDWR | O_CREAT, 0777, NULL);
       
    se.sigev_notify = SIGEV_SIGNAL;
    se.sigev_signo = SIGUSR1;
    
    mq_notify(msgqdes, &se);
    
    sa.sa_flags = SA_SIGINFO;
    sa.sa_sigaction = message_callback;
    
    sigemptyset(&(sa.sa_mask));
    if(sigaction(SIGUSR1, &sa, NULL) == -1){
        perror("sigaction");
        exit(1);
    }
    
    pause();
    
    mq_close(msgqdes);
    
    return 0;
}

In order to use MQ, it's necessary to include mqueue.h, as in line 6. MQ works with the idea of creating named mailboxes, which can be accessed via a string key. String keys should always start with a "/" and should obey the C identifier lexical convention. Mailboxes can be created/opened with the mq_open function, as shown in line 18.

The first parameter for mq_open is the string key used to identify the mailbox being opened. The second parameter indicates how to open the mailbox. In the example, we're opening the mailbox for reading and writing, while also creating it, in case it does not exist. The fourth parameter indicates the file mode. Since the mailbox is represented in the filesystem, its creation must have a valid file mode. For simplicity, in the example I just used 0777 as the file mode (you can learn about file modes in the open(2) man page). The last parameter lets the user configure the properties of the mailbox being created. Let's just leave it at NULL for default values. mq_open returns a descriptor for the requested mailbox. Here, we're using the global msgqdes for this purpose.

Now comes the tricky part: we want this process to be notified through a signal whenever a new message arrives. Therefore, we must tell it to do so. The function mq_notify (line 23) is used exactly for that purpose. Its first parameter indicates what mailbox we're registering for delivery notification, which in this case, is the one we just opened in line 18. The second parameter is a pointer to a sigevent structure, which details how to notify the process that it received a new message.  Line 20 indicates that the system must notify the message arrival through signals. Line 21 indicates which signal should be delivered. As there's no specific signal for message arrival, I just selected the SIGUSR1 for this purpose.

Now, we should be able to setup the signal handler that is called whenever a new message arrives. Lines 25 through 31 are responsible for setting up the signal handler, and are relatively straight-forward. In specific, line 26 indicates which function should be called when a new message arrives (message_callback), which is  discussed in a minute.

After setting up the signal handler, the program just goes to sleep in the pause call, waiting to be awoken by the SIGUSR1 signal. The function mq_close on line 36 is used to close the mailbox file descriptor.

The following function is the message callback, which is called whenever a message arrives. It should be placed with the preceding code snippet in server.c.

void* message_callback(int signum, siginfo_t* info, void* context){
    char* buffer;
    
    if(signum == SIGUSR1){
        if(info->si_code == SI_MESGQ){
            printf("Message received from PID %d:\n", info->si_pid);
            buffer = malloc(32768);
            mq_receive(msgqdes, buffer, 32768, NULL);
            printf("%s~EOF~\n", buffer);
            free(buffer);        
        }
    } 
}

This function is pretty simple. It just checks if the signal received is the one we're waiting for, in this case, SIGUSR1. If so, we must verify if the signal code, that lives within the info parameter and gets set by the MQ library, is SI_MESGQ. This tells us that the signal was, in fact, generated by the MQ library, as SIGUSR1 signals may be generated freely. It's worth noting that the mq_receive function is used for receiving, i.e. retrieving, the message from the mailbox. The first parameter indicates the mailbox descriptor, the second parameter is a pointer where the message data is to be copied, the third parameter denotes the maximum size of the message. Let's leave the fourth parameter for a later talk.

The Client Program (client.c)

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <mqueue.h>

int main(int argc, char **argv){
    
    mqd_t msgqdes;
    struct mq_attr attr;
    char message[32768];
    
    printf("(CLIENT) My PID: %d\n\n", getpid());
    
    msgqdes = mq_open("/mqueuetest", O_RDWR | O_CREAT, 0777, NULL);
    
    printf("Message? ");
    fgets(message, 32768, stdin);
    
    mq_send(msgqdes, message, strlen(message), 0);
    
    return 0;
}

Lines 1 through 16 are similar to those initial steps in the server program. They open the same mailbox, since they use the same string key, in this case, "/mqueuetest". The following lines are used to read the message which is to be sent to the server program.

At last, the mq_send function is responsible for sending the message to a mailbox. The first parameter is the message descriptor, the second parameter is a pointer to the message itself, the third parameter should indicate the size of the message, which in this case is calculated by strlen (since it's a string, anyway), and the last parameter is the priority (0-31, being 0 the lowest).

Testing the MQ Library

In order to  compile the programs, one must link them with -lrt, the real-time library, which has all the object-code for the MQ library.

To compile:
 $gcc -o server server.c -lrt 
 $gcc -o client client.c -lrt 
Now, just run the server program in the background:
 $./server &
And then run the client program:
 $./client
 Message? Hello Message World!
As soon as the user presses Enter, the server program is delivered a signal, and "Hello Message World" is printed on the screen:
 Message received from PID XXXX:
 Hello Message World!
 ~EOF~
Great!

I hope this is useful for somebody, since it took me some time to go over the manual pages for this stuff. If you have any questions, please ask!

And sorry for the somewhat clumsy code. This is just something I put together in order to test the library functionality.

1 comment:

  1. Great article my friend.

    :)

    Now, your blog is in my favorites :)

    Sucess! I hope you do not discontinue this project, if you
    need some ideas, let me know!

    Regards!
    Gui

    ReplyDelete