1. Introduction
Les POSIX Message Queues permettent à plusieurs processus d'échanger des données sous forme de messages complets.
Contrairement à un pipe, une file de messages ne représente pas simplement un flux continu d'octets.
Chaque appel à mq_send() ajoute un message distinct.
Chaque appel à mq_receive() récupère un message complet.
Les POSIX Message Queues sont similaires aux files de messages System V, mais elles possèdent plusieurs caractéristiques particulières :
- utilisation d'un nom POSIX ;
- utilisation d'un descripteur de type mqd_t ;
- gestion de priorités ;
- possibilité de fonctionnement bloquant ou non bloquant ;
- possibilité d'utiliser des délais d'attente ;
- possibilité de demander une notification asynchrone lorsqu'un message arrive.
2. Vue d'ensemble de l'API
Fonctions principales
Code: Select all
mq_open()
Code: Select all
mq_close()
Code: Select all
mq_unlink()
Code: Select all
mq_send()
Code: Select all
mq_receive()
Code: Select all
mq_getattr()
Code: Select all
mq_setattr()
Code: Select all
mq_notify()
Code: Select all
mq_timedsend()
Code: Select all
mq_timedreceive()
3. Création et ouverture d'une file
mq_open()
Prototype simplifié :
Code: Select all
mqd_t mq_open(
const char *name,
...
);
Lorsque O_CREAT est utilisé, deux arguments supplémentaires peuvent être fournis :
mode_t mode
et :
Code: Select all
struct mq_attr *attr
Code: Select all
mqd_t mq = mq_open(
Code: Select all
O_CREAT | O_RDWR,
NULL
);
Valeur de retour
Code: Select all
mq_open() retourne un descripteur de type :
mqd_t
(mqd_t)-1
4. Nom d'une file POSIX
Les files de messages POSIX utilisent des noms.
Exemple :
"/inside_queue"
Sur Linux, un nom POSIX doit commencer par :
Code: Select all
/
Exemple :
Processus A :
Code: Select all
mq_open("/inside_queue", ...);
Code: Select all
mq_open("/inside_queue", ...);
5. Flags de mq_open()
O_RDONLY
Ouvre la file uniquement en lecture.
Le processus pourra utiliser :
Code: Select all
mq_receive()
Ouvre la file uniquement en écriture.
Le processus pourra utiliser :
Code: Select all
mq_send()
Ouvre la file en lecture et en écriture.
O_CREAT
Crée la file si elle n'existe pas.
O_EXCL
Utilisé avec O_CREAT.
Si la file existe déjà, mq_open() échoue.
Exemple :
Code: Select all
O_CREAT | O_EXCL | O_RDWR
Active le fonctionnement non bloquant.
Une opération qui devrait normalement attendre échoue immédiatement.
6. Permissions
Lors de la création d'une file, le paramètre mode permet de choisir les permissions.
Exemple :
Code: Select all
0666
Elles sont également affectées par le umask du processus.
7. Fermeture d'une file
mq_close()
Prototype :
Code: Select all
int mq_close(mqd_t mqdes);
mq_close() ferme le descripteur détenu par le processus.
Code: Select all
mq_close(mq);
La file peut toujours exister et être ouverte par d'autres processus.
8. Suppression d'une file
mq_unlink()
Prototype :
Code: Select all
int mq_unlink(const char *name);
Code: Select all
mq_unlink("/inside_queue");
mq_unlink() supprime le nom de la file.
L'objet est réellement détruit lorsque :
- son nom a été supprimé ;
- aucun processus ne possède encore de référence ouverte.
9. Descripteurs de files POSIX
Le type utilisé pour représenter une file ouverte est :
Code: Select all
mqd_t
Sur Linux, ce descripteur est étroitement intégré au modèle des descripteurs de fichiers.
Un fork() fait hériter au processus fils des descripteurs ouverts.
Le parent et le fils peuvent donc référencer la même description de file ouverte après fork().
10. Relation entre processus, descripteurs et file
Plusieurs processus peuvent ouvrir le même objet POSIX.
Par exemple :
Processus A
|
+--> mqd_t
|
+--> File POSIX
Processus B
|
+--> mqd_t
|
+--> Même file POSIX
Chaque processus possède son propre descripteur.
Cependant, les deux descripteurs peuvent désigner le même objet de file maintenu par le noyau.
11. Attributs d'une file de messages
Les propriétés d'une file sont représentées par :
Code: Select all
struct mq_attr
Code: Select all
struct mq_attr {
long mq_maxmsg;
long mq_msgsize;
long mq_curmsgs;
Code: Select all
};
Contient les flags associés au descripteur.
Sur Linux, l'information importante est généralement :
Code: Select all
O_NONBLOCK
Nombre maximal de messages pouvant être présents simultanément dans la file.
mq_msgsize
Taille maximale, en octets, d'un seul message.
mq_curmsgs
Nombre actuel de messages présents dans la file.
12. Définir les attributs lors de la création
Lorsqu'une file est créée avec mq_open(), un pointeur vers struct mq_attr peut être fourni.
Exemple :
Code: Select all
struct mq_attr attr;
attr.mq_flags = 0;
attr.mq_maxmsg = 10;
attr.mq_msgsize = 2048;
attr.mq_curmsgs = 0;
mqd_t mq = mq_open(
Code: Select all
O_CREAT | O_RDWR,
&attr
);
Lors de la création, les deux champs particulièrement importants sont :
mq_maxmsg
et :
mq_msgsize
mq_curmsgs est déterminé par le noyau.
mq_flags concerne plutôt le comportement du descripteur.
13. Récupérer les attributs
mq_getattr()
Prototype :
Code: Select all
int mq_getattr(
mqd_t mqdes,
struct mq_attr *attr
Exemple :
Code: Select all
struct mq_attr attr;
if (mq_getattr(mq, &attr) == -1) {
perror("mq_getattr");
}
printf("Maximum messages : %ld\n", attr.mq_maxmsg);
printf("Taille maximale : %ld\n", attr.mq_msgsize);
printf("Messages présents : %ld\n", attr.mq_curmsgs);
14. Modifier les attributs
mq_setattr()
Prototype :
Code: Select all
int mq_setattr(
mqd_t mqdes,
Code: Select all
struct mq_attr *oldattr
Code: Select all
mq_setattr() ne permet pas de changer librement tous les champs.
Code: Select all
O_NONBLOCK
Code: Select all
struct mq_attr attr;
mq_getattr(mq, &attr);
attr.mq_flags |= O_NONBLOCK;
mq_setattr(mq, &attr, NULL);
mq_send()
Prototype :
Code: Select all
int mq_send(
mqd_t mqdes,
const char *msg_ptr,
Code: Select all
unsigned int msg_prio
Exemple :
Code: Select all
const char *msg = "Inside The System";
mq_send(
msg,
strlen(msg) + 1,
5
);
Paramètres
mqdes
Descripteur de la file.
msg_ptr
Adresse du message à envoyer.
msg_len
Longueur du message.
msg_prio
Priorité du message.
16. Taille maximale d'un message
Le paramètre msg_len ne doit pas dépasser :
mq_msgsize
Si le message est trop grand, mq_send() échoue.
La taille maximale peut être récupérée avec :
Code: Select all
mq_getattr()
Chaque message POSIX possède une priorité entière non négative.
Exemple :
Code: Select all
mq_send(mq, "LOW", 4, 1);
mq_send(mq, "HIGH", 5, 10);
À priorité identique, les messages conservent leur ordre d'arrivée.
On obtient donc globalement :
priorité la plus élevée d'abord
puis :
FIFO pour les messages ayant la même priorité.
18. Différence avec System V pour la sélection des messages
Avec System V, on utilise souvent un champ :
long mtype;
Le récepteur peut sélectionner un message selon ce type.
Avec POSIX, les messages utilisent directement un niveau de priorité.
Il n'existe donc pas exactement le même mécanisme que mtype.
La sélection se fait selon la priorité puis l'ordre d'arrivée.
19. Comportement lorsque la file est pleine
Si la file contient déjà :
mq_maxmsg
messages, elle est pleine.
Avec un descripteur bloquant :
Code: Select all
mq_send()
Avec :
Code: Select all
O_NONBLOCK
mq_send() échoue immédiatement.
Code: Select all
EAGAIN
mq_receive()
Prototype :
Code: Select all
ssize_t mq_receive(
mqd_t mqdes,
char *msg_ptr,
Code: Select all
unsigned int *msg_prio
Exemple :
Code: Select all
struct mq_attr attr;
mq_getattr(mq, &attr);
char *buffer = malloc(attr.mq_msgsize);
unsigned int priority;
ssize_t size = mq_receive(
buffer,
Code: Select all
attr.mq_msgsize,
);
Valeur de retour
En cas de succès :
nombre d'octets reçus.
En cas d'erreur :
Code: Select all
-1
Le buffer fourni à mq_receive() doit être assez grand pour contenir la taille maximale autorisée pour un message dans la file.
En pratique, on peut faire :
Code: Select all
struct mq_attr attr;
mq_getattr(mq, &attr);
char *buffer = malloc(attr.mq_msgsize);
Code: Select all
mq_receive(
buffer,
Code: Select all
attr.mq_msgsize,
);
Si le buffer est trop petit, l'appel échoue.
22. Priorité du message reçu
Le dernier argument de mq_receive() permet de récupérer la priorité.
Exemple :
Code: Select all
unsigned int priority;
mq_receive(
buffer,
size,
&priority
);
Après l'appel :
priority
contient la priorité du message reçu.
Si cette information n'est pas nécessaire, il est possible d'utiliser :
NULL
23. File vide
Si la file ne contient aucun message :
Code: Select all
mq_receive()
Avec :
Code: Select all
O_NONBLOCK
Code: Select all
EAGAIN
Par défaut, une file est généralement utilisée en mode bloquant.
Exemple :
Code: Select all
mq_receive(...);
De même :
Code: Select all
mq_send(...);
Ce comportement est pratique lorsqu'on souhaite naturellement synchroniser producteur et consommateur.
25. Mode non bloquant
Le flag :
Code: Select all
O_NONBLOCK
Exemple :
Code: Select all
mq_open(
Code: Select all
O_RDONLY | O_NONBLOCK
Si aucun message n'est disponible :
Code: Select all
mq_receive()
Code: Select all
errno == EAGAIN
26. Envoi avec timeout
mq_timedsend()
Prototype conceptuel :
Code: Select all
int mq_timedsend(
mqd_t mqdes,
const char *msg_ptr,
Code: Select all
unsigned int msg_prio,
);
Cette fonction fonctionne comme mq_send(), mais permet de limiter le temps d'attente.
Si la file est pleine et qu'aucune place ne devient disponible avant l'échéance :
Code: Select all
mq_timedsend()
27. Réception avec timeout
mq_timedreceive()
Prototype conceptuel :
Code: Select all
ssize_t mq_timedreceive(
mqd_t mqdes,
char *msg_ptr,
Code: Select all
unsigned int *msg_prio,
);
Cette fonction fonctionne comme mq_receive(), mais l'attente possède une limite temporelle.
Si aucun message n'arrive avant l'échéance :
Code: Select all
mq_timedreceive()
Code: Select all
ETIMEDOUT
Les fonctions temporisées utilisent un :
Code: Select all
struct timespec
On peut récupérer l'heure actuelle puis ajouter un délai.
Exemple conceptuel :
Code: Select all
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
ts.tv_sec += 5;
mq_timedreceive(
buffer,
size,
&priority,
&ts
);
Le processus attend au maximum jusqu'à cette échéance.
29. Notification de message
Une fonctionnalité importante des POSIX Message Queues est :
Code: Select all
mq_notify()
Prototype
Code: Select all
int mq_notify(
mqd_t mqdes,
);
30. Principe de mq_notify()
L'idée est la suivante :
1. Le processus ouvre une file.
2. Il appelle mq_notify().
3. Il demande au noyau de le prévenir lorsqu'un message arrive.
4. Le processus peut effectuer autre chose.
5. Lorsqu'un message devient disponible, le mécanisme demandé est déclenché.
Cela évite de rester bloqué en permanence dans mq_receive().
31. Une seule inscription à la fois
Une seule inscription de notification peut être active pour une file donnée.
Si un autre processus tente d'enregistrer une notification alors qu'une inscription existe déjà, l'appel échoue.
Cela impose de coordonner les consommateurs lorsque plusieurs processus utilisent la même file.
32. Notification ponctuelle
Une caractéristique importante de mq_notify() est que l'inscription est généralement ponctuelle.
Après qu'une notification a été déclenchée, le processus doit s'enregistrer à nouveau s'il souhaite continuer à recevoir des notifications.
Le schéma classique est donc :
Code: Select all
mq_notify()
notification
↓
lecture des messages
↓
Code: Select all
mq_notify() de nouveau
Le comportement de mq_notify() est choisi avec :
Code: Select all
struct sigevent
- aucune notification ;
- l'envoi d'un signal ;
- l'exécution d'une fonction via un thread.
34. SIGEV_NONE
Avec :
Code: Select all
SIGEV_NONE
Le processus peut néanmoins utiliser cette valeur dans certains contextes pour gérer l'enregistrement.
35. SIGEV_SIGNAL
Avec :
Code: Select all
SIGEV_SIGNAL
Exemple conceptuel :
Code: Select all
struct sigevent sev;
sev.sigev_notify = SIGEV_SIGNAL;
sev.sigev_signo = SIGUSR1;
mq_notify(mq, &sev);
SIGUSR1
36. sigev_value
Code: Select all
struct sigevent peut également transporter une valeur définie par le programme.
sigev_value
Elle peut ensuite être récupérée par certains mécanismes de notification.
Cela permet de transmettre du contexte au gestionnaire de notification.
37. SIGEV_THREAD
Avec :
Code: Select all
SIGEV_THREAD
Exemple conceptuel :
Code: Select all
struct sigevent sev;
sev.sigev_notify = SIGEV_THREAD;
sev.sigev_notify_function = handler;
sev.sigev_notify_attributes = NULL;
mq_notify(mq, &sev);
handler()
sera exécutée lors de la notification.
38. Course critique avec mq_notify()
L'utilisation de mq_notify() nécessite une certaine prudence.
Un schéma important consiste souvent à réenregistrer la notification avant de vider complètement la file.
Sinon, un message peut arriver entre :
- la dernière lecture ;
- le nouvel appel à mq_notify().
L'ordre des opérations est donc important dans un programme concurrent.
39. Lecture de tous les messages après notification
Une stratégie fréquente est :
1. recevoir une notification ;
2. réenregistrer mq_notify() ;
3. lire tous les messages disponibles ;
4. arrêter lorsque mq_receive() retourne EAGAIN.
Pour cette stratégie, la file est généralement ouverte avec :
Code: Select all
O_NONBLOCK
40. Exemple simplifié d'envoi
Code: Select all
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <mqueue.h>
int main(void)
{
mqd_t mq;
"/inside_queue",
Code: Select all
O_CREAT | O_WRONLY,
NULL
);
Code: Select all
if (mq == (mqd_t)-1) {
perror("mq_open");
return EXIT_FAILURE;
}
const char *message = "Inside The System";
if (mq_send(
message,
strlen(message) + 1,
10
) == -1) {
Code: Select all
perror("mq_send");
}
mq_close(mq);
return EXIT_SUCCESS;
}
Code: Select all
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <mqueue.h>
int main(void)
{
mqd_t mq;
struct mq_attr attr;
"/inside_queue",
Code: Select all
O_RDONLY
Code: Select all
if (mq == (mqd_t)-1) {
perror("mq_open");
return EXIT_FAILURE;
}
if (mq_getattr(mq, &attr) == -1) {
perror("mq_getattr");
mq_close(mq);
return EXIT_FAILURE;
}
char *buffer = malloc(attr.mq_msgsize);
if (buffer == NULL) {
perror("malloc");
mq_close(mq);
return EXIT_FAILURE;
}
unsigned int priority;
ssize_t n = mq_receive(
buffer,
Code: Select all
attr.mq_msgsize,
);
Code: Select all
if (n == -1) {
perror("mq_receive");
} else {
printf("Message : %s\n", buffer);
printf("Priorité : %u\n", priority);
}
free(buffer);
mq_close(mq);
return EXIT_SUCCESS;
}
Après utilisation :
Code: Select all
mq_close(mq);
Code: Select all
mq_unlink("/inside_queue");
Code: Select all
mq_close()
Code: Select all
mq_unlink()
mq_close() ferme une référence.
mq_unlink() retire le nom de l'objet.
Sous Linux, les files POSIX peuvent être représentées dans un système de fichiers virtuel.
Le point de montage généralement utilisé est :
Code: Select all
/dev/mqueue
Montage
Exemple :
Code: Select all
mount -t mqueue none /dev/mqueue
On peut ensuite utiliser des commandes classiques comme :
Code: Select all
ls /dev/mqueue
cat /dev/mqueue/nom_de_file
44. Informations visibles dans /dev/mqueue
Sous Linux, les informations exposées peuvent inclure notamment :
- quantité de données actuellement présente ;
- informations concernant une notification ;
- PID du processus enregistré pour la notification ;
- type de notification.
45. Limites système
Les files de messages POSIX possèdent plusieurs limites.
Exemples :
- nombre maximal de files ;
- nombre maximal de messages par file ;
- taille maximale d'un message ;
- priorité maximale.
MQ_PRIO_MAX
Cette limite correspond au nombre de niveaux de priorité disponibles.
POSIX impose un minimum portable.
Linux peut proposer une plage différente ou plus grande selon l'implémentation.
OPEN_MAX
Cette valeur concerne le nombre maximal de descripteurs ouverts qu'un processus peut posséder.
Les descripteurs de files POSIX peuvent compter dans ces limites selon l'implémentation.
46. Limites Linux
Linux expose plusieurs paramètres permettant de contrôler les ressources utilisées par les POSIX Message Queues.
Ces limites peuvent concerner :
- la taille maximale d'un message ;
- le nombre maximal de messages par file ;
- le nombre maximal de files ;
- la quantité totale de mémoire utilisée.
47. Comparaison avec System V
Les deux mécanismes permettent d'échanger des messages entre processus.
Mais leur conception diffère.
System V
Fonctions principales :
msgget()
msgsnd()
msgrcv()
msgctl()
Identifiant :
int
Sélection des messages :
mtype
Suppression :
msgctl(..., IPC_RMID, ...)
POSIX
Fonctions principales :
Code: Select all
mq_open()
mq_send()
mq_receive()
mq_close()
mq_unlink()
Code: Select all
mqd_t
priorité
Suppression du nom :
Code: Select all
mq_unlink()
Les files POSIX possèdent plusieurs avantages.
- API relativement cohérente avec le reste de POSIX.
- Noms lisibles.
- Priorités intégrées.
- Notification asynchrone.
- Mode non bloquant.
- Fonctions temporisées.
- Bonne intégration avec Linux.
- Possibilité d'inspection via /dev/mqueue.
49. Inconvénients
Quelques limites existent également.
- Elles ne sont pas disponibles ou identiques sur tous les systèmes Unix.
- Leur portabilité pratique peut être inférieure à celle d'autres mécanismes POSIX très répandus.
- Les limites maximales peuvent varier selon le système.
- Leur utilisation avec notifications demande une bonne gestion des courses critiques.
- Pour de très gros volumes de données, la mémoire partagée peut être plus adaptée.
50. Choix entre pipe, message queue et mémoire partagée
Pipe
Adapté à un flux simple entre processus.
Exemple :
producteur -> consommateur
Message queue
Adaptée lorsque les données doivent rester séparées en messages.
Particulièrement utile si :
- plusieurs messages doivent être mis en attente ;
- des priorités sont nécessaires ;
- une notification est utile.
Mémoire partagée
Adaptée pour échanger rapidement de grandes quantités de données.
Elle nécessite cependant généralement un mécanisme supplémentaire de synchronisation.
51. Schéma général d'utilisation
Création :
Code: Select all
mq_open()
Configuration éventuelle :
Code: Select all
mq_getattr()
mq_setattr()
Communication :
Code: Select all
mq_send()
mq_receive()
Code: Select all
mq_timedsend()
mq_timedreceive()
Notification éventuelle :
Code: Select all
mq_notify()
Fermeture :
Code: Select all
mq_close()
Suppression :
Code: Select all
mq_unlink()
Une POSIX Message Queue conserve des messages distincts.
La fonction :
Code: Select all
mq_open()
Le type du descripteur est :
Code: Select all
mqd_t
Code: Select all
mq_send()
Code: Select all
mq_receive()
Les messages possèdent une priorité.
La priorité la plus élevée est reçue en premier.
À priorité égale, l'ordre reste FIFO.
Les attributs sont décrits par :
Code: Select all
struct mq_attr
mq_flags
mq_maxmsg
mq_msgsize
mq_curmsgs
Code: Select all
mq_getattr() récupère ces informations.
mq_setattr() permet principalement de modifier le comportement O_NONBLOCK.
- mq_send() ne bloque pas si la file est pleine ;
- mq_receive() ne bloque pas si la file est vide.
Code: Select all
EAGAIN
Les fonctions :
Code: Select all
mq_timedsend()
Code: Select all
mq_timedreceive()
Code: Select all
mq_notify() permet de recevoir une notification lorsqu'un message devient disponible.
Code: Select all
SIGEV_SIGNAL
Code: Select all
SIGEV_THREAD
Code: Select all
mq_close() ferme un descripteur.
mq_unlink() supprime le nom de la file.
Code: Select all
/dev/mqueue
Les POSIX Message Queues constituent un mécanisme IPC permettant d'échanger des messages structurés entre processus.
Elles sont ouvertes avec mq_open() et représentées par des descripteurs de type mqd_t.
Les messages sont envoyés avec mq_send() et reçus avec mq_receive().
Chaque message possède une priorité, ce qui permet au noyau de délivrer en premier les messages les plus importants.
Le comportement bloquant peut être modifié avec O_NONBLOCK.
Les fonctions temporisées permettent d'éviter une attente infinie.
Le mécanisme mq_notify() permet de transformer une file en mécanisme plus événementiel, en prévenant le processus lorsqu'un nouveau message devient disponible.
Enfin, mq_close() et mq_unlink() séparent clairement la fermeture d'un descripteur et la suppression du nom de l'objet.
Les POSIX Message Queues sont donc particulièrement adaptées aux communications entre processus lorsque l'on souhaite conserver les frontières des messages, gérer des priorités ou recevoir des notifications asynchrones.
