Programmation en temps partag� - communication entre processus

ArticleCategory:

SoftwareDevelopment

AuthorImage:

[Leonardo]

TranslationInfo:

original in it: Leonardo Giordani

it to 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 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 :

Vous devriez lire le premier article de la s�rie, car il constitue une base indispensable � la compr�hension de celui-ci : November 2002, article 272.

ArticleIllustration:

[run in paralell]

ArticleBody:

Introduction

Nous sommes � nouveau ici en face des fonctions multit�ches du syst�me Linux. Comme nous l'avons dit dans l'article pr�c�dent, d�doubler l'ex�cution d'un programme dans un processus diff�rent ne n�cessite que quelques lignes de code, puisque c'est le syst�me d'exploitation qui prend en charge l'initialisation, la gestion et le partage du temps pour le processus cr��.

Ce service offert par le syst�me est fondamental, c'est la 'supervision de processus' ; chaque processus est ex�cut� dans un environnement qui lui est d�di�. Le d�veloppeur, qui perd ainsi le contr�le du d�roulement de l'ex�cution, a donc un probl�me de synchronisation, qui se r�sume par la question : comment est-il possible de faire collaborer deux processus distincts ?

Ce probl�me est plus complexe qu'il ne semble : il ne suffit pas de synchroniser l'ex�cution des processus, mais aussi de partager des donn�es, � la fois en mode lecture ou �criture.

Parlons un peu des probl�mes classiques pos�s par l'acc�s concurrentiel � des donn�es ; si deux processus lisent le m�me ensemble de donn�es, il n'y a pas de probl�me majeur et l'ex�cution est dite CONSISTENTE. Mais si un des processus est autoris� � modifier ces donn�es, le second pourra renvoyer des r�sultats diff�rents selon l'instant o� il lira les donn�es, avant ou apr�s que le premier ait �crit ces donn�es modifi�es. Par exemple : deux processus "A" et "B" partagent un entier "d". Le processus A incr�mente d de 1, le processus B affiche sa valeur. Dans un meta-langage nous pourrions l'exprimer ainsi :

A { d->d+1 } & B { d->output }

en donnant � "&" le sens d'une ex�cution en 'concurrence'. Une premi�re ex�cution pourra �tre :

(-) d = 5 (A) d = 6 (B) output = 6

mais si le processus B est ex�cut� le premier nous aurons :

(-) d = 5 (B) output = 5 (A) d = 6

Vous comprenez imm�diatement l'importance cruciale de la gestion de telles situations: le risque d'INCONSISTENCE des donn�es est grand et ne peut �tre accept�. Essayez d'imaginer que ces donn�es concernent votre compte bancaire, et vous ne sous-estimerez plus jamais ce probl�me.

Nous avons abord� une premi�re forme de synchronisation de processus dans l'article pr�c�dent, avec la fonction waitpid(2), qui force un processus � attendre l'ach�vement d'un autre avant de poursuivre sa propre ex�cution. Ceci permet en fait de r�gler une partie des conflits provoqu�s par la lecture et l'�criture des donn�es : d�s que l'ensemble de donn�es sur lequel un processus P1 doit travailler a �t� d�fini, un processus P2 qui doit travailler sur le m�me ensemble ou sur un sous-ensemble doit attendre l'ach�vement de P1 avant de commencer lui-m�me � s'ex�cuter.

Si cette m�thode pr�sente une solution, il est clair qu'elle n'est pas la meilleure, car P2 va devoir attendre un temps ind�termin�, qui peut �tre long, que P1 termine son ex�cution, et cela m�me s'il n'y a plus aucun travail sur des donn�es partag�es. Nous devons donc augmenter la granularit� de notre contr�le, c'est-�-dire, r�genter l'acc�s � une seule donn�e ou un seul jeu de donn�es. La solution � ce probl�me est un ensemble de primitives de la biblioth�que standard connue sous le nom de SysV IPC (System V InterProcess Communication).

Les cl�s de SysV

Avant d'aborder les arguments strictement li�s � la th�orie des processus concurrents et � leur mise en oeuvre, parlons un peu d'une structure type de SysV : les cl�s IPC. Une cl� IPC est un nombre qui identifie de mani�re unique une structure de contr�le IPC (elles seront d�crites plus loin), mais qui peut aussi �tre utilis� pour g�n�rer des identifiants g�n�riques, par exemple, pour organiser des structures non IPC. Une cl� peut �tre cr��e par la fonction ftok(3) :

key_t ftok(const char *pathname, int proj_id);

qui a besoin d'un nom de fichier existant (pathname) et d'un entier. L'unicit� de la cl� n'est pas assur�e, parce que les param�tres du fichier (les nombres d�signant son i-node et son p�riph�rique) peuvent mener � des combinaisons identiques. Une bonne solution consiste � cr�er une petite biblioth�que charg�e de conserver une trace des cl�s affect�es et d'�viter les doublons.

S�maphores

Le concept de s�maphore utilis� dans la r�gulation du trafic automobile est applicable sans grandes modifications au contr�le d'acc�s aux donn�es. Un s�maphore est une structure de donn�es contenant une valeur plus grande ou �gale � z�ro et qui g�re une file d'attente de processus attendant qu'advienne une condition particuli�re propre au s�maphore. Contrairement aux apparences, de simples s�maphores sont tr�s puissants et par cons�quent entra�nent une complexit� grandissante. Comme toujours, nous allons commencer en ignorant le contr�le d'erreurs : nous l'introduirons plus tard quand nos programmes deviendront plus complexes.

Les s�maphores sont utilisables dans le contr�le d'acc�s � une ressource : la valeur du s�maphore repr�sente le nombre de processus qui peuvent acc�der � la ressource; chaque acc�s d'un processus � la ressource d�cr�mente la valeur, qui sera incr�ment�e � nouveau lorsque la ressource sera lib�r�e. Dans le cas d'une ressource exclusive (i.e. un seul processus peut y acc�der) la valeur initiale du s�maphore sera 1.

Une t�che diff�rente peut �tre accomplie par un s�maphore, c'est le compteur de ressource : sa valeur est dans ce cas le nombre de ressources disponibles (par exemple le nombre de plages m�moire libres).

Examinons un cas r�el dans lequel nous utiliserons des types de s�maphore : imaginons un tampon dans lequel plusieurs processus S1,..., Sn peuvent �crire mais dans lequel un seul processus L peut lire; de plus, les op�rations ne peuvent �tre simultan�es (i.e. un seul processus agit sur le tampon � un instant donn�). Bien s�r les processus S peuvent toujours �crire, sauf quand le tampon est plein, alors que L ne peut lire que si le tampon n'est pas vide. Donc nous allons utiliser trois s�maphores : un premier pour l'acc�s � la ressource, un second et un troisi�me pour compter les �l�ments pr�sents dans le tampon (nous verrons plus tard pourquoi deux s�maphores ne suffisent pas).

Comme le tampon est une ressource d'acc�s exclusif le premier s�maphore peut �tre binaire (sa valeur sera 0 ou 1), alors que les deux autres auront des valeurs li�es � la taille du tampon.

Etudions comment mettre en oeuvre ces s�maphores en C gr�ce aux primitives de SysV. La fonction qui cr�e un s�maphore se nomme semget(2)

int semget(key_t key, int nsems, int semflg);

o� key est une cl� IPC, nsems le nombre de s�maphores que l'on d�sire cr�er et semflg est le contr�le d'acc�s repr�sent� sur 12 bits, les 3 premiers relatifs � la cr�ation et les 9 autres aux acc�s en lecture et �criture de l'utilisateur ('user'), du groupe ('group') et des autres ('other') (notez la similitude avec le syst�me de fichiers Unix); pour plus de pr�cisions lisez les pages de manuel d'ipc(5). Vous pouvez voir que SysV g�re des ensembles de s�maphores plut�t que des s�maphores isol�s, ce qui offre un code plus compact.

Cr�ons notre premier s�maphore

#include <stdio.h>
#include <stdlib.h>
#include <linux/types.h>
#include <linux/ipc.h>
#include <linux/sem.h>
   
int main(void)
{
  key_t key;
  int semid;

  key = ftok("/etc/fstab", getpid());

  /* create a semaphore set with only 1 semaphore: */
  semid = semget(key, 1, 0666 | IPC_CREAT);

  return 0;
}
Avant d'aller plus loin apprenons � g�rer et � supprimer des s�maphores; ceci se r�alise avec la primitive semctl(2)

int semctl(int semid, int semnum, int cmd, ...)

qui effectue l'action d�sign�e par cmd sur l'ensemble d�sign� par semid et (si l'action le requiert) sur le s�maphore particulier semnum. Nous introduirons des options si n�cessaire, toutefois une liste compl�te peut �tre obtenue sur la page de manuel. Selon l'action cmd il peut �tre requis de sp�cifier un autre argument, dont le type est :
union semun {
 int val;                  /* value for SETVAL */
 struct semid_ds *buf;     /* buffer for IPC_STAT, IPC_SET */
 unsigned short *array;    /* array for GETALL, SETALL */
                           /* Linux specific part: */
 struct seminfo *__buf;    /* buffer for IPC_INFO */
};
Pour d�finir la valeur d'un s�maphore la directive SETVAL est obligatoire et la valeur doit �tre sp�cifi�e dans la structure union semun; modifions le programme pr�c�dent en fixant la valeur du s�maphore � 1
[...]

  /* create a semaphore set with only 1 semaphore */
  semid = semget(key, 1, 0666 | IPC_CREAT);

  /* set value of semaphore number 0 to 1 */
  arg.val = 1;
  semctl(semid, 0, SETVAL, arg);

[...]
Lib�rons maintenant le s�maphore en d�saffectant les structures utilis�es pour sa gestion; cette t�che est accomplie par la directive IPC_RMID de semctl. Cette directive efface le s�maphore et envoie un message � tous les processus en attente d'acc�s � la ressource. Une derni�re modification du programme est :
[...]

   /* set value of semaphore number 0 to 1 */
  arg.val = 1;
  semctl(semid, 0, SETVAL, arg);

  /* deallocate semaphore */
  semctl(semid, 0, IPC_RMID);

[...]
Comme nous venons de le voir, cr�er et g�rer des structures pour contr�ler une ex�cution simultan�e n'est pas trop difficile; mais si nous introduisons la gestion d'erreurs, les choses vont se compliquer un peu, mais seulement du point de vue de la complexit� du code.

Le s�maphore peut maintenant �tre utilis� avec la fonction semop(2)

int semop(int semid, struct sembuf *sops, unsigned nsops);

o� semid identifie l'ensemble, sops est un tableau contenant les op�rations � effectuer et nsops le nombre de ces op�rations. Chaque op�ration est repr�sent�e par une structure sembuf :

unsigned short sem_num; short sem_op; short sem_flg;

par exemple, le num�ro du s�maphore dans l'ensemble (sem_num), l'op�ration (sem_op) et un drapeau d�finissant la politique d'attente; pour l'instant laissons ce sem_flg � 0. Les op�rations que nous pouvons sp�cifier sont des entiers et suivent les r�gles suivantes :
  1. sem_op < 0
    Si la valeur absolue du s�maphore est plus grande ou �gale � celle de sem_op l'op�ration est effectu�e et sem_op est ajout�e � la valeur du s�maphore (en fait elle est soustraite puisque sem_op est n�gatif). Si la valeur absolue de sem_op est inf�rieure � la valeur du s�maphore le processus se met en sommeil jusqu'� ce que la quantit� de ressources requise soit disponible.
  2. sem_op = 0
    Le processus est en sommeil jusqu'� ce que la valeur du s�maphore atteigne 0.
  3. sem_op > 0
    La valeur de sem_op est ajout�e � celle du s�maphore, lib�rant les ressources utilis�es jusqu'alors.
Le programme suivant va tenter de montrer comment utiliser les s�maphores pour mettre en oeuvre le pr�c�dent exemple de tampon : nous allons cr�er 5 processus nomm�s W (writers) et un processus nomm� R (reader). Chaque processus W essaie de prendre le contr�le de la ressource (le tampon) en la verrouillant gr�ce � un s�maphore, et, si le tampon n'est pas plein, d'y ajouter un �l�ment et de lib�rer la ressource. Le processus R essaie de verrouiller la ressource, d'y prendre un �l�ment si le tampon n'est pas vide et de lib�rer la ressource.

Ces lectures et �critures sont virtuelles : en effet, comme nous l'avons vu dans l'article pr�c�dent, chaque processus dispose de son propre espace m�moire et ne peut acc�der � celui d'un autre processus. Cela emp�che la gestion correcte du tampon par 5 processus, puisque chacun voit sa propre copie du tampon. Pour l'instant nous devons renoncer au v�ritable �change de donn�es jusqu'� ce que nous abordions la m�moire partag�e.

Pourquoi faut-il 3 s�maphores ? Le premier (num�ro 0) agit comme verrou d'acc�s au tampon et poss�de une valeur maximale de 1, alors que les deux autres g�rent les conditions de d�bordement par le haut (overflow) ou par le bas (underflow). Un seul s�maphore ne peut pas g�rer les deux situations, puisque semop n'agit que dans un sens.

Soyons encore plus clairs : avec un seul s�maphore (nomm� O), dont la valeur repr�sente le nombre d'emplacements libres dans le tampon. Chaque fois qu'un processus S �crit quelque chose dans le tampon il d�cr�mente de un la valeur du s�maphore, jusqu'� ce qu'elle atteigne z�ro, i.e. que le tampon soit plein. Ce s�maphore ne peut g�rer la condition d'underflow : le processus R peut augmenter sa valeur sans limite. Il nous faut donc un s�maphore sp�cial (nomm� U), dont la valeur repr�sente le nombre d'�l�ments dans le tampon. Chaque fois qu'un processus W �crit un �l�ment dans le tampon il incr�mente la valeur du s�maphore U et d�cr�mente celle du s�maphore O. A l'inverse, le processus R d�cr�mente la valeur du s�maphore U et incr�mente celle du s�maphore O.

La condition d'overflow est ainsi identifi�e par l'impossibilit� de d�cr�menter le s�maphore O et la condition d'underflow par l'impossibilit� de d�cr�menter le s�maphore U.

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <linux/types.h>
#include <linux/ipc.h>
#include <linux/sem.h>

int main(int argc, char *argv[])
{
  /* IPC */
  pid_t pid;
  key_t key;
  int semid;
  union semun arg;
  struct sembuf lock_res = {0, -1, 0};
  struct sembuf rel_res = {0, 1, 0};
  struct sembuf push[2] = {1, -1, IPC_NOWAIT, 2, 1, IPC_NOWAIT};
  struct sembuf pop[2] = {1, 1, IPC_NOWAIT, 2, -1, IPC_NOWAIT};

  /* Other */
  int i;
  
  if(argc < 2){
    printf("Usage : bufdemo [dimension]\n");
    exit(0);
  }
  
  /* Semaphores */
  key = ftok("/etc/fstab", getpid());

  /* Create a semaphore set with 3 semaphore  */
  semid = semget(key, 3, 0666 | IPC_CREAT);

  /* Initialize semaphore #0 to 1 - Resource controller */
  arg.val = 1;
  semctl(semid, 0, SETVAL, arg);

  /* Initialize semaphore #1 to buf_length - Overflow controller */
  /* Sem value is 'free space in buffer' */
  arg.val = atol(argv[1]);
  semctl(semid, 1, SETVAL, arg);

  /* Initialize semaphore #2 to buf_length - Underflow controller */
  /* Sem value is 'elements in buffer' */
  arg.val = 0;
  semctl(semid, 2, SETVAL, arg);

  /* Fork */
  for (i = 0; i < 5; i++){
    pid = fork();
    if (!pid){
      for (i = 0; i < 20; i++){
	sleep(rand()%6);
	/* Try to lock resource - sem #0 */
	if (semop(semid, &lock_res, 1) == -1){
	  perror("semop:lock_res");
	}
	/* Lock a free space - sem #1 / Put an element - sem #2*/
	if (semop(semid, &push, 2) != -1){
	  printf("---> Process:%d\n", getpid());
	}
	else{
	  printf("---> Process:%d  BUFFER FULL\n", getpid());
	}
	/* Release resource */
	semop(semid, &rel_res, 1);
      }
      exit(0);
    }
  }
    
  for (i = 0;i < 100; i++){
    sleep(rand()%3);
    /* Try to lock resource - sem #0 */
    if (semop(semid, &lock_res, 1) == -1){
      perror("semop:lock_res");
    }
    /* Unlock a free space - sem #1 / Get an element - sem #2 */
    if (semop(semid, &pop, 2) != -1){
      printf("<--- Process:%d\n", getpid());
    }
    else printf("<--- Process:%d  BUFFER EMPTY\n", getpid());
    /* Release resource */
    semop(semid, &rel_res, 1);
  }
  
  /* Destroy semaphores */
  semctl(semid, 0, IPC_RMID);

  return 0;
}
Commentons les parties les plus int�ressantes de ce code:
struct sembuf lock_res = {0, -1, 0};
struct sembuf rel_res = {0, 1, 0};
struct sembuf push[2] = {1, -1, IPC_NOWAIT, 2, 1, IPC_NOWAIT};
struct sembuf pop[2] = {1, 1, IPC_NOWAIT, 2, -1, IPC_NOWAIT};
Ces 4 lignes d�signent les actions possibles sur notre ensemble de s�maphores : les deux premi�res sont des actions simples, les autres sont doubles. La premi�re action, lock_res, essaie de verrouiller la ressource : elle d�cr�mente de 1 la valeur du premier s�maphore (num�ro 0), si cette valeur n'est pas z�ro, ou le processus reste en attente (none) si la ressource est d�j� occup�e. L'action rel_res est identique � l'action lock_res mais la ressource est lib�r�e (valeur positive).

Les actions push et pop sont plus particuli�res. Ce sont des tableaux de 2 actions, la premi�re sur le s�maphore num�ro 1 et la seconde sur le s�maphore num�ro 2; lorsque la valeur du premier est incr�ment�e la valeur du second est d�cr�ment�e et vice versa, mais le comportement n'est plus l'attente : IPC_NOWAIT force le processus � continuer son ex�cution si la ressource est occup�e.

/* Initialize semaphore #0 to 1 - Resource controller */
arg.val = 1;
semctl(semid, 0, SETVAL, arg);

/* Initialize semaphore #1 to buf_length - Overflow controller */
/* Sem value is 'free space in buffer' */
arg.val = atol(argv[1]);
semctl(semid, 1, SETVAL, arg);

/* Initialize semaphore #2 to buf_length - Underflow controller */
/* Sem value is 'elements in buffer' */
arg.val = 0;
semctl(semid, 2, SETVAL, arg);
Nous initialisons ici la valeur des s�maphores : le premier � 1 puisqu'il contr�le l'acc�s � une ressource exclusive, le second � la longueur du tampon (indiqu�e sur la ligne de commande) et le troisi�me � 0, comme expliqu� plus haut pour l'overflow et l'underflow.
/* Try to lock resource - sem #0 */
if (semop(semid, &lock_res, 1) == -1){
  perror("semop:lock_res");
}
/* Lock a free space - sem #1 / Put an element - sem #2*/
if (semop(semid, &push, 2) != -1){
  printf("---> Process:%d\n", getpid());
}
else{
  printf("---> Process:%d  BUFFER FULL\n", getpid());
}
/* Release resource */
semop(semid, &rel_res, 1);
Le processus W essaie de verrouiller la ressource gr�ce � l'action lock_res ; ensuite il ex�cute un push et le signale sur la sortie standard : si l'op�ration ne peut s'effectuer il informe que le tampon est plein. Enfin il lib�re la ressource.
/* Try to lock resource - sem #0 */
if (semop(semid, &lock_res, 1) == -1){
  perror("semop:lock_res");
}
/* Unlock a free space - sem #1 / Get an element - sem #2 */
if (semop(semid, &pop, 2) != -1){
  printf("<--- Process:%d\n", getpid());
}
else printf("<--- Process:%d  BUFFER EMPTY\n", getpid());
/* Release resource */
semop(semid, &rel_res, 1);
Le processus R agit plus ou moins a l'identique du processus W : il verrouille la ressource, effectue un pop et lib�re la ressource.

Nous parlerons dans le prochain article des files d'attente de messages, qui constituent une autre structure de l'IPC (InterProcess Communication) et de la synchronisation. Comme d'habitude si vous �crivez quelque chose de simple � partir de ce que vous avez appris ici, envoyez-le moi, avec votre nom et votre adresse mail, je serai heureux de le lire. Bon travail !

Lectures recommand�es