Lors de la création de programme, il faut se poser plusieurs questions quant à l’accès aux ressources de ce programme. Prenons en exemple, deux processus accédant tout deux à un tableau en mémoire partagée, ou à un fichier en écriture …
Cela peut poser de gros problèmes dans les cas suivants :
- Si le processus 1 lit la ressource au moment ou le processus 2 la modifie
- Si les deux processus modifient la ressource en même temps
On appelle dans un code source, l’accès à une ressource critique, la section critique.
Cet cet endroit délicat, auquel il faut faire attention, et il existe des moyens de réserver ces ressources ou d’endormir les processus pour ériger une synchronisation d’écriture/lecture.
Sous UNIX, il existe différents mécanismes pour protéger une ressource critique :
Les sémaphores, les sémaphores d’exclusion mutuelle (mutex), ou les verrous. Je ne verrais pas les mécanismes sous Windows ici, bien qu’ils existent aussi.
Les verrous
Nous allons créer un processus écrivain qui va écrire des caractères dans un fichier.
ecrivain.c
#include <stdio.h> #include <fcntl.h> #include <errno.h> #include <string.h> #include <unistd.h> #include <stdlib.h> #define TEMPS_MAX 5 int main( int nb_arg , char * tab_arg[] ) { struct flock ptr_latch; ptr_latch.l_whence = SEEK_SET; // depuis le debut du fichier ptr_latch.l_start = 0; // pas d'offset ptr_latch.l_len = 0; // jusqu'a la fin du fichier ptr_latch.l_pid = getpid(); // pid du processus bloquant int d_fich; /* Descripteur du fichier partage */ char ref_fichier[256] ; /* Reference du fichier partage */ char lettre = 'A'-1 ; int nb_ecritures = 0 ; int i ; char mess[256] ; /*----------*/ if( nb_arg != 3) { fprintf( stderr , "Usage : %s <reference fichier> <nb ecritures>\n" , tab_arg[0]); exit(-1); } strcpy(ref_fichier,tab_arg[1]); // creation du verrou /* if( (latch = creat(ref_fichier, 0666))== -1 ) { printf("Erreur creat \n"); exit(-1); }*/ sscanf(tab_arg[2] , "%d" , &nb_ecritures); // 2eme argument= nb ecriture /* Ouverture du fichier partage */ if( (d_fich = open( ref_fichier , O_RDWR|O_CREAT , 0666 )) == -1 ) { sprintf( mess, "Pb open sur %s\n", ref_fichier); perror(mess); exit(-1); } /* Initialisation des nombre aleatoires */ srandom(getpid()); for(i=0 ; i<nb_ecritures ; i++ ) { /* * Acces au fichier partage */ // verrou ptr_latch.l_type = F_WRLCK; // verrou en ecriture dans ltype if(fcntl(d_fich, F_SETLK, &ptr_latch)==-1) { perror("Fcntl error \n"); } else { printf("Verrou posé \n"); } /* lecture du dernier caractere */ lseek( d_fich , 0 , SEEK_SET ); while( read( d_fich , &lettre , sizeof(char) ) ) ; /* Ajout d'un autre caractere */ lettre = lettre+1 ; write( d_fich , &lettre , sizeof(char) ) ; printf( "Ecrivain %d --> ecriture de %c \n" , getpid() , lettre ) ; //verrou unlock ptr_latch.l_type = F_UNLCK; // on definit ltype a deverouillage if(fcntl(d_fich, F_SETLK, &ptr_latch)==-1) { perror("Fcntl error \n"); } else { printf("Verrou levé \n"); } /* Pause */ sleep(random()%TEMPS_MAX) ; } printf("L'ecrivain %d a fini d'écrire \n", getpid()); /* fermeture du fichier partage */ close(d_fich); exit(0); }
Ce code va permettre d’écrire dans un fichier « file » un nombre donné de caractère « n », on l’exécute de la manière suivante :
./ecrivain <file> <n> , ou file est le nom du fichier et n le nombre d’écriture désiré.
J’ai essayé de commenter un maximum le code pour laisser le moins d’incompréhension possible aux personnes qui ne sont pas familiarisées au C.
On retiendra ici principalement la fonction fcntl() qui permet de verrouiller ou déverrouiller l’accès à une ressource en fonction de la structure qu’elle prend en dernier argument. Elle fonctionne de cette façon :
- Premier argument, descripteur de fichier (la ressource à verrouiller/déverrouiller)
- Deuz’, la constante qui dit que l’on traite avec F_SETLK (càd que l’on veut agir sur le verrou)
- Troisième argument, l’adresse de la structure qui définit certaines options (ici struct flock ptr_latch)
Si on repère dans mon code, les deux endroits ou je verrouille et déverrouille la ressource, on peut voir que entre ces deux actions, se trouve la section critique de mon processus. Bien, c’est bien beau de pouvoir écrire dans un fichier, mais essayons maintenant d’y lire le contenu.
Je vais créer un processus lecteur, qui va au rythme effréné d’une boucle infinie, lire simplement et afficher les caractères contenus dans le fichier texte.
lecteur.c
#include <stdio.h> #include <fcntl.h> #include <errno.h> #include <string.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #define TEMPS_MAX 2 int main( int nb_arg , char * tab_arg[] ) { struct flock ptr_latch; ptr_latch.l_whence = SEEK_SET; // depuis le debut du fichier ptr_latch.l_start = 0; // pas d'offset ptr_latch.l_len = 0; // jusqu'a la fin du fichier ptr_latch.l_pid = getpid(); // pid du processus bloquant int d_fich ; /* Descripteur du fichier partage */ char ref_fichier[256]; /* Reference du fichier partage */ char mess[256] ; char lettre ; time_t prec_time = (time_t)0 ; struct stat inoeud ; /*----------*/ if( nb_arg != 2) { fprintf( stderr , "Usage : %s <reference fichier>\n" , tab_arg[0]); exit(-1); } strcpy(ref_fichier,tab_arg[1]); /* Ouverture du fichier partage */ if( (d_fich = open( ref_fichier , O_RDONLY , 0666 )) == -1 ) { sprintf( mess , "Lecteur %d : Pb open sur %s\n", getpid() , ref_fichier ); perror(mess); exit(-1); } while(1) { /* Infos du fichier memoire */ if( stat(ref_fichier, &inoeud) == -1 ) { sprintf( mess , "Lecteur %d : pb stat sur fichier %s" , getpid() , ref_fichier); perror(mess); exit(-2); } ptr_latch.l_type = F_RDLCK; // verrou en ecriture dans ltype if(fcntl(d_fich, F_SETLK, &ptr_latch)==-1) { perror("Verrou utilisé ! \n"); } else { printf("Verrou posé \n"); } /* Visualisation du fichier */ if( difftime(inoeud.st_mtime , prec_time) ) { lseek( d_fich , 0 , SEEK_SET ); /* * Acces au fichier partage */ printf("Lecteur %d : " , getpid() ); /* Lecture */ while( read( d_fich , &lettre , sizeof(char) ) ) { printf( "| %c " , lettre ); } printf("|\n"); //verrou unlock ptr_latch.l_type = F_UNLCK; // on definit ltype a deverouillage if(fcntl(d_fich, F_SETLK, &ptr_latch)==-1) { perror("Verrou utilisé \n"); } else { printf("Verrou levé \n"); } } prec_time = inoeud.st_mtime ; } /* fermeture du fichier partage */ close(d_fich); exit(0); }
Toujours dans le même esprit, on verrouille la ressource lorsqu’on la lit, pour éviter que le processus écrivain insère des valeurs pendant que l’on lit.
Note : Il faut noter que dans mes deux fichiers sources, je change l’attribut l_type de la structure ptr_latch juste avant de verrouiller ou de déverrouiller mon descripteur de fichier, de F_RDLCK à F_UNLCK (verrou en lecture, déverrouillage).
Le lecteur, se lance ainsi : ./lecteur <file> ou file est le nom du fichier à lire.
Maintenant que l’on possède nos deux processus, essayons de lancer en même temps, 2 écrivains et 1 lecteur dans deux consoles différentes.
Dans une console par exemple, on va faire : ./ecrivain F 10 & ./ecrivain F 15
et dans l’autre : ./lecteur F
Normalement, si vos verrous sont bien agencés, chaque processus va réserver puis libérer ses ressources, et le fichier ne va être donc manipulé que par 1 processus à la fois.