Efficacité des IPC : les files de messages Posix

Publié par cpb
Sep 17 2011

Un client m’a demandé récemment de le conseiller sur le choix d’un mécanisme de communication entre processus pour transférer rapidement des données entre deux applications. Il existe plusieurs systèmes d’IPC (Inter Process Communication), chacun avec ses avantages et inconvénients, et j’ai eu envie de les comparer pour déterminer le plus rapide, en fonction du type de données à transférer. Ce premier article est consacré aux message queues, les files de messages.

Posix message queues

Dans l’API standard Unix, il existe deux implémentations pour les files de message : les message queue Système V et les message queues Posix. Les premières sont apparues dans les systèmes Unix des années 80. Elles étaient efficaces mais disposaient d’une interface de programmation un peu bancale : il fallait réserver dans le bloc de données à envoyer quatre octets pour stocker la priorité du message. Ceci n’est pas très dérangeant, mais pouvait nécessiter quand même une recopie systématique du message s’il était produit par une bibliothèque indépendante par exemple. En outre, comme l’ensemble des IPC Système V, ces message queues ne s’intégraient pas dans le concept général des descripteurs de fichiers Unix.

Dans les années 90, avec la volonté d’uniformiser les interfaces de programmation des systèmes Unix par l’intermédiaire de la norme Posix, sont apparues de nouvelles files de message, dont l’interface est plus simple, et dont les descripteurs sont plus proches de ceux des fichiers. Les message queues Posix n’ont été intégrées qu’assez tardivement dans Linux (dans la version 2.6.10 si ma mémoire est bonne).

Les appels-système qui nous concernent sont :

#include <mqueue.h>

mqd_t   mq_open    (const char * nom, int flags, mode_t mode, struct mq_attr * attr);
int     mq_send    (mqd_t mq, const char * msg, size_t lg, unsigned int prio);
ssize_t mq_receive (mqd_t mq, char * msg, size_t lg, unsigned int * prio);
int     mq_close   (mqd_t mq);
int     mq_unlink  (const char * nom);

On peut se reporter aux pages de manuels respectives de ces fonctions pour avoir plus de détails.

Transfert à faible débit

Voici un petit programme qui va nous permettre de mesurer le temps de transfert d’un message d’un processus à un autre, pour des messages relativement courts (8 octets), avec un faible débit.

Nous créeons deux processus, un émetteur qui envoie toutes les secondes dans la file un message contenant l’heure actuelle (secondes + microsecondes) et un récepteur qui compare la valeur reçue avec l’heure qu’il vient de lire lui-même. Notez que sur la plupart des machines actuelles, la lecture de l’heure à l’aide de gettimeofday() dure environ une micro-seconde. Le débit étant faible (un message par seconde), nous pouvons afficher directement la différence (en micro-secondes).

Pour que le fonctionnement soit optimal, il conviendra de placer l’émetteur et le récepteur sur deux cœurs ou deux processeurs différents.

Voici le premier émetteur qui envoie régulièrement l’heure dans la file de messages. Les fichiers-source ainsi que le fichier Makefile se trouvent dans cette archive.

emetteur-01.c:
#include <fcntl.h>
#include <mqueue.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/time.h>

int main(int argc,char * argv[])
{
    mqd_t mq;
    struct timeval heure;

    if (argc != 2) {
        fprintf(stderr, "usage: %s nom_file_message\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    mq = mq_open(argv[1], O_WRONLY | O_CREAT, 0600, NULL);
    if (mq == (mqd_t) -1) {
        perror(argv[1]);
        exit(EXIT_FAILURE);
    }
    while (1) {
        gettimeofday(& heure, NULL);
        mq_send(mq, (char *) & heure, sizeof(heure), 1);
        sleep(1);
    }
    return EXIT_SUCCESS;
}

Et voici le premier récepteur. On notera que dans l’API des message queues Posix, la fonction mq_receive() réclame un buffer capable de contenir (au moins) le plus grand message susceptible d’être véhiculée par la file. Cette valeur est obtenue avec mq_getattr().

recepteur-01.c:
#include <fcntl.h>
#include <mqueue.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/time.h>

int main(int argc,char * argv[])
{
    mqd_t mq;
    int taille;
    char * buffer;
    long int duree;
    struct mq_attr attr;
    struct timeval heure;
    struct timeval * recue;

    if (argc != 2) {
        fprintf(stderr, "usage: %s nom_file_message\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    mq = mq_open(argv[1], O_RDONLY | O_CREAT, 0600, NULL);
    if (mq == (mqd_t) -1) {
        perror(argv[1]);
        exit(EXIT_FAILURE);
    }

    if (mq_getattr(mq, & attr) != 0) {
        perror("mq_getattr");
        exit(EXIT_FAILURE);
    }
    taille = attr.mq_msgsize;
    buffer = malloc(taille);

    if (buffer == NULL) {
        perror("malloc");
        exit(EXIT_FAILURE);
    }

    recue = (struct timeval *) buffer;
    while (1) {
        mq_receive(mq, buffer, taille, NULL);
        gettimeofday(& heure, NULL);
        duree  = heure.tv_sec - recue->tv_sec;
        duree *= 1000000;
        duree += heure.tv_usec - recue->tv_usec;
        fprintf(stdout, "%ld usec\n", duree);
    }
    return EXIT_SUCCESS;
}

Pour l’exécution, je lance sur un premier terminal :

$ taskset -c 0 ./recepteur-01 /essai

Notez que le nom de la file de message (essai) doit obligatoirement être précédé d’un slash ‘/‘.

Puis sur un second terminal :

$ taskset -c 1 ./emetteur-01 /essai

Les résultats s’affichent dans le premier terminal :

$ taskset -c 0 ./recepteur-01 /essai
16 usec
14 usec
14 usec
15 usec
13 usec
12 usec
13 usec
15 usec
14 usec
(Contrôle-C)
$

Sur ce système – Intel Core 2 Quad avec Linux 2.6.38 générique Ubuntu – le temps de passage d’un message sporadique est donc d’une quinzaine de micro-secondes. Essayons à présent avec un débit plus élevé.

Transfert d’un message à haut débit

Nous allons à présent envoyer des messages aussi vite que possible. Pour cela, nous retirons simplement le sommeil d’une seconde de l’exemple précédent :

emetteur-02.c:
#include <fcntl.h>
#include <mqueue.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/time.h>

int main(int argc,char * argv[])
{
    mqd_t mq;
    struct timeval heure;

    if (argc != 2) {
        fprintf(stderr, "usage: %s nom_file_message\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    mq = mq_open(argv[1], O_WRONLY | O_CREAT, 0600, NULL);
    if (mq == (mqd_t) -1) {
        perror(argv[1]);
        exit(EXIT_FAILURE);
    }
    while (1) {
        gettimeofday(& heure, NULL);
        mq_send(mq, (char *) & heure, sizeof(heure), 1);
    }
    return EXIT_SUCCESS;
}

Pour la réception, le problème est plus compliqué. Nous ne pouvons pas nous permettre de faire un fprintf() à chaque message, car cela perturberait l’exécution du programme. J’ai donc décidé d’afficher régulièrement les durées minimale, maximale et moyenne sur une période donnée, de l’ordre d’une seconde (calculée avec les résultats précédents).

recepteur-02.c:
#include <fcntl.h>
#include <mqueue.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/time.h>

int main(int argc,char * argv[])
{
    mqd_t mq;
    int taille;
    char * buffer;
    struct mq_attr attr;
    struct timeval heure;
    struct timeval * recue;

    int nb_messages;
    long int duree;
    long int duree_max;
    long int duree_min;
    long int somme_durees;

    if (argc != 2) {
        fprintf(stderr, "usage: %s nom_file_message\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    mq = mq_open(argv[1], O_RDONLY | O_CREAT, 0600, NULL);
    if (mq == (mqd_t) -1) {
        perror(argv[1]);
        exit(EXIT_FAILURE);
    }

    if (mq_getattr(mq, & attr) != 0) {
        perror("mq_getattr");
        exit(EXIT_FAILURE);
    }

    taille = attr.mq_msgsize;
    buffer = malloc(taille);
    if (buffer == NULL) {
        perror("malloc");
        exit(EXIT_FAILURE);
    }

    recue = (struct timeval *) buffer;
    while (1) {
        nb_messages = 0;
        duree_max = 0;
        duree_min = -1;
        somme_durees = 0;
        do {
            mq_receive(mq, buffer, taille, NULL);
            gettimeofday(& heure, NULL);
            duree  = heure.tv_sec - recue->tv_sec;
            duree *= 1000000;
            duree += heure.tv_usec - recue->tv_usec;
            if (nb_messages > 0) { // Ignorer le premier message (retarde)
                if (duree_max < duree)
                    duree_max = duree;
                if ((duree_min == -1) || (duree_min > duree))
                    duree_min = duree;
                somme_durees += duree;
            }
            nb_messages ++;
        } while (nb_messages < 100000); // arbitraire, de l'ordre de la seconde
        fprintf(stdout, "min =%3ld   max =%3ld moy=%5.1f\n",
            duree_min, duree_max, ((float) somme_durees) / (nb_messages - 1));
    }
    return EXIT_SUCCESS;
}

Comme précédemment, je lance les deux processus sur deux cœurs distincts. Voici les résultats :

$ taskset -c 0 ./recepteur-02 /essai
min =  1   max = 78 moy=  7.8
min =  1   max =102 moy=  6.9
min =  1   max = 65 moy=  7.5
min =  1   max = 61 moy=  7.4
min =  1   max = 70 moy=  5.5
min =  1   max = 64 moy=  6.6
min =  1   max = 63 moy=  5.7
min =  1   max = 75 moy=  5.5
min =  1   max = 84 moy=  5.6
min =  1   max = 71 moy=  6.8
min =  1   max = 68 moy=  6.3
min =  1   max =688 moy=  6.8
min =  1   max = 69 moy=  5.7
min =  1   max = 69 moy=  7.7
min =  1   max = 68 moy=  6.7
min =  2   max = 69 moy=  7.8
min =  2   max =102 moy=  6.7
min =  1   max = 70 moy=  5.6
min =  1   max = 73 moy=  6.3
min =  1   max = 63 moy=  6.2
min =  1   max = 71 moy=  4.8
min =  1   max = 87 moy=  5.5
min =  3   max = 74 moy=  6.4
min =  1   max = 71 moy=  4.1
min =  1   max = 71 moy=  8.1
min =  1   max = 80 moy=  5.8
min =  1   max = 76 moy=  7.2
min =  1   max =177 moy=  7.9
min =  1   max = 55 moy=  5.4
min =  2   max = 74 moy=  8.5
min =  4   max = 74 moy=  8.2
min =  3   max = 77 moy=  6.4
^C
$

En fait, le temps moyen lorsqu’on envoie des messages fréquents est plutôt de l’ordre de 8 micro-secondes alors qu’avec des messages rares, il était de 15 micro-secondes environ. Il y a naturellement de temps à autres des temps de transferts plus long à cause de l’activité du système. Si l’on ordonnance en temps-réel les deux tâches, les durées maximales sont plus stables. Il faut toutefois obtenir les droits root.

# chrt -f 40 taskset -c 1 ./emetteur-02 /essai

et sur l’autre terminal

#  chrt -f 40 taskset -c 0 ./recepteur-02 /essai
min =  1   max = 80 moy=  6.1
min =  1   max = 67 moy=  6.3
min =  1   max = 64 moy=  5.3
min =  1   max = 70 moy=  8.2
min =  3   max = 67 moy=  8.8
min =  2   max = 62 moy=  5.7
min =  2   max = 62 moy=  7.8
min =  1   max = 59 moy=  7.5
min =  1   max = 63 moy=  6.6
min =  2   max = 63 moy=  6.4
min =  2   max = 66 moy=  7.2
min =  1   max = 58 moy=  5.9
min =  1   max = 55 moy=  6.4
min =  2   max = 65 moy=  8.5
min =  2   max = 71 moy=  9.0
min =  2   max = 81 moy=  8.5
min =  1   max = 63 moy=  7.5
min =  1   max = 66 moy=  8.1
min =  3   max = 69 moy=  8.7
min =  2   max = 64 moy=  7.2
min =  1   max = 62 moy=  5.7
min =  1   max = 63 moy=  7.2
min =  1   max = 65 moy=  8.2
min =  2   max = 64 moy=  7.2
min =  1   max = 68 moy=  6.0
min =  1   max = 69 moy=  6.3
min =  1   max = 60 moy=  6.5
min =  1   max = 67 moy=  7.3
min =  1   max = 62 moy=  7.1
min =  2   max = 62 moy=  8.6
min =  1   max = 57 moy=  6.7
min =  2   max = 72 moy=  8.7
min =  1   max = 85 moy=  7.9
min =  1   max = 72 moy=  7.1
min =  1   max = 58 moy=  5.0
min =  1   max = 64 moy=  5.8
min =  2   max = 73 moy=  7.4
min =  1   max = 55 moy=  7.1
min =  1   max = 62 moy=  7.3
min =  1   max = 66 moy=  8.3
min =  1   max = 58 moy=  5.2
min =  5   max = 75 moy=  9.3
min =  3   max = 68 moy=  9.1

Nos messages sont très courts, deux entiers de 32 bits, soient 8 octets en tout. J’ai fait quelques expériences en augmentant leur taille (jusqu’à 64 ko), sans constater de différence sensible de temps de passage d’un message.

Conclusion

Nous pouvons donc estimer que sur ce système le temps de transfert d’un message d’un processus à l’autre par les files Posix est d’environ 8 micro-secondes en moyenne, avec des pics de quelques dizaines de micro-secondes. Nous comparerons ces valeurs avec d’autres mécanismes IPC dans les prochains articles.

2 Réponses

  1. Amine dit :

    Super !

  2. Merci pour ces résultats et l’exemple d’utilisation des files d’attente

URL de trackback pour cette page