GNU/Linux >> Tutoriels Linux >  >> Linux

Communication inter-processus sous Linux :utilisation de canaux et de files d'attente de messages

Ceci est le deuxième article d'une série sur la communication interprocessus (IPC) sous Linux. Le premier article portait sur la CIB via le stockage partagé :fichiers partagés et segments de mémoire partagée. Cet article se tourne vers les canaux, qui sont des canaux qui connectent les processus pour la communication. Un canal a une fin d'écriture pour écrire des octets, et une fin de lecture pour lire ces octets dans l'ordre FIFO (premier entré, premier sorti). Dans une utilisation typique, un processus écrit sur le canal et un processus différent lit à partir de ce même canal. Les octets eux-mêmes peuvent représenter n'importe quoi :des chiffres, des dossiers d'employés, des films numériques, etc.

Les tubes existent en deux types, nommés et sans nom, et peuvent être utilisés de manière interactive à partir de la ligne de commande ou dans des programmes ; des exemples sont à venir. Cet article se penche également sur les files d'attente de mémoire, qui sont tombées en désuétude, mais à tort.

Les exemples de code du premier article reconnaissaient la menace de conditions de concurrence (basées sur les fichiers ou sur la mémoire) dans IPC qui utilise le stockage partagé. La question se pose naturellement de la sécurité de la concurrence pour l'IPC basé sur les canaux, qui sera abordée dans cet article. Les exemples de code pour les canaux et les files d'attente de mémoire utilisent des API avec le sceau d'approbation POSIX, et l'un des principaux objectifs des normes POSIX est la sécurité des threads.

Considérez les pages de manuel pour le mq_open fonction, qui appartient à l'API de file d'attente mémoire. Ces pages incluent une section sur les Attributs avec ce petit tableau :

Interface Attribut Valeur
mq_open() Sécurité des fils MT-Safe

La valeur MT-Safe (avec MT pour le multithread) signifie que le mq_open la fonction est thread-safe, ce qui implique à son tour un process-safe :un processus s'exécute précisément dans le sens où l'un de ses threads s'exécute, et si une condition de concurrence ne peut pas survenir entre les threads dans le même processus, une telle condition ne peut pas survenir entre les threads de différents processus. Le MT-Safe l'attribut garantit qu'une condition de concurrence ne se produit pas dans les invocations de mq_open . En général, l'IPC basé sur les canaux est sécurisé en simultané, bien qu'une mise en garde soit soulevée dans les exemples qui suivent.

Canaux sans nom

Commençons par un exemple de ligne de commande artificiel qui montre comment fonctionnent les canaux sans nom. Sur tous les systèmes modernes, la barre verticale | représente un canal sans nom sur la ligne de commande. Supposons % est l'invite de la ligne de commande, et considérez cette commande :

% sleep 5 | echo "Hello, world!" ## writer to the left of |, reader to the right 

Le sommeil et écho les utilitaires s'exécutent en tant que processus séparés et le canal sans nom leur permet de communiquer. Cependant, l'exemple est artificiel en ce qu'aucune communication ne se produit. La salutation Hello, world ! apparaît à l'écran ; puis, après environ cinq secondes, l'invite de la ligne de commande revient, indiquant que les deux dorment et écho les processus sont terminés. Que se passe-t-il ?

Dans la syntaxe de la barre verticale de la ligne de commande, le processus à gauche (dormir ) est l'auteur, et le processus à droite (echo ) est le lecteur. Par défaut, le lecteur bloque jusqu'à ce qu'il y ait des octets à lire sur le canal, et l'écrivain, après avoir écrit ses octets, termine en envoyant un marqueur de fin de flux. (Même si l'écrivain se termine prématurément, un marqueur de fin de flux est envoyé au lecteur.) Le canal sans nom persiste jusqu'à ce que l'écrivain et le lecteur se terminent.

[Télécharger le guide complet de la communication inter-processus sous Linux]

Dans l'exemple artificiel, le dormir Le processus n'écrit aucun octet dans le canal, mais se termine après environ cinq secondes, ce qui envoie un marqueur de fin de flux au canal. En attendant, l'écho Le processus écrit immédiatement le message d'accueil sur la sortie standard (l'écran) car ce processus ne lit aucun octet du canal, il n'attend donc pas. Une fois le sommeil et écho les processus se terminent, le canal sans nom - qui n'est pas du tout utilisé pour la communication - disparaît et l'invite de ligne de commande revient.

Voici un exemple plus utile utilisant deux canaux sans nom. Supposons que le fichier test.dat ressemble à ceci :

c'est
la
voie
la
fin du monde
fin

La commande :

% cat test.dat | sort | uniq 

dirige la sortie du cat (concaténer) processus dans le sort processus pour produire une sortie triée, puis dirige la sortie triée vers le uniq processus pour éliminer les enregistrements en double (dans ce cas, les deux occurrences de le réduire à un) :

fins
est
la
cette
façon
monde

Le décor est maintenant planté pour un programme avec deux processus qui communiquent via un canal sans nom.

Exemple 1. Deux processus communiquant via un canal sans nom.

#include  /* attendre */
#include
#include   /* quitter les fonctions */
# include   /* read, write, pipe, _exit */
#include

#define ReadEnd  0
#define WriteEnd 1

void report_and_exit(const char* msg) {
  perror(msg);
  exit(-1); /** échec **/
}

int main() {
  int pipeFDs[2] ; /* deux descripteurs de fichier */
  char buf ; /* Tampon de 1 octet */
  const char* msg ="Le premier vert de la nature est l'or\n" ; /* octets à écrire */

  if (pipe(pipeFDs) <0) report_and_exit("pipeFD");
  pid_t cpid =fork(); /* fork un processus enfant */
  if (cpid <0) report_and_exit("fork"); /* vérifier l'échec */

  if (0 ==cpid) {    /*** enfant ***/                 /* processus enfant */
    close(pipeFDs[WriteEnd]); /* l'enfant lit, n'écrit pas */

    while (read(pipeFDs[ReadEnd], &buf, 1)> 0)       /* lit jusqu'à la fin du flux d'octets */
     écrit (STDOUT_FILENO, &buf, sizeof(buf)); /* écho à la sortie standard */

    close(pipeFDs[ReadEnd]); /* ferme le ReadEnd :tout est fait */
    _exit(0); /* quitter et informer immédiatement le parent  */
  }
  else {              /*** parent ***/
    close(pipeFDs[ReadEnd]); /* le parent écrit, ne lit pas */

    write(pipeFDs[WriteEnd], msg, strlen(msg)); /* écrit les octets dans le tube */
    close(pipeFDs[WriteEnd]); /* écriture terminée :générer eof */

    wait(NULL); /* attend que l'enfant quitte */
    exit(0); /* sortie normalement */
  }
  return 0 ;
}

Le pipeUN le programme ci-dessus utilise la fonction système fork pour créer un processus. Bien que le programme n'ait qu'un seul fichier source, un traitement multiple se produit pendant l'exécution (réussie). Voici les détails dans un examen rapide du fonctionnement de la bibliothèque fork fonctionne :

  • La fourchette fonction, appelée dans le parent processus, renvoie -1 au parent en cas d'échec. Dans le pipeUN exemple, l'appel est :
    pid_t cpid = fork(); /* called in parent */ 

    La valeur retournée est stockée, dans cet exemple, dans la variable cpid de type entier pid_t . (Chaque processus a son propre identifiant de processus , un entier non négatif qui identifie le processus.) La création d'un nouveau processus peut échouer pour plusieurs raisons, y compris une table de processus complète , une structure que le système gère pour suivre les processus. Les processus zombies, clarifiés brièvement, peuvent entraîner le remplissage d'une table de processus s'ils ne sont pas récoltés.

  • Si le fork réussit, il engendre (crée) un nouveau processus enfant, renvoyant une valeur au parent mais une valeur différente à l'enfant. Le processus parent et le processus enfant exécutent le même code qui suit l'appel à fork . (L'enfant hérite des copies de toutes les variables déclarées jusqu'à présent dans le parent.) En particulier, un appel réussi à fork renvoie :
    • Zéro au processus enfant
    • L'ID de processus de l'enfant au parent
  • Un si/sinon ou une construction équivalente est généralement utilisée après un fork réussi appel pour séparer le code destiné au parent du code destiné à l'enfant. Dans cet exemple, la construction est :
    if (0 ==cpid) {    /*** child ***/
    ...
    }
    else { /*** parent ***/
    }

Si la duplication d'un enfant réussit, le pipeUN programme se déroule comme suit. Il existe un tableau d'entiers :

int pipeFDs[2]; /* two file descriptors */ 

pour contenir deux descripteurs de fichier, un pour écrire dans le tube et un autre pour lire à partir du tube. (L'élément de tableau pipeFDs[0] est le descripteur de fichier pour la fin de lecture et l'élément de tableau pipeFDs[1] est le descripteur de fichier pour la fin d'écriture.) Un appel réussi au système pipe fonction, faite juste avant l'appel à fork , remplit le tableau avec les deux descripteurs de fichier :

if (pipe(pipeFDs) < 0) report_and_exit("pipeFD"); 

Plus de ressources Linux

  • Aide-mémoire des commandes Linux
  • Aide-mémoire des commandes Linux avancées
  • Cours en ligne gratuit :Présentation technique de RHEL
  • Aide-mémoire sur le réseau Linux
  • Aide-mémoire SELinux
  • Aide-mémoire sur les commandes courantes de Linux
  • Que sont les conteneurs Linux ?
  • Nos derniers articles Linux

Le parent et l'enfant ont maintenant des copies des deux descripteurs de fichier, mais la séparation des préoccupations motif signifie que chaque processus requiert exactement un des descripteurs. Dans cet exemple, le parent écrit et l'enfant lit, bien que les rôles puissent être inversés. La première déclaration dans l'enfant if -clause code, par conséquent, ferme la fin d'écriture du tube :

close(pipeFDs[WriteEnd]); /* called in child code */ 

et la première instruction dans le parent else -clause code ferme la fin de lecture du tube :

close(pipeFDs[ReadEnd]);  /* called in parent code */ 

Le parent écrit ensuite quelques octets (codes ASCII) dans le tube sans nom, et l'enfant les lit et les renvoie à la sortie standard.

Un autre aspect du programme doit être clarifié :l'appel à l'attente fonction dans le code parent. Une fois généré, un processus enfant est largement indépendant de son parent, car même le court pipeUN programme illustre. L'enfant peut exécuter du code arbitraire qui n'a rien à voir avec le parent. Cependant, le système avertit le parent par un signal—si et quand l'enfant se termine.

Que se passe-t-il si le parent met fin avant l'enfant ? Dans ce cas, sauf précautions, l'enfant devient et reste un zombie processus avec une entrée dans la table de processus. Les précautions sont de deux grands types. Une précaution consiste à demander au parent d'informer le système qu'il n'a aucun intérêt dans le licenciement de l'enfant :

signal(SIGCHLD, SIG_IGN); /* in parent: ignore notification */ 

Une deuxième approche consiste à demander au parent d'exécuter une attente à la fin de l'enfant, garantissant ainsi que le parent survit à l'enfant. Cette deuxième approche est utilisée dans le pipeUN programme, où le code parent a cet appel :

wait(NULL); /* called in parent */ 

Cet appel à attendre signifie attendre que l'arrêt de tout enfant se produise , et dans le pipeUN programme, il n'y a qu'un seul processus enfant. (Le NULL peut être remplacé par l'adresse d'une variable entière pour contenir le statut de sortie de l'enfant.) Il existe un waitpid plus flexible fonction pour un contrôle fin, par exemple, pour spécifier un processus enfant particulier parmi plusieurs.

Le pipeUN programme prend une autre précaution. Lorsque le parent a fini d'attendre, il termine avec l'appel à la sortie habituelle une fonction. En revanche, l'enfant se termine par un appel à la _exit variante, qui accélère la notification de résiliation. En effet, l'enfant dit au système d'informer le parent dès que possible que l'enfant a terminé.

Si deux processus écrivent dans le même canal sans nom, les octets peuvent-ils être entrelacés ? Par exemple, si le processus P1 écrit :

foo bar 

dans un tube et le processus P2 écrit simultanément :

baz baz 

au même tube, il semble que le contenu du tube puisse être quelque chose d'arbitraire, tel que :

baz foo baz bar 

La norme POSIX garantit que les écritures ne sont pas entrelacées tant qu'aucune écriture ne dépasse PIPE_BUF octets. Sur les systèmes Linux, PIPE_BUF a une taille de 4 096 octets. Ma préférence avec les canaux est d'avoir un seul rédacteur et un seul lecteur, évitant ainsi le problème.

Canaux nommés

Un canal sans nom n'a pas de fichier de sauvegarde :le système maintient un tampon en mémoire pour transférer les octets de l'enregistreur au lecteur. Une fois que l'écrivain et le lecteur se terminent, le tampon est récupéré, de sorte que le canal sans nom disparaît. En revanche, un tube nommé a un fichier de sauvegarde et une API distincte.

Examinons un autre exemple de ligne de commande pour comprendre l'essentiel des canaux nommés. Voici les étapes :

  • Ouvrez deux terminaux. Le répertoire de travail doit être le même pour les deux.
  • Dans l'un des terminaux, entrez ces deux commandes (l'invite est à nouveau % , et mes commentaires commencent par ## ) :
    % mkfifo tester  ## crée un fichier de sauvegarde nommé tester
    % cat tester     ## saisissez le contenu du tube dans stdout

    Au début, rien ne devrait apparaître dans le terminal car rien n'a encore été écrit dans le tube nommé.

  • Dans le deuxième terminal, saisissez la commande :
    % cat> tester  ## redirige la saisie au clavier vers le canal
    hello, world ! ## puis appuyez sur la touche Entrée
    bye, bye        ## idem
        ## terminez la session avec un Control-C

    Tout ce qui est tapé dans ce terminal est répercuté dans l'autre. Une fois Ctrl+C est entré, l'invite de ligne de commande habituelle revient dans les deux terminaux :le tuyau a été fermé.

  • Nettoyez en supprimant le fichier qui implémente le canal nommé :
    % unlink tester 

Comme nom de l'utilitaire mkfifo implique, un canal nommé est également appelé FIFO car le premier octet entrant est le premier octet sortant, et ainsi de suite. Il existe une fonction de bibliothèque nommée mkfifo qui crée un canal nommé dans les programmes et est utilisé dans l'exemple suivant, qui se compose de deux processus :l'un écrit dans le canal nommé et l'autre lit à partir de ce canal.

Exemple 2. Le fifoWriter programme

#include 
#include
#include
#include
#include
#include
#include

#define MaxLoops         12000   /* boucle externe */
#define ChunkSize           16   /* combien d'écrits à la fois */
#define IntsPerChunk         4   /* quatre entiers de 4 octets par bloc */
#define MaxZs              250   /* max microsecondes pour dormir * /

int main() {
  const char* pipeName ="./fifoChannel";
  mkfifo(pipeName, 0666); /* lecture/écriture pour utilisateur/groupe/autres */
  int fd =open(pipeName, O_CREAT | O_WRONLY); /* ouvert en écriture seule */
  if (fd <0) return -1 ; /* ne peut pas continuer */

  int i;
  for (i =0; i     int j;
    for (j =0; j       int k;
      int chunk[IntsPerChunk];
      for (k =0 ; k         chunk[k] =rand();
      write(fd, chunk, sizeof(chunk));
    }
    usleep ((rand() % MaxZs) + 1); /* pause un peu pour plus de réalisme */
  }

  close(fd); /* close pipe :génère un marqueur de fin de flux */
  unlink(pipeName); /* dissocier du fichier d'implémentation */
  printf("%i ints envoyés au tube.\n", MaxLoops * ChunkSize * IntsPerChunk);

  return 0 ;
}

Le fifoWriter programme ci-dessus peut être résumé comme suit :

  • Le programme crée un tube nommé pour écrire :
    mkfifo(pipeName, 0666); /* autorisations de lecture/écriture pour utilisateur/groupe/autres */
    int fd =open(pipeName, O_CREAT | O_WRONLY);

    pipeName est le nom du fichier de sauvegarde transmis à mkfifo comme premier argument. Le tube nommé est alors ouvert avec l'appel désormais familier à open fonction, qui renvoie un descripteur de fichier.

  • Pour une touche de réalisme, le fifoWriter n'écrit pas toutes les données à la fois, mais écrit à la place un morceau, dort un nombre aléatoire de microsecondes, etc. Au total, 768 000 valeurs entières de 4 octets sont écrites dans le canal nommé.
  • Après avoir fermé le tube nommé, le fifoWriter dissocie également le fichier :
    close(fd); /* close pipe :génère un marqueur de fin de flux */
    unlink(pipeName); /* dissocier du fichier d'implémentation */

    Le système récupère le fichier de sauvegarde une fois que chaque processus connecté au canal a effectué l'opération de dissociation. Dans cet exemple, il n'y a que deux processus de ce type :le fifoWriter et le fifoReader , qui font tous les deux une dissociation opération.

Les deux programmes doivent être exécutés dans des terminaux différents avec le même répertoire de travail. Cependant, le fifoWriter doit être lancé avant le fifoReader , car le premier crée le tuyau. Le fifoReader accède alors au tube nommé déjà créé.

Exemple 3. Le fifoReader programme

#include 
#include
#include
#include
#include

unsigned is_prime(unsigned n) { /* pas joli, mais efficace */
  if (n <=3) return n> 1;
  if ( 0 ==(n % 2) || 0 ==(n % 3)) renvoie 0 ;

  i non signé ;
  for (i =5; (i * i) <=n ; i +=6)
    si (0 ==(n % i) || 0 ==(n % (i + 2))) renvoie 0 ;

  renvoie 1 ; /* a trouvé un nombre premier ! */
}

int main() {
  const char* file ="./fifoChannel";
  int fd =open(file, O_RDONLY);
  si (fd <0) renvoie -1 ; /* inutile de continuer */
  unsigned count =0, total =0, primes_count =0 ;

  while (1) {
    int next;
    int i;

    ssize_t count =read(fd, &next, sizeof(int));
    if (0 ==count) break ; /* fin du flux */
    else if (count ==sizeof(int)) {        /* lit une valeur int de 4 octets */
      total++ ;
      if (is_prime(next) ) nombre_primes++ ;
    }
  }

  close(fd); /* ferme le tube à partir de la fin de la lecture */
  unlink(file); /* dissocier du fichier sous-jacent */
  printf("Entiers reçus : %u, nombres premiers : %u\n", total, nombre_nombre de primes);

  return 0 ;
}

Le fifoReader programme ci-dessus peut être résumé comme suit :

  • Parce que le fifoWriter crée le tube nommé, le fifoReader n'a besoin que de l'appel standard open pour accéder au tube via le fichier de sauvegarde :
    const char* file ="./fifoChannel" ;
    int fd =open(file, O_RDONLY);

    Le fichier s'ouvre en lecture seule.

  • Le programme entre alors dans une boucle potentiellement infinie, essayant de lire un morceau de 4 octets à chaque itération. La lecture call :
    ssize_t count = read(fd, &next, sizeof(int)); 

    renvoie 0 pour indiquer la fin du flux, auquel cas le fifoReader sort de la boucle, ferme le canal nommé et dissocie le fichier de sauvegarde avant de se terminer.

  • Après avoir lu un entier de 4 octets, le fifoReader vérifie si le nombre est premier. Cela représente la logique métier qu'un lecteur de niveau production peut exécuter sur les octets reçus. Sur un échantillon, il y avait 37 682 nombres premiers parmi les 768 000 entiers reçus.

Lors d'exécutions répétées d'échantillons, le fifoReader lire avec succès tous les octets que le fifoWriter a écrit. Ce n'est pas surprenant. Les deux processus s'exécutent sur le même hôte, éliminant les problèmes de réseau de l'équation. Les canaux nommés sont un mécanisme IPC hautement fiable et efficace et, par conséquent, largement utilisé.

Voici la sortie des deux programmes, chacun lancé depuis un terminal distinct mais avec le même répertoire de travail :

% ./fifoWriter
768 000 entiers envoyés au tube.
###
% ./fifoReader
 Entiers reçus :768 000, nombres premiers :37 682

Files d'attente de messages

Les tubes ont un comportement FIFO strict :le premier octet écrit est le premier octet lu, le deuxième octet écrit est le deuxième octet lu, et ainsi de suite. Les files d'attente de messages peuvent se comporter de la même manière mais sont suffisamment flexibles pour que les blocs d'octets puissent être récupérés dans l'ordre FIFO.

Comme son nom l'indique, une file d'attente de messages est une séquence de messages, chacun composé de deux parties :

  • La charge utile, qui est un tableau d'octets (char en C)
  • Un type, donné sous la forme d'une valeur entière positive ; les types catégorisent les messages pour une récupération flexible

Considérez la représentation suivante d'une file d'attente de messages, chaque message étant étiqueté avec un type entier :

          +-+    +-+    +-+    +-+
expéditeur--->|3|--->|2|--->|2|--->|1|-- ->récepteur
          +-+    +-+    +-+    +-+

Parmi les quatre messages affichés, celui marqué 1 est à l'avant, c'est-à-dire le plus proche du récepteur. Viennent ensuite deux messages avec l'étiquette 2, et enfin, un message étiqueté 3 à l'arrière. Si un comportement FIFO strict était en jeu, alors les messages seraient reçus dans l'ordre 1-2-2-3. Cependant, la file d'attente de messages autorise d'autres ordres de récupération. Par exemple, les messages pourraient être récupérés par le destinataire dans l'ordre 3-2-1-2.

La mqueue exemple se compose de deux programmes, le expéditeur qui écrit dans la file d'attente des messages et le récepteur qui lit à partir de cette file d'attente. Les deux programmes incluent le fichier d'en-tête queue.h illustré ci-dessous :

Exemple 4. Le fichier d'en-tête queue.h

#define ProjectId 123
#define PathName  "queue.h" /* n'importe quel fichier accessible existant ferait l'affaire */
#define MsgLen    4
#define MsgCount  6

typedef struct {
  type long ; /* doit être de type long */
  char payload[MsgLen + 1] ; /* octets dans le message */
} queuedMessage ;

Le fichier d'en-tête définit un type de structure nommé queuedMessage , avec charge utile (tableau d'octets) et type champs (entiers). Ce fichier définit également des constantes symboliques (le #define instructions), dont les deux premières sont utilisées pour générer une clé qui, à son tour, est utilisée pour obtenir un ID de file d'attente de messages. L'ID de projet peut être n'importe quelle valeur entière positive, et le PathName doit être un fichier existant et accessible — dans ce cas, le fichier queue.h . Les instructions de configuration à la fois dans l'expéditeur et le récepteur les programmes sont :

key_t key =ftok(PathName, ProjectId); /* générer la clé */
int qid =msgget(clé, 0666 | IPC_CREAT); /* utiliser la clé pour obtenir l'identifiant de la file d'attente */

L'identifiant qid est, en fait, le pendant d'un descripteur de fichier pour les files d'attente de messages.

Exemple 5. Le message expéditeur programme

#include 
#include
#include
#include
#include
#include "queue.h"

void report_and_exit(const char* msg) {
  perror(msg);
  exit (-1); /* EXIT_FAILURE */
}

int main() {
  key_t key =ftok(PathName, ProjectId);
  if (key <0) report_and_exit(" impossible d'obtenir la clé...");

  int qid =msgget(clé, 0666 | IPC_CREAT);
  if (qid <0) report_and_exit("impossible d'obtenir l'identifiant de la file d'attente ...");

  char* payloads[] ={"msg1", "msg2", "msg3", "msg4", "msg5", "msg6"} ;
int types[] ={1, 1, 2, 2, 3, 3} ; /* chacun doit être> 0 */
  int i;
  for (i =0; i     /* construit le message */
    queuedMessage msg;
    msg.type =types[i] ;
    strcpy(msg.payload, payloads[i]);

    /* envoyer le message */
    msgsnd (qid, &msg, sizeof(msg), IPC_NOWAIT); /* ne pas bloquer */
    printf("%s envoyé comme type %i\n", msg.payload, (int) msg.type);
  }
  return 0 ;
}

L'expéditeur programme ci-dessus envoie six messages, deux chacun d'un type spécifié :les premiers messages sont de type 1, les deux suivants de type 2 et les deux derniers de type 3. L'instruction d'envoi :

msgsnd(qid, &msg, sizeof(msg), IPC_NOWAIT); 

est configuré pour être non bloquant (le drapeau IPC_NOWAIT ) parce que les messages sont si petits. Le seul danger est qu'une file d'attente pleine, peu probable dans cet exemple, entraîne un échec d'envoi. Le récepteur le programme ci-dessous reçoit également des messages en utilisant le IPC_NOWAIT drapeau.

Exemple 6. Le message récepteur programme

#include 
#include
#include
#include
#include "queue.h"

void report_and_exit(const char* msg) {
  perror(msg);
  exit(-1); /* EXIT_FAILURE */
}

int main() {
  key_t key=ftok(PathName, ProjectId); /* clé pour identifier la file d'attente */
  if (key <0) report_and_exit("key not gotten...");

  int qid =msgget(key, 0666 | IPC_CREAT); /* accès si déjà créé */
  if (qid <0) report_and_exit("pas d'accès à la file d'attente...");

  int types[] ={3, 1, 2 , 1, 3, 2} ; /* différent de celui de l'expéditeur */
  int i ;
  for (i =0 ; i     queuedMessage msg ; /* défini dans queue.h */
    if (msgrcv(qid, &msg, sizeof(msg), types[i], MSG_NOERROR | IPC_NOWAIT) <0)
      puts("msgrcv problem... ");
    printf("%s reçu comme type %i\n", msg.payload, (int) msg.type);
  }

  /** supprimer la file d'attente **/
  if (msgctl(qid, IPC_RMID, NULL) <0)  /* NULL ='no flags' */
    report_and_exit("problème de suppression de la file d'attente...");

  renvoie 0 ;
}

Le récepteur programme ne crée pas la file d'attente de messages, bien que l'API le suggère. Dans le récepteur , l'appel :

int qid = msgget(key, 0666 | IPC_CREAT); 

est trompeur à cause de IPC_CREAT flag, mais ce flag signifie en fait créer si nécessaire, sinon accéder . L'expéditeur le programme appelle msgsnd pour envoyer des messages, alors que le destinataire appelle msgrcv pour les récupérer. Dans cet exemple, l'expéditeur envoie les messages dans l'ordre 1-1-2-2-3-3, mais le récepteur puis les récupère dans l'ordre 3-1-2-1-3-2, montrant que les files d'attente de messages ne sont pas liées au comportement FIFO strict :

% ./sender
msg1 envoyé en tant que type 1
msg2 envoyé en tant que type 1
msg3 envoyé en tant que type 2
msg4 envoyé en tant que type 2
msg5 envoyé en tant que type 3
msg6 envoyé comme type 3

% ./receiver
msg5 reçu comme type 3
msg1 reçu comme type 1
msg3 reçu comme type 2
msg2 reçu comme type 1
msg6 reçu comme type 3
msg4 reçu comme type 2

La sortie ci-dessus montre que l'expéditeur et le récepteur peuvent être lancés depuis le même terminal. La sortie montre également que la file d'attente des messages persiste même après l'expéditeur Le processus crée la file d'attente, y écrit et se termine. La file d'attente ne disparaît qu'après le récepteur le processus le supprime explicitement avec l'appel à msgctl :

if (msgctl(qid, IPC_RMID, NULL) < 0) /* remove queue */ 

Conclusion

Les canaux et les API de file d'attente de messages sont fondamentalement unidirectionnels :un processus écrit et un autre lit. Il existe des implémentations de canaux nommés bidirectionnels, mais mes deux cents sont que ce mécanisme IPC est à son meilleur quand il est le plus simple. Comme indiqué précédemment, les files d'attente de messages ont perdu de leur popularité, mais sans raison valable ; ces files d'attente sont encore un autre outil dans la boîte à outils IPC. La partie 3 complète cette visite rapide de la boîte à outils IPC avec des exemples de code d'IPC via des sockets et des signaux.


Linux
  1. Présentation du guide de communication inter-processus sous Linux

  2. Communication inter-processus sous Linux :Sockets et signaux

  3. Comment démarrer la commande Linux en arrière-plan et détacher le processus dans le terminal

  4. Comment tuer un processus sous Linux en utilisant la commande ?

  5. Comment vérifier le système d'exploitation et la version à l'aide d'une commande Linux

Commande murale sous Linux

Trucs et astuces pour utiliser la commande wget Linux

Comment cloner et restaurer une partition Linux à l'aide de la commande dd

commande mailx sous Linux - envoyer et recevoir du courrier Internet

Utilisation de la commande Watch sous Linux

Comment tuer les processus sous Linux en utilisant kill, killall et pkill