original in en: Leonardo Giordani
en to fr: Paul Delannoy
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.
Pour comprendre l'article il faudrait avoir :
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 } }
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.
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.