PROFDINFO.COM

Votre enseignant d'informatique en ligne

La communication inter-processus

Introduction

Puisque QNX Neutrino gère les threads globalement, la communication entre deux threads situées dans deux processus distincts est alors une communication inter-processus. La communication inter-processus peut être synchrone ou asynchrone, bloquante ou non-bloquante. La communication est synchrone lorsque le message ne peut être reçu par le récepteur qu'à un moment précis: lorsque celui-ci se place en attente d'un message (état RECEIVE). La communication est asynchrone lorsque le récepteur peut être interrompu à tout moment par un message. La communication est bloquante lorsque l'émetteur doit attendre une réponse du récepteur (états SEND et REPLY). Elle est non-bloquante si l'émetteur peut continuer son exécution sans se soucier du récepteur.

La messagerie de QNX Neutrino

La messagerie de QNX Neutrino est le système de communication inter-processus de base directement programmé dans le micro-noyau. C'est à la base une communication synchrone et bloquante. Voici son fonctionnement.

Une thread émettrice envoie un message synchrone à une thread réceptrice grâce à la fonction MsgSend. La thread émettrice bloquera (état SEND) jusqu'à ce que la thread réceptrice appelle la fonction MsgReceive. La thread émettrice passera alors de l'état SEND à l'état REPLY ce qui indique que la thread attend une réponse. La thread réceptrice traite le message et envoie une réponse à l'aide de la fonction MsgReply. Ce qui aura pour effet de débloquer la thread émettrice. Si la thread réceptrice appelle la fonction MsgReply avant que la thread émettrice n'envoie le message, elle bloquera (état RECEIVE). La fonction MsgReply permet de retourner un code erreur et un message tandis que la fonction MsgError ne retourne qu'un code d'erreur. Voici les différents états par lesquels passe la thread émettrice et la thread réceptrice:

États des msg

Il y deux schémas de communication par messages synchrones et bloquants:

  1. Le schéma send-driven est utilisé lors de communication client-serveur typique. Dans ce schéma le récepteur exécute une tâche après la réception d'un message:
    Émetteur
    Récepteur
    Envoie un message (MsgSend)
    Recoit un message (MsgReceive)
    Exécute une tâche
    Envoie un réponse (MsgReply)
  2. Le schéma reply-driven est utilisé par les systèmes de calcul distribué. L'émetteur exécute une tâche après la réception d'une réponse:
    Émetteur
    Récepteur
    Envoie un message (MsgSend)
    Recoit un message (MsgReceive)
    Envoie un réponse (MsgReply)
    Exécute une tâche

Les messages sont copiés de la zone mémoire de la thread émettrice vers la zone mémoire de la thread réceptrice. Il n'y a pas de temporisation. La vitesse de communication est donc très proche de la vitesse de la mémoire vive en autant que les threads résident sur les mêmes postes de travail.

Avant de pouvoir recevoir des messages, une thread réceptrice doit créer un canal de communication grâce à la fonction ChannelCreate. Lorsque la communication est terminée le récepteur doit détruire le canal avec la fonction ChannelDestroy.

Avant de pouvoir envoyer des messages, une thread émettrice doit se connecter à un canal de communication avec la fonction ConnectAttach. Lorsque la communication est terminée l'émetteur doit se déconnecter du canal avec la fonction ConnectDetach.

Les messages bloquants vs non-bloquants

Dans certaines situations, on ne veut pas que la thread émettrice attende une réponse du récepteur. Pour ce faire elle doit envoyer un pulse en utilisant la fonction MsgSendPulse. Un pulse ne peut contenir qu'un code sur 8 bits et un message sur 32 bits. La thread réceptrice peut recevoir un pulse à même la fonction MsgReceive. La fonction MsgReceivePulse est spécialement conçue pour ne recevoir que des pulses.

Pour une utilisation robuste des messages

Pour éviter que deux threads ou plus entrent en inter blocage, il faut:

Si vous respectez ces quelques règles simples, aucun inter blocage (du moins causé par les messages) n'est possible.

Les signaux

QNX implémente les signaux standards POSIX. La spécification POSIX originale définie les l'implémentation des signaux au niveau des processus seulement. Les signaux utilisent la communication asynchrone puisque le processus récepteur peut être interrompu à tout moment par un signal provenant du processus émetteur. C'est une interruption logiciel! Il n'y pas de réponse envoyée à la thread émettrice et les messages sont prédéterminés par des constantes (voir signal.h). Voici les signaux les plus importants:

Un processus peut envoyer un signal à elle-même avec la fonction raise ou à un autre processus avec la fonction kill. Un processus répond à un signal en installant un gestionnaire de signal avec la fonction signal dont voici le prototype:

void (*signal(int sig, void (*func)(int)))(int)

Voici un exemple qui affiche un message lorsque les touches CTRL-C sont enfoncées:

	  // Un même gestionnaire peut répondre à plusieurs signaux. 
	  // Le paramètre signum permet de connaître le numéro du signal reçu. 
	  static void GestionDuCTRL(int signum)
	  {
	     std::cout << "Ne pas toucher a CTRL-C!" << std::endl;
	     // réinstallation du gestionnaire par défaut 
	     signal(SIGINT, SIG_DFL);
	  } 
	  int main()
	  {
	     // Installation du gestionnaire de signal 
	     // Le nom du gestionnaire peut être remplacé par les constantes suivantes:
	     //    SIG_DFL: Gestionnaire par défaut  
	     //    SIG_IGN: Ignorer le signal 
	     signal(SIGINT, GestionDuCTRLC); 
	  }  	   

La mémoire partagées

Lorsqu'une grande quantité d'information doit être transférée d'un processus à un autre, le système de messagerie de base de QNX ne suffit plus. Nous pourrions dans ce cas utiliser le système de fichiers mais pour des raisons de performance cette solution est rarement viable. Il faut plutôt placer les données en mémoire vive et demander au système d'exploitation de créer une zone de mémoire partagée.

Dans un premier temps, il faut ouvrir (ou créer) une zone de mémoire partagée à l'aide de shm_open:

fd = shm_open("/nomdelazone", O_RDWR|O_CREAT, 0777);

Ensuite, il faut donner la taille voulue à la zone mémoire à l'aide de ftruncate:

ftruncate( fd, taille );

La taille doit être un multiple de la taille des pages PAGESIZE qui est de 4096 sur les processeurs Intel. Ensuite il faut associer une partie de la mémoire du processus à la zone mémoire partagée à l'aide de mmap:

addr = mmap(0,taille,PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);

Ensuite on peut utiliser librement la mémoire pointée par la variable addr:

*addr = 1; 

Lorsqu'on n'a plus besoin de la zone de mémoire partagée, on ferme la zone en utilisant close:

close(fd);

Finalement, on détruit la zone mémoire en utilisant shm_unlink:

shm_unlink("/nomdelazone");

Évidemment, la zone ne sera détruite que si elle est inutilisée. Contrairement au système de messagerie de base de QNX, une mémoire partagée ne permet pas de communiquer entre des postes distants.

Les autres types de communication inter-processus de QNX

Les files de messages POSIX

Un peut comme une boîte de réception de courriels, on peut créer des files de messages visibles par toutes les threads du système. La communication est non-bloquante. On peut voir les files de messages POSIX en cours d'utilisation dans le répertoire /dev/mqueue. On peut manipuler les files de messages POSIX avec les fonctions mq_open, mq_close, mq_unlink, mq_send et mq_receive. La gestion des files de messages POSIX n'est pas prise en charge par le micro-noyau mais par le processus serveur indépendant mq. L'utilisation de files de messages POSIX est plus souple que les messages de base de QNX mais moins efficace.

Les tuyaux

Un tuyau est un canal de communication unidirectionnel et temporaire établi entre un premier processus qui écrit dans le tuyau et un deuxième processus qui lit les données du tuyau. Le symbole | est utilisé pour connecter la sortie standard d'un processus à l'entrée standard d'un autre processus comme dans la commande ls | more. Par programmation, on utilise les tuyaux à l'aide des fonctions popen et pipe. La gestion des tuyaux n'est pas prise en charge par le micro-noyau mais par le processus serveur indépendant pipe.

Les FIFOs

Les FIFOs sont essentiellement identiques aux tuyaux sauf qu'ils sont conservés de façon permanente à même le système de fichiers.