Programmation en temps partag� - Files d'attente de messages (2)

ArticleCategory:

SoftwareDevelopment

AuthorImage:

[Leonardo]

TranslationInfo:

original in en: Leonardo Giordani

en to fr: Paul Delannoy

AboutTheAuthor:

Etudiant ing�nieur en t�l�communications � l'�cole Polytechnique de Milan, il travaille comme administrateur r�seau et s'int�resse � la programmation (surtout en langage assembleur et en C/C++). Depuis 1999 il ne travaille que sous Linux/Unix.

Abstract:

Cette s�rie d'articles se propose d'initier le lecteur au concept de multit�che et � sa mise en oeuvre dans le syst�me d'exploitation Linux. Nous partirons des concepts th�oriques de base concernant le multit�che pour aboutir � l'�criture compl�te d'une application illustrant la communication entre processus, avec un protocole simple mais efficace.

Pour comprendre l'article il faudrait avoir :

Toute r�f�rence � des pages de manuel est plac�e entre parenth�ses apr�s le nom de la commande concern�e. Toute fonction de la glibc est document�e par la commande "info Libc".

ArticleIllustration:

[run in paralell]

ArticleBody:

Introduction

Nous avons appris dans l'article pr�c�dent � synchroniser et � faire travailler ensemble deux processus (ou plus) en utilisant des files d'attente de messages. Nous allons maintenant un peu plus loin, en commen�ant � cr�er notre propre protocole d'�change de messages.

Nous avons pr�c�demment d�fini un protocole comme un ensemble de r�gles qui permettent le dialogue entre deux personnes ou machines, m�me si elles sont diff�rentes. L'usage de la langue anglaise par exemple est un protocole, puisqu'il me permet de parler � mes lecteurs indiens (qui ont toujours �t� tr�s int�ress�s par ce que j'�cris). Pour parler de choses plus proches de Linux, si vous recompilez votre noyau (pas de panique, ce n'est pas si compliqu�), vous remarquerez sans doute la section Networking (R�seau), qui permet de faire comprendre � votre noyau diff�rents protocoles r�seau, comme TCP/IP.

Afin de cr�er un protocole, nous allons commencer par d�finir le type d'application que nous envisageons. Notre exemple sera un simulateur de commutateur t�l�phonique. Un processus principal simulera le commutateur lui-m�me, et ses processus fils simuleront les actions des utilisateurs : chaque utilisateur devra pouvoir �changer des messages avec un autre au travers du commutateur.

Le protocole doit g�rer trois situations diff�rentes : la naissance d'un utilisateur (i.e. l'utilisateur existe et se connecte), le travail courant d'un utilisateur, et la disparition d'un utilisateur (il n'est plus connect�). Abordons les trois cas :

Lorsqu'un utilisateur se connecte au syst�me, il cr�e sa propre file d'attente de messages (n'oublions pas : c'est un processus), dont les identifiants doivent �tre envoy�s au commutateur afin que celui-ci sache comment atteindre cet utilisateur. Il a le temps d'initialiser des structures de donn�es si n�cessaire. Il re�oit du commutateur l'identifiant d'une file d'attente de messages, dans laquelle il pourra �crire les messages d�livr�s � d'autres utilisateurs par le commutateur lui-m�me.

L'utilisateur peut donc envoyer et recevoir des messages. Lorsqu'il �met un message vers un autre utilisateur, deux cas sont possibles : le destinataire est connect� ou non. Nous d�cidons que dans les deux cas, un accus� de r�ception sera envoy� � l'exp�diteur, afin qu'il sache ce qu'il advient de son message. Ceci peut �tre accompli par le commutateur lui-m�me, sans que le destinataire n'ait � faire quoi que ce soit.

Lorsqu'un utilisateur se d�connecte, il doit en informer le commutateur, mais aucune autre action n'est n�cessaire. Le m�tacode d�crivant cette fa�on de faire est le suivant

/* Naissance */
create_queue
init
send_alive
send_queue_id
get_switch_queue_id

/* Travail courant */
while(!leaving){
 receive_all
 if(<send condition>){
  send_message
 }
 if(<leave condition>){
  leaving = 1
 }
}

/* Disparition */
send_dead

Maintenant il faut d�finir le comportement de notre commutateur t�l�phonique : lorsqu'un utilisateur se connecte il nous envoie l'identifiant de sa file d'attente de messages; nous devons conserver cet identifiant, afin de faire parvenir � cet utilisateur les messages qui lui sont destin�s, et nous devons r�pondre en lui fournissant l'identifiant d'une file d'attente dans laquelle il peut �crire les messages destin�s � d'autres utilisateurs. Nous devons ensuite analyser les messages en attente et v�rifier que les destinataires soient vivants : si le destinataire est connect� nous d�livrons le message, sinon nous l'ignorons; dans les deux cas nous informons l'exp�diteur. Lors de la disparition d'un utilisateur, nous supprimons simplement l'identifiant de sa file d'attente, il devient ainsi injoignable.

A nouveau, le m�tacode est le suivant :

while(1){
 /* Nouvel utilisateur */
 if (<birth of a user>){
  get_queue_id
  send switch_queue_id  
 }

 /* Disparition utilisateur */
 if (<death of a user>){
  remove_user
 }

 /* Distribution des messages */
 check_message
 if (<user alive>){
  send_message
  ack_sender_ok
 }
 else{
  ack_sender_error
 }
}

Gestion des erreurs

G�rer les erreurs est une des choses les plus importantes et difficiles, dans la conduite d'un projet. De plus, un bon ensemble de routines de gestion d'erreurs, peut repr�senter la moiti� des lignes de code n�cessaires au projet. Nous n'allons pas expliquer ici comment d�velopper un bon syst�me d'analyse des erreurs, c'est un sujet trop cons�quent, mais � partir de maintenant je vais toujours tester les conditions d'erreur. Une bonne introduction � ces questions d'analyse d'erreurs est la lecture du manuel de la biblioth�que glibc (www.gnu.org) mais, si vous �tes int�ress�s, j'�crirai un article sur le sujet.

Mise en oeuvre du protocole - Couche 1

Notre petit protocole aura deux couches : la premi�re (la plus basse) est constitu�e des fonctions de gestion des files d'attente, de pr�paration et d'envoi des messages, alors que la plus haute met en oeuvre le protocole gr�ce � des fonctions ressemblant au m�tacode utilis� pour d�crire le comportement du commutateur et des utilisateurs.

La toute premi�re chose � faire est de d�finir la structure des messages, en utilisant le prototype msgbuf fourni par le noyau

typedef struct
{
  int service;
  int sender;
  int receiver;
  int data;
} messg_t;

typedef struct
{
  long mtype; /* Type du message */
  messg_t message;
} mymsgbuf_t;

Il y a ici quelque chose de g�n�ral que nous pourrons �tendre plus tard : les champs exp�diteur et destinataire contiennent un identifiant d'utilisateur et le champ donn�es contient des donn�es quelconques, tandis que le champ service permet d'adresser une demande particuli�re au commutateur. Par exemple il pourrait y avoir deux services pr�vus : l'un pour la distribution imm�diate d'un message, l'autre pour la distribution diff�r�e, le champ data devant alors contenir le d�lai en secondes. Ce n'est qu'un exemple, mais il permet de comprendre que le champ service offre de nombreuses possibilit�s.

Nous pouvons maintenant d�finir quelques fonctions pour traiter nos structures de donn�es, particuli�rement pour lire ou �crire les valeurs des champs des messages. Ce sont plus ou moins toujours les m�mes, aussi je ne vous en propose que deux, vous trouverez les autres dans les fichiers *.h

void set_sender(mymsgbuf_t * buf, int sender)
{
  buf->message.sender = sender;
}

int get_sender(mymsgbuf_t * buf)
{
  return(buf->message.sender);
}

Le but de telles fonctions n'est certes pas de compresser le code (elles ne contiennent qu'une seule ligne !) : elles ont pour but de conserver la d�finition du protocole proche d'un langage humain, et donc d'�tre plus simples � utiliser.

Ecrivons maintenant les fonctions destin�es � g�n�rer des cl�s IPC, cr�er et supprimer des files d'attente de messages, envoyer et recevoir des messages : construire une cl� IPC est simplement ceci :

key_t build_key(char c)
{
  key_t key;
  key = ftok(".", c);
  return(key);
}

La fonction qui cr�e une file d'attente

int create_queue(key_t key)
{
  int qid;
  
  if((qid = msgget(key, IPC_CREAT | 0660)) == -1){
    perror("msgget");
    exit(1);
  }
  
  return(qid);
}

comme vous le voyez la gestion d'erreurs est ici extr�mement simple. La fonction suivante supprime une file d'attente

int remove_queue(int qid)
{
  if(msgctl(qid, IPC_RMID, 0) == -1)
  {
    perror("msgctl");
    exit(1);
  }
  return(0);
}

Et enfin les fonctions de r�ception et d'envoi de messages : pour nous, �mettre un message c'est l'�crire dans une file d'attente sp�cifique, celle qui nous a �t� indiqu�e par le commutateur.

int send_message(int qid, mymsgbuf_t *qbuf)
{
  int result, lenght;
  lenght = sizeof(mymsgbuf_t) - sizeof(long);
  if ((result = msgsnd(qid, qbuf, lenght, 0)) == -1)
   {
     perror("msgsnd");
    exit(1);
    }
  
  return(result);
}

int receive_message(int qid, long type, mymsgbuf_t *qbuf)
{
  int result, length;
  length = sizeof(mymsgbuf_t) - sizeof(long);
  
  if((result = msgrcv(qid, (struct msgbuf *)qbuf, length, type, IPC_NOWAIT)) == -1)
   {
    if(errno == ENOMSG){
      return(0);
    }
    else
    {
      perror("msgrcv");
      exit(1);
    }
  }
  
  return(result);
}

C'est tout. Vous trouverez des instructions compl�mentaires dans le fichier layer1.h : essayez de cr�er un programme (par ex. celui du dernier article) avec elles. Dans le prochain article nous aborderons la couche 2 du protocole et sa mise en oeuvre.

Lectures recommand�es

Comme toujours vous pouvez m'adresser commentaires, corrections, questions � mon adresse �lectronique (leo.giordani(at)libero.it) ou par la page de discussion. S.V.P �crivez en anglais, allemand ou italien.