TD-corr PDF
Document Details
Uploaded by SpellboundSugilite3988
ESIEA
2024
ESIEE
Xavier Hilaire
Tags
Summary
This document is an exam paper from ESIEE, covering programming system concepts. The paper contains questions related to processes, wait, file operations, and inter-process communication (IPC).
Full Transcript
Programmation système Travaux dirigés Xavier Hilaire [email protected] 18 novembre 2024 1 Processus Exercice 1.1 – Hiérarc...
Programmation système Travaux dirigés Xavier Hilaire [email protected] 18 novembre 2024 1 Processus Exercice 1.1 – Hiérarchie Dessiner la hiérarchie (en chronogramme, et en arbre généalogique) de processus créés par chacun des extraits de code ci-dessous : 1. fork(); fork(); Sol. : La solution n’est pas unique, car on ne peut prédire lequel du père ou du premier fils créé sera le plus diligent pour faire le 2ème appel fork(). 29 29 OU BIEN: 30 31 30 32 32 31 29 29 30 30 31 OU BIEN: 31 32 32 2. if (fork() > 0) fork(); Sol. : 29 30 29 31 30 31 1 3. int i; for (i= 0; i < 3; i++) fork(); Sol. : On ne donnera que l’arbre généalogique : la combinatoire fait exploser ici le nombre de chronogrammes possibles. Les PID n’étant pas significatifs, on préférera leur substituer la valeur de i que trouve chaque processus immédiatement après sa création. i=0 i=1 i=2 i=1 i=2 i=2 i=2 4. int i; for (i= 0; i < 3; i++) if (fork() == 0) break; Sol. : 29 30 29 31 30 31 32 32 5. int i; for (i= 0 ; i < 3 ; i++) if (fork() > 0) break; Sol. : 29 29 30 30 31 31 32 32 Exercice 1.2 – Wait Ecrire un programme qui réalise les opérations suivantes : — un processus père créé N fils (N défini par #define) — chacun des fils calcule et stocke dans une variable t le carré de son pid — puis chacun des fils appelle usleep(t), qui a pour effet de forcer son sommeil pendant t microsecondes, et sort finalement avec le code de sortie t␣%␣256 2 — pendant ce temps-là, le père attend la mort de tous ses fils, puis affiche la somme des codes d’erreurs qu’il a reçus d’eux, et termine. Sol. : #include #include < s t d l i b. h> #include #include #include #define N 5 int main ( ) { int i , s t a t u s , somme= 0 ; /∗ C r e a t i o n d e s N f i l s => c o p i e r −c o l l e r p a r f a i t du code p r o d u i t a l ’ e x e r c i c e p r e c e d e n t du TD ∗/ fo r ( i= 0 ; i < N; i ++) i f ( f o r k ( ) == 0 ) break ; /∗ Les N+1 p r o c e s s u s a t t e i g n e n t ∗ t o u s ∗ c e t t e l i g n e , mais l e p e r e p e u t e t r e d i s t i n g u é d e s f i l s par deux moyens : − i l e s t l e s e u l pour q u i p > 0 − i l e s t é g a l e m e n t l e s e u l pour q u i i == N, c a r i l e s t l e s e u l a a v o i r pu a l l e r j u s q u ’ au b o u t de l a b o u c l e f o r ∗/ i f ( i < N) /∗ code d e s f i l s ∗/ { int t= g e t p i d ( ) ∗ ( int ) g e t p i d ( ) ; usleep ( t ) ; e x i t ( t %256); } /∗ code du p e r e s e u l ∗/ while ( w a i t (& s t a t u s ) > 0 ) somme += ( s t a t u s >> 8 ) & 2 5 5 ; p r i n t f ( "Somme␣=␣%d\n" , somme ) ; return 0 ; } Le programme laisse t’il des processus zombies derrière lui ? Justifier. Sol. : Il ne peut pas y avoir de processus zombie après la fin du main(), car le père se jette immédiatement dans la boucle while, où il récupère tous les codes de sortie des ses fils. Pendant ce temps, les fils temporisent du fait de l’appel à usleep(). Remarquer que la boucle while n’est toutefois nécessaire que pour faire la sommation : on aurait pu la supprimer sans créer de zombies pour autant. 3 Exercice 1.3 – Zombie Soit le code suivant : int main() { if (fork() == 0) { sleep(20); printf("PID=%d, PPID=%d\n", getpid(), getppid()); } return 0; } Donner des valeurs plausibles affichées. Le programme créé t’il un ou des processus zombies à un moment donné ? Que se passe si sleep(20) est déplacé en alternative (i.e. devient le code de else) du test if ? Avec le sleep() tel qu’il est écrit : PID=30, PPID=1. L’appel à sleep retarde considérable- ment l’exécution du fils. Pendant ce temps, le père s’est tué, car il n’a rien d’autre à exécuter que return 0. Le fils devient alors orphelin, et se fait adopter par init, de PID 1. D’où le 1 affiché lors du printf(). Il n’y a pas de processus zombie, mais un processus orphelin dans ce cas, ce qui n’est pas du tout la même chose. Avec le sleep() déplacé en else du if, cela donne int main() { if (fork() == 0) printf("PID=%d, PPID=%d\n", getpid(), getppid()); else sleep(20); return 0; } Et change tout : le fils se tue immédiatement après avoir fait son printf(), mais son père tarde à venir récupérer son code de sortie, car il est suspendu par le sleep(20). Deux conséquences : 1) le fils n’est jamais orphelin, puisque son père est vivant lorsqu’il se tue ; 2) pendant le sommeil du père (20 s), et seulement pendant ce temps-là, le fils est à l’état zombie. Tout rentre dans l’ordre lorsque le père termine, puisque le fils devient orphelin et se fait adopter par init, qui récupère son code. Le code de sortie du père est normalement récupéré par le shell (ou le processus qui l’a lancé). Affichage possible : PID=30, PPID=29. Exercice 1.4 – Arbre binaire et wait Ecrire le code C qui créé une hiérarchie d’arbre binaire sur h niveaux. Après avoir éventuellement créé ses fils, chacun des processus (y compris le processus racine) doit appeler une fonction jetravaille(), puis attendre la mort de tous ses enfants avant d’afficher « j’ai fini » à l’écran. Il ne doit pas rester de processus zombie. Sol. : 4 #include #include #include #include #define H 2 void j e t r a v a i l l e ( ) /∗ pas demandee , l i v r e e a t i t r e d ’ exemple s e u l e m e n t ∗/ { p r i n t f ( "PDI␣%d␣a ␣ pour ␣ p e r e ␣%d , ␣ e s t ␣ dans ␣ j e t r a v a i l l e ( ) \ n" , getpid () , getppid ( ) ) ; } int main ( ) { int i ; fo r ( i= 0 ; i < H; i ++) i f ( f o r k ( ) > 0) i f ( f o r k ( ) > 0) /∗ l e p e r e e s t l e s e u l a p o u v o i r r e u s s i r l e s deux c o n d i t i o n s ∗/ break ; jetravaille (); /∗ l ’ a p p e l a n t a t t e n d l a mort de t o u s s e s e n f a n t s ∗/ while ( w a i t (NULL) > 0 ) {} return 0 ; } Il ne reste pas de processus zombie dans cet exemple, car la fonction jetravaille() ne bloque pas. Par contre, si elle bloquait pour un processus donné mais pas pour l’un de ses fils, ce dernier deviendrait zombie. L’absence de zombie est garantie ici non pas par la boucle while (en l’absence de boucle, chaque processus se serait tué directement, mais les fils orphelins auraient été adoptés par le PID 1 (init)), mais par le caractère non bloquant de jetravaille(). Exercice 1.5 – Exec Dire ce qu’affichent les extraits de code suivants, et le code de retour renvoyé par chaque processus. On supposera que /bin/test et /bin/ls sont des exécutables valides ; et qu’au contraire, /bin/berk n’est pas valide. 1. if (execl("/bin/test", "/bin/test", "-d", "/tmp", NULL)) execl("/bin/ls", "/bin/ls", "/tmp"); exit(2); Sol. : Le 1er appel à execl() réussit car /bin/test est un exécutable valide. Donc, ni le if, ni le 2ème appel à execl(), ni le exit() ne seront jamais exécutés. Affichage : rien, car /bin/test n’affiche rien. Code de sortie : celui de /bin/test -d /tmp, ç-à-d 0 car /tmp existe fort probablement. 5 2. int i; if ((i=execlp("berk", "ls", "-l", "/tmp", 0))) printf("ls a réussi, code de sortie%d\n", i); exit(i); Sol. : L’appel à execlp() va échouer ici, car berk n’est pas un exécutable valide. Conséquence : execlp() va retourner -1 car erreur, donc i=-1, le test du if passe, et printf est exécuté. Affichage : ls a réussi, code de sortie -1. Code de sortie : 255, car les bits 0 à 7 sont tous positionnés dans la représentation binaire de -1, mais que seuls les 8 premiers bits sont significatifs. 3. int i; if (!(i=execlp("ls", "berk", "-l", "/ter65rRZ", 0))) printf("ls a échoué, code de sortie%d\n", i); exit(i); Sol. : L’appel à execlp() réussit ici, puisque ls fait en réalité référence à /bin/ls, mais qu’il s’agit d’une fonction execlp() et non execl(). Le if, le printf(), et le exit() ne peuvent donc jamais exécutés. Affichage : celui de /bin/ls -l /ter65rRZ. Code de sortie : celui de /bin/ls -l /ter65rRZ. 4. execlp("/bin/berk", "berk", NULL); execl("/bin/ls", "/bin/ls", NULL); printf("Je continue\n"); Sol. : Le 1er execlp() échoue et renvoie -1, et ne sert à rien. L’appel à exel() suivant réussit, et a pour effet de lancer /bin/ls sans arguments, qui va lister le répertoire courant. Affichage : le contenu du répertoire courant. Code de sortie : 0, car à moins que le répertoire courant n’existe plus, ls va sortir sans erreur. 5. execl("/bin/berk", NULL); if (fork() == 0 && execlp("ls", "ls", "/tmp", 0)) printf("/bin/ls a échoué"); else execlp("test", "-d", "/home", 0); Sol. : Rappel : en langage C, le && est la version court-circuitée de l’opérateur ET. Ce qui veut dire que dans l’expression f() && g(), g() ne sera jamais appelée si f() a renvoyé 0. Il ne revient pas au même d’écrire f() && g() et f() & g(), l’opérateur & garantissant au contraire l’évaluation complète de l’expression, donc l’appel à g() quoi qu’il arrive. Retour à notre exemple : fork() est d’abord appelée, et à son retour, seul le fils passe la condition fork() == 0, et exécute le execlp() comme deuxième partie de la condition. Le père, qui ne satisfait pas cette condition, n’évalue pas le reste de l’expression. Il se jette dans le else, et exécute le execlp() sur /bin/test, qui réussit. Affichage : celui de ls (et du fils), car /bin/test est silencieux. Codes de sortie : pour le fils, 0 car /tmp existe, donc ls réussit ; pour le père, celui du code de sortie de test -d /home. 2 Fichiers Exercice 2.1 – Pagaille Pour chacune des fonctions f1() à f3() ci-après : 1. Décrire ce que fait la fonction en 1-2 phrases (un dessin peut être utile) Sol. : — Pour f1() : créé un fichier /tmp/toto avec droits en lecture+écriture pour l’utili- sateur, ou remet à zéro ce même fichier s’il existe déjà. Puis créé un fils, qui va 6 chercher à écrire “yyy” deux fois de suite dans le fichier, alors que le père cherchera à écrire “xxx” deux fois de suite. Ecritures concurrentes. — Pour f2() : créé un fils. Père et fils ouvrent alors /tmp/toto chacun de leur côté. Le fils écrit “abcdef” dans ce fichier, tandis que le père écrit “123456” dedans. — Pour f3() : créé un fils, qui va créer ou tronquer un fichier /tmp/toto, et y écrire “abcdef” avant de se tuer. De son côté, le père tente d’ouvrir le fichier et de le lire. Il n’y a aucune synchronisation entre les deux processus. 2. Dire quelle est la longueur du fichier /tmp/toto après exécution (pour f2() on sup- posera que le fichier existe initialement et est vide) Sol. : — Pour f1() : 12 octets — Pour f2() : 6 octets — Pour f3() : 6 octets 3. Quels sont les contenus possibles de /tmp/toto ? Si le contenu est unique, dire pour- quoi, et le donner. Sinon, lister tous les contenus possibles. Sol. : — Pour f1() : 6 affichages possibles :xxxxxxyyyyyy OU xxxyyyxxxyyy OU xxxyyyyyyxxx OU yyyxxxyyyxxx OU yyyxxxxxxyyy OU yyyyyyxxxxxx — Pour f2() : en toute théorie, 2 affichages possibles : abcdef OU 123456. Mais le sleep(1) fait que abcdef sera rarissime en pratique. — Pour f3() : l’affichage sera Fils a ecrit suivi de : — soit abcdef, lui-même suivi de caractères indéfinis (jusqu’à 512-6 = 506) — soit jusqu’à 512 caractères tous indéfinis. Explications : le processus père lit ce que le processus fils est supposé avoir écrit. Sauf qu’il n’y a aucune synchronisation entre les deux (un appel à wait dans le père, avant open, aurait été bienvenu), et que par conséquent : — soit le père ouvre le fichier avant que le fils n’ai terminé d’écrire ⇒ le read ne lira rien, on affichera donc la mémoire de msg, qui est indéfinie — pire encore, le père tente d’ouvrir le fichier avant même qu’il n’existe ⇒ le open échoue, donc le read aussi, mais le résultat reste le même que précedemment — soit le père ouvre le fichier après que le fils ait fini d’écrire ⇒ il relira la chaîne, mais comme le fils n’a transmis que 6 caractères et non 7, le 0 terminal de chaîne n’est pas transmis, donc on affiche aussi les caractères restants de msg. Listing 1 – Fonction f1() void f 1 ( ) { int f= open ( " /tmp/ t o t o " , O_RDWR | O_CREAT | O_TRUNC, 0 6 4 4 ) , i ; i f ( f o r k ( ) > 0) { fo r ( i= 1 ; i #include #include #include #define MAXMEM 9000000 char mem[MAXMEM] ; int c a p t u r e f i n d ( char ∗ r e p e r t o i r e , void ∗ buf ) { int f , l u= −1; i f ( f o r k ( ) == 0 ) /∗ code du f i l s ∗/ { f= open ( " /tmp/ s o r t i e " , O_WRONLY | O_CREAT | O_TRUNC, 0 6 4 4 ) ; dup2 ( f , 1 ) ; close ( f ); e x e c l p ( " f i n d " , " f i n d " , r e p e r t o i r e , "−type " , "d" , NULL ) ; e x i t ( 2 5 5 ) ; /∗ normalement j a m a i s a t t e i n t ∗/ } e l s e /∗ code du p e r e ∗/ { char ∗p= buf ; w a i t (NULL ) ; /∗ a t t e n d r e que l e f i l s a i t t e r m i n e ∗/ /∗ r o u v r i r l e f i c h i e r e t l e l i r e ∗/ f= open ( " /tmp/ s o r t i e " , O_RDONLY) ; while ( ( l u= r e a d ( f , p , MAXMEM) ) > 0 ) p += l u ; close ( f ); 9 l u= p−(char ∗ ) buf ; /∗ t o t a l d ’ o c t e t s e f f e c t i v e m e n t t r a n s f e r e s ∗/ } return l u ; } int main ( int argc , char ∗ argv [ ] ) { int i ; a s s e r t ( a r g c == 2 ) ; i= c a p t u r e f i n d ( argv [ 1 ] , mem) ; mem[ i ]= 0 ; p r i n t f ( " c a p t u r e f i n d ( ’% s ’ ) ␣a ␣ r e n v o y e : ␣%s \n" , argv [ 1 ] , mem) ; return 0 ; } Une autre solution consiste à ne faire qu’un seul appel à open() avant de forker, en demandant l’ouverture en O_RDWR (le fils écrit, le père lit). Auquel cas, il y a une précaution à prendre : lorsque le père s’apprête à relire le fichier, il doit repositionner le pointeur à son début, sinon read() renverra zéro d’emblée, puisque le pointeur est en fin de fichier après que le fils a écrit dessus. Ce qui donne la version ci-après. #include < s t d l i b. h> #include #include #include #include < f c n t l. h> #include #include #include #define MAXMEM 9000000 char mem[MAXMEM] ; int c a p t u r e f i n d ( char ∗ r e p e r t o i r e , void ∗ buf ) { int f , l u= −1; f= open ( " /tmp/ s o r t i e " , O_RDWR | O_CREAT | O_TRUNC, 0 6 4 4 ) ; i f ( f o r k ( ) == 0 ) /∗ code du f i l s ∗/ { dup2 ( f , 1 ) ; close ( f ); e x e c l p ( " f i n d " , " f i n d " , r e p e r t o i r e , "−type " , "d" , NULL ) ; e x i t ( 2 5 5 ) ; /∗ normalement j a m a i s a t t e i n t ∗/ } e l s e /∗ code du p e r e ∗/ 10 { char ∗p= buf ; w a i t (NULL ) ; /∗ a t t e n d r e que l e f i l s a i t t e r m i n e ∗/ /∗ r e p l a c e r l e p o i n t e u r de f i c h i e r au d e b u t du f i c h i e r ∗/ l s e e k ( f , 0 , SEEK_SET ) ; while ( ( l u= r e a d ( f , p , MAXMEM) ) > 0 ) p += l u ; close ( f ); l u= p−(char ∗ ) buf ; /∗ t o t a l d ’ o c t e t s e f f e c t i v e m e n t t r a n s f e r e s ∗/ } return l u ; } int main ( int argc , char ∗ argv [ ] ) { int i ; a s s e r t ( a r g c == 2 ) ; i= c a p t u r e f i n d ( argv [ 1 ] , mem) ; mem[ i ]= 0 ; p r i n t f ( " c a p t u r e f i n d ( ’% s ’ ) ␣a ␣ r e n v o y e : ␣%s \n" , argv [ 1 ] , mem) ; return 0 ; } Exercice 2.3 – Inversion Ecrire un programme inversion.c tel que inversion fichier inverse le fichier passé en paramètre : le 1er octet est échangé avec le dernier, le 2ème avec l’avant-dernier, etc. On pourra se contenter de procéder octet par octet. Sol. : #include < s t d l i b. h> #include #include #include < f c n t l. h> #include int main ( int argc , char ∗ argv [ ] ) { int f= open ( argv [ 1 ] , O_RDWR) , p , q , p t r= 0 ; char debut , f i n ; a s s e r t ( f >0); do 11 { /∗ l i r e l ’ o c t e t s u i v a n t en d e b u t de f i c h i e r e t son " m i r r o i r " en f i n de f i c h i e r ∗/ p= l s e e k ( f , ptr , SEEK_SET ) ; r e a d ( f , &debut , 1 ) ; q= l s e e k ( f , −1−ptr , SEEK_END) ; r e a d ( f , &f i n , 1 ) ; /∗ permuter l e s deux ∗/ l s e e k ( f , ptr , SEEK_SET ) ; w r i t e ( f , &f i n , 1 ) ; l s e e k ( f , −1−ptr , SEEK_END) ; w r i t e ( f , &debut , 1 ) ; p++, q−−; p t r ++; } while ( p < q ) ; return 0 ; } 3 Communication inter-processus (IPC) Exercice 3.1 – Tubes Soit le programme reproduit en listing 4. 1. Que fait ce programme (un dessin peut être utile) ? Sol. : Il créé un tube, puis 3 fils dans la boucle for, qui héritent tous des deux descripteurs alloués par le père lors de la création du tube. Avec une probabilité a priori de 1/2 (en supposant les PID des fils imprévisibles et équiprobables), chaque fils écrit 3 caractères identiques dans le tube (aaa pour le 1er, bbb pour le 2ème, ccc pour le 3ème). Dans le même temps, le père relit le tube caractère par caractère, et affiche ce qu’il a lu. La situation est résumée sur la figure suivante : 12 2. Combien de processus ce programme créé t’il ? Sol. : Trois. 3. Qu’affiche ce programme ? Si l’affichage est unique, expliquer pourquoi, et le donner. Sinon, donner tous les affichages possibles. Sol. : Il peut afficher 0, 3, 6, ou 9 caractères selon que chacun des fils aura bien voulu écrire ou non. L’ordre dans lequel ils le font est imprévisible, mais la longueur des chaînes étant de 3 caractères, et 3 < 512, les chaînes transmises ne peuvent être coupées. Affichages possibles : rien OU aaa OU bbb OU ccc OU aaabbb OU aaaccc OU bbbaaa OU bbbccc OU cccaaa OU cccbbb OU aaabbbccc OU aaacccbbb OU bbbaaaccc OU bbbcccaaa OU cccaaabbb OU cccbbbaaa 4. Ce programme termine t’il ? Sol. : Non. En effet, le père n’a pas fermé tube = le côté du tube qu’il n’utilise pas. Donc le compteur de références reste à 1 même lorsque tous les fils sont morts. Il s’ensuit que l’appel à read(), après que le tube a été vidé de ses données, est bloquant : le père s’est auto-bloqué, et la boucle ne termine jamais. 5. Créé t’il des processus zombies ? Si vous pensez que non, expliquer pourquoi. Sinon, donner les modifications à apporter qu’il n’en créé pas. Sol. : Oui, parce qu’il ne termine pas. Les fils meurent tous, car rien ne les empêche d’atteindre l’appel à exit(), mais leur père est bloqué sur la lecture du tube, et il n’a fait aucun appel à wait() auparavant. Donc les codes de sortie des fils ne sont jamais récupérés, ce qui fait d’eux des zombies par définition. Pour éviter cela, il suffit d’éviter que le père bloque : on ajoutera un close(tube) juste avant le while, qui résoudra aussi bien le problème du blocage vu à la question précédente, que des zombies. Ainsi, lorsque tous les fils seront morts, le compteur de références de l’entrée du tube passera à 0, donc read() sortira avec la valeur 0. La boucle while, terminera, et le père se tuera, entraînant l’adoption des fils orphelins par init, qui récupérera leur code de sortie. Listing 4 – Tubes #include #include < s t d l i b. h> #include 13 int main ( ) { char tampon [ 1 0 ] , t ; int tube [ 2 ] , i ; p i p e ( tube ) ; fo r ( i =0; i < 3 ; i ++) i f ( f o r k ( ) == 0 ) { char c= ’ a ’+i ; s p r i n t f ( tampon , "%c%c%c " , c , c , c ) ; i f ( g e t p i d ( ) % 2 == 0 ) w r i t e ( tube [ 1 ] , tampon , 3 ) ; exit (0); } while ( ( i= r e a d ( tube [ 0 ] , &t , 1 ) ) > 0 ) w r i t e ( 1 , &t , 1 ) ; p r i n t f ( " \n" ) ; return 0 ; } Exercice 3.2 – Tubes (encore) Soit la commande shell ls -la | wc -l Ecrire un programme réalisant son équivalent en langage C. Votre programme laisse t’il des processus zombie derrière lui ? Sol. : Le programme du listing 5 ne laisse aucun zombie derrière lui, pour 2 raisons : — Par construction, le fils exécute wc, c’est-à-dire qu’il est lecteur : il est susceptible, au pire, de ne jamais terminer, donc ne peut pas être zombie. — Les appels à close sur l’entrée et la sortie du tube sont systématiquement faits après duplication du côté utile de chaque processus. De fait, même si on inversait les rôles du père et du fils, le père débloquerait à la mort du fils du fait que le compteur de références de la sortie du tube tomberait à zéro. Listing 5 – shpipe.c #include < s t d l i b. h> #include int main ( ) { int tube [ 2 ] ; p i p e ( tube ) ; 14 i f ( f o r k ( ) == 0 ) /∗ code du f i l s = e x e c u t a n t de wc − l ∗/ { dup2 ( tube [ 0 ] , 0 ) ; c l o s e ( tube [ 1 ] ) ; c l o s e ( tube [ 0 ] ) ; e x e c l p ( "wc" , "wc" , "− l " , NULL ) ; } e l s e /∗ code du p e r e = e x e c u t a n t de l s −l a ∗/ { dup2 ( tube [ 1 ] , 1 ) ; c l o s e ( tube [ 1 ] ) ; c l o s e ( tube [ 0 ] ) ; e x e c l p ( " l s " , " l s " , "−l a " , NULL ) ; } return 0 ; /∗ normalement j a m a i s a t t e i n t ∗/ } Exercice 3.3 – Client-serveur en mémoire partagée On souhaite faire communiquer deux programmes à l’aide d’une mémoire partagée : — Le programme serveur.c se comporte comme un serveur : il lit des messages en mémoire partagée, qui sont des ordres, et les traite. — A contrario, le programme client.c écrit ses ordres en mémoire partagée. Il n’attend pas de réponse particulière du serveur. Le listing 7 donne le code exécuté par le serveur, qui va traiter 3 questions seulement. 1. Expliquer le fonctionnement du serveur. A quoi servent les tubes /tmp/in et /tmp/out ? Quelles données circulent dessus ? Sol. : Le serveur commence par créer deux tubes nommés sur disque, /tmp/in et /tmp/out, qu’il ouvre chacun d’un côté seulement : l’entrée pour /tmp/in, la sortie pour /tmp/out. Il alloue aussi un segment de mémoire partagée de TAILLE octets, qu’il monte (shmat). La boucle for attend trois fois de suite un acquitement du client en lisant 1 octet sur le tube /tmp/out. Avant cela, elle a notifié le client que le serveur était prêt à traiter la prochaine question en envoyant la clé de la mémoire partagée sur le tube /tmp/in. Les tubes ne servent donc qu’à la synchronisation de l’ensemble : aucune information vraiment utile n’y circule, sinon la clé de mémoire partagée, que le client pourra récupérer directement sur le tube à sa première lecture. On aurait tout aussi bien pu écrire write(in, &x, 1);, mais la clé de la mémoire partagée aurait alors du être passée au client par la ligne de commande. 2. En déduire le code du client posant lui aussi 3 questions. Sol. : Listing 6 – Programme client.c #include < s t d l i b. h> #include #include #include #include #include #include 15 #include #include < f c n t l. h> int main ( ) { int in , out , c l e , i ; char ∗mem, x ; i n= open ( "/tmp/ i n " , O_RDONLY, 0 ) ; out= open ( "/tmp/ out " , O_WRONLY, 0 ) ; assert ( in > 0); a s s e r t ( out > 0 ) ; f o r ( i =0; i < 3 ; i ++) { r e a d ( in , &c l e , s i z e o f ( int ) ) ; i f ( i == 0 ) { /∗ c l e l u e pour l a 1 e r e f o i s => memoire p a r t a g e e d o i t e t r e montee ∗ p r i n t f ( " C l i e n t : ␣ c l e ␣=␣%d\n" , c l e ) ; mem= shmat ( c l e , NULL, 0 ) ; a s s e r t (mem != NULL ) ; } s p r i n t f (mem, " message ␣numero␣%d␣ f o r m a t e " , i ) ; w r i t e ( out , &x , 1 ) ; sleep (1); } return 0 ; } 3. Que se passerait-il si, au lieu de n’avoir qu’un seul processus client posant 3 questions, on lançait 3 processus d’affilée ne posant chacun qu’une question ? Si on laisse le code serveur inchangé, ce dernier va sortir dès la 2ème itération de boucle : en effet, lorsque le client quitte après n’avoir posé qu’une seule question, le système va fermer les entrées et sorties des tubes qu’il a ouvertes mais pas refermées, ce qui fera tomber à 0 le compteur de références de la sortie de /tmp/in, et de l’en- trée de /tmp/out. L’ordre d’écriture write(in, &cle, sizeof(int)) va provoquer l’émission d’un signal SIGPIPE par le système au processus appelant (c’est une fautre d’écrire sur un tube dont la sortie est inutilisable, voir cours), en l’occurence, le ser- veur. Pour éviter cela, il suffit que le serveur ouvre aussi les côtés des tubes inutilisés, donc d’ajouter des lignes de la forme : open("/tmp/in", O_RDONLY); open("/tmp/out"), O_WRONLY); Dans ces conditions, le client peut quitter quand il le souhaite. La concurrence d’accès est également gérée : plusieurs processus peuvent faire le read(in, &cle, sizeof(int)), mais seul l’un d’entre eux obtiendra la clé, les autres étant bloqués pendant ce temps- là. 4. Le code du serveur se termine t’il “proprement” (ç-à-d, laisse t’il les ressources système 16 dans l’état où il les a trouvées) ? Si oui, expliquer pourquoi. Sinon, expliquer ce qu’il faut rajouter. Sol. : Presque. Le segment de mémoire partagée est bien supprimé par l’appel à shmctl(cle, IPC_RIMD, NULL), donc pas de problème de ce côté. Par contre, les deux tubes créés par les appels à mkfifo() restent sur disque. Pour une sortie parfaitement « propre », il faudrait ajouter unlink("/tmp/in"); unlink("/tmp/out"); avant le return 0 Listing 7 – Programme serveur.c #include < s t d l i b. h> #include #include #include #include #include #include #include #include < f c n t l. h> #define TAILLE 20000 void t r a i t e m e n t ( char ∗mem) { p r i n t f ( " S e r v e u r : ␣ message ␣ l u ␣=␣%s \n" , mem) ; } int main ( ) { int in , out , c l e , i ; char ∗mem, x ; m k f i f o ( "/tmp/ i n " , 0 6 6 6 ) ; m k f i f o ( "/tmp/ out " , 0 6 6 6 ) ; c l e= shmget (IPC_PRIVATE, TAILLE , IPC_CREAT | IPC_EXCL | 0 6 0 0 ) ; assert ( cle > 0); mem= shmat ( c l e , NULL, 0 ) ; a s s e r t (mem != NULL ) ; i n = open ( " /tmp/ i n " , O_WRONLY, 0 ) ; out= open ( "/tmp/ out " , O_RDONLY, 0 ) ; assert ( in > 0); a s s e r t ( out > 0 ) ; p r i n t f ( " S e r v e u r : ␣ c l e ␣=␣%d\n" , c l e ) ; fo r ( i= 0 ; i < 3 ; i ++) { a s s e r t ( w r i t e ( in , &c l e , s i z e o f ( int ) ) >= 0 ) ; a s s e r t ( r e a d ( out , &x , 1 ) >= 0 ) ; 17 t r a i t e m e n t (mem) ; } shm ctl ( c l e , IPC_RMID, NULL ) ; return 0 ; } Exercice 3.4 – Tubes et processus (examen 18-19) Considérez le programme du listing 8. Listing 8 – Processus 1 #i n c l u d e 2 #i n c l u d e 3 4 i n t main ( ) 5 { 6 c h a r mem[ 1 0 0 0 0 ] ; 7 i n t tube [ 2 ] , l u ; 8 9 p i p e ( tube ) ; 10 dup2 ( tube [ 1 ] , 1 ) ; 11 c l o s e ( tube [ 1 ] ) ; 12 13 i f ( f o r k ( ) == 0 ) 14 e x e c l p ( " l s " , " l s " , NULL ) ; 15 16 w a i t (NULL ) ; 17 close (1); 18 19 w h i l e ( ( l u= r e a d ( tube [ 0 ] , mem, 1 0 0 0 0 ) ) > 0 ) 20 w r i t e ( 1 , mem, l u ) ; 21 22 return 0; 23 } 1. Qu’est censé faire ce programme ? Sol. : Le main() de ce programme lance un fils qui va se sacrifier pour exécuter ls. Avant de forker, le père a pris soin de rediriger sa sortie standard vers l’entrée d’un tube qu’il a créé, de sorte qu’en écrivant sur 1 (sortie standard), ls va écrire dans l’entrée du tube. Dans la dernière partie, le père attend la mort de son enfant, puis relit le contenu du tube, et cherche à afficher ce qu’il a relu à l’écran. La figure suivante résume la situation. 18 2. Lorsqu’on exécute ce programme, rien n’apparaît à l’écran. Expliquer pourquoi. Sol. : Pour deux raisons : non seulement il y a un close(1) juste avant la boucle de lecture, qui fait que la sortie standard est inutilisable immédiatement après puisque fermée ; mais en plus, cette même fermeture est faite beaucoup plus tôt, lors du dup2(tube,1), qui commence par faire elle-même un close(1) par définition. Il y a donc 2 problèmes : — Les deux lignes après pipe(tube) ne devraient être exécutées que par le fils, et surtout pas par le père, qui n’a en fait pas besoin de rediriger quoi que ce soit — Le père doit fermer le côté du tube qu’il n’utilise pas, ç-à-dire faire un close(tube) et non un close(1) avant de se lancer dans sa boucle, sous peine que cette dernière ne termine jamais du fait read() ne puisse pas renvoyer 0. 3. Si on corrige l’erreur précédente, il arrive que le programme se bloque. Expliquer pourquoi. Sol. : Parce qu’il reste le problème de l’appel à wait(NULL), qui conditionne la lecture du tube par le père à la mort de son fils. Ce qui non seulement ne sert à rien, mais est même indésirable et faux. Il se peut très bien, en effet, que le fils doive écrire plus de données dans le tube que le tube lui-même ne peut en contenir (4096 octets, cf. cours). Les appels à write(1,...) dans le fils peuvent donc bloquer, car le tube peut être saturé, et la situation n’évoluera pas puisque le père attend la fin du fils pour vider le tube. Il faut donc supprimer l’appel à wait, pour que le père engage sa boucle de lecture sans attendre. Dernière erreur : la taille de 10000, qui doit être abaissée à 512 si l’on ne veut pas écrire de boucle qui garantisse que les appels à write() auront bien produit les lu octets lus par read(), 4. Corriger le programme pour qu’il fonctionne correctement. Sol. : Il existe deux manières de corriger les problèmes précédents : — Soit on veut absolument conserver le close(1) avant la relecture du tube dans le père. Auquel cas, on doit sauvegarder ce vers quoi la sortie standard (1) menait avant l’appel à dup2 par une ligne de la forme f= dup(1), puis changer l’appel à write(1,...) en write(f,...) pour que le père écrive sur f, et non sur 1. Cette solution, qui n’est clairement pas la meilleure, donne la figure suivante, et le code en listing 9. 19 — Soit on prend en compte la remarque faite à la question 2, selon laquelle les redi- rections en lignes 9 à 11 sont faites beaucoup trop tôt, et ne devraient concerner que l’enfant, pas le père. Auquel cas, on doit les déplacer dans le code de l’enfant, et le père fait un close(tube). Ce qui donne la figure suivante, et le listing 10. Listing 9 – Code corrigé, 1ère solution 1 #i n c l u d e 2 #i n c l u d e 3 4 i n t main ( ) 5 { 6 c h a r mem[ 1 0 0 0 0 ] ; 7 i n t tube [ 2 ] , lu , f ; 8 9 p i p e ( tube ) ; 10 f= dup ( 1 ) ; 11 dup2 ( tube [ 1 ] , 1 ) ; 12 c l o s e ( tube [ 1 ] ) ; 13 14 i f ( f o r k ( ) == 0 ) 15 e x e c l p ( " l s " , " l s " , NULL ) ; 16 17 close (1); 18 19 w h i l e ( ( l u= r e a d ( tube [ 0 ] , mem, 1 0 0 0 0 ) ) > 0 ) 20 w r i t e ( f , mem, l u ) ; 21 20 22 return 0; 23 } Listing 10 – Code corrigé, 2ème solution 1 #i n c l u d e 2 #i n c l u d e 3 4 i n t main ( ) 5 { 6 c h a r mem [ 5 1 2 ] ; 7 i n t tube [ 2 ] , l u ; 8 9 p i p e ( tube ) ; 10 11 i f ( f o r k ( ) == 0 ) { 12 dup2 ( tube [ 1 ] , 1 ) ; 13 c l o s e ( tube [ 1 ] ) ; /∗ f a c u l t a t i f ∗/ 14 15 e x e c l p ( " l s " , " l s " , NULL ) ; 16 } 17 18 c l o s e ( tube [ 1 ] ) ; 19 20 w h i l e ( ( l u= r e a d ( tube [ 0 ] , mem, 5 1 2 ) ) > 0 ) 21 w r i t e ( 1 , mem, l u ) ; 22 23 return 0; 24 } 4 Synchronisation Exercice 4.1 – Signaux La fonction alarm(int n) a pour effet de demander au système l’envoi du signal SI- GALRM au processus appelant, environ n secondes après le retour de l’appel. Il n’y a qu’un seul signal délivré, et à la différence de sleep(), la fonction n’est pas bloquante. 1. Ecrivez un programme qui se comporte de la manière suivante : — Il demande à l’utilisateur de saisir une valeur entière (entre 0 et 254) au clavier. — Si l’utilisateur répond dans les 20s, et si la valeur entrée correspond à l’unique argument passé au programme, alors le programme termine avec un code de sortie égal à cette valeur. — Dans les autres cas, le programme doit sortir avec le code 255, et jamais au-delà de 20s. Sol. : Voir listing 11. 2. Modifiez votre programme pour que tant que l’utilisateur n’a saisi sa proposition, il affiche aussi « Vous êtes lent ! » toutes les 5s. Sol. : Voir listing 12. 21 Sol. : Listing 11 – Sortie simple au bout de 20s #include #include #include < s t d l i b. h> #include void q u i t t e r ( int s i g ) { p r i n t f ( "Trop␣ long , ␣ j e ␣ q u i t t e \n" ) ; exit (255); } int main ( int argc , char ∗ argv [ ] ) { int code ; struct s i g a c t i o n s ; /∗ On u t i l i s e l a v e r s i o n sa_handler , e t non s a _ s i g a t i o n => f l a g s a 0 D e l a i de p l u s i e u r s s e c o n d e s e n t r e l e s s i g n a u x => pas b e s o i n de masquer ∗/ s. sa_handler= q u i t t e r ; s. s a _ f l a g s= 0 ; s i g e m p t y s e t (& s. sa_mask ) ; /∗ a s s o c i e r l e g e s t i o n n a i r e a SIGALRM ∗/ s i g a c t i o n (SIGALRM, &s , NULL ) ; /∗ s i g n a l envoye dans 20 s ∗/ alarm ( 7 ) ; /∗ r e s t e du programme ∗/ p r i n t f ( " Entrez ␣ une ␣ v a l e u r : ␣" ) ; f f l u s h ( stdout ) ; s c a n f ( "%d" , &code ) ; i f ( code == a t o i ( argv [ 1 ] ) ) return code ; return 2 5 5 ; } Listing 12 – Sortie au bout de 20s avec relances toutes les 5s #include #include #include < s t d l i b. h> #include void q u i t t e r ( int s i g ) { s t a t i c int compteur= 0 ; i f (++compteur >= 4 ) { 22 p r i n t f ( "Trop␣ long , ␣ j e ␣ q u i t t e \n" ) ; exit (255); } p r i n t f ( "Vous␣ e t e s ␣ l e n t ! \ n" ) ; alarm ( 5 ) ; /∗ reprogrammer l ’ alarme ∗/ } int main ( int argc , char ∗ argv [ ] ) { int code= −1; struct s i g a c t i o n s ; /∗ On u t i l i s e l a v e r s i o n sa_handler , e t non s a _ s i g a c t i o n => pas de SA_SIGINFO Par c o n t r e , on d o i t s p e c i f i e r SA_RESTART en masque , c a r s c a n f ( ) , q u i a t t e n d SIGIO , s o r t i r a i t e t ne s e r a i t pas r e l a n c e D e l a i de p l u s i e u r s s e c o n d e s e n t r e l e s s i g n a u x => pas b e s o i n de masquer ∗/ s. sa_handler= q u i t t e r ; s. s a _ f l a g s= SA_RESTART; s i g e m p t y s e t (& s. sa_mask ) ; /∗ a s s o c i e r l e g e s t i o n n a i r e a SIGALRM ∗/ s i g a c t i o n (SIGALRM, &s , NULL ) ; /∗ 1 e r s i g n a l envoye dans 5 s ∗/ alarm ( 5 ) ; /∗ r e s t e du programme ∗/ p r i n t f ( " Entrez ␣ une ␣ v a l e u r : ␣" ) ; f f l u s h ( stdout ) ; s c a n f ( "%d" , &code ) ; i f ( code == a t o i ( argv [ 1 ] ) ) return code ; return 2 5 5 ; } Exercice 4.2 – Moines Deux moines Shaolins passent leur temps à combattre et manger du riz pour reprendre des forces. Lorsqu’ils n’ont pas faim, ils combattent. Lorsque l’un a faim, il s’assied du côté où se trouve son bol sur la table, mais a besoin des deux baguettes pour manger son riz. 23 En supposant que chaque moine est un processus, expliquer comment synchroniser le problème en utilisant le moins de sémaphores possible. Exercice 4.3 – Circulation alternée Un tronçon de route est temporairement mis en circulation alternée pour cause de tra- vaux. Un grand nombre de voitures à pilotage automatique attendent de chaque côté pour pouvoir passer. Le code exécuté par l’ordinateur embarqué se résume à celui présenté en lis- ting 13 : chaque voiture est représentée par un processus qui exécute la fonction voiture(0) ou voiture(1) selon qu’elle se trouve du côté gauche ou droit du tronçon. Listing 13 – Voitures int v o i t u r e ( int d r o i t e ) { char ∗ s [ ] = { " gauche " , " d r o i t e " } ; int c o t e= d r o i t e ; fo r ( ; ; ) { /∗ a t t e n d r e l a p e r m i s s i o n de p a s s e r de l ’ a u t r e c o t e ∗/ /∗.... code a c o m p l e t e r.... ∗/ /∗ s ’ e n g a g e r e t t r a v e r s e r l e t r o n c o n ∗/ p r i n t f ( " V o i t u r e ␣%d␣ p a s s e ␣ de ␣%s ␣a ␣%s \n" , g e t p i d ( ) , s [ c o t e ] , s [1− c o t e ] ) ; p a s s e r (1− c o t e ) ; /∗ A p r e s e n t , v o i t u r e de l ’ a u t r e c o t e ∗/ c o t e= 1− c o t e ; /∗.... code a c o m p l e t e r.... ∗/ } } Vous pourrez supposer qu’un tableau de sémaphores d’identifiant semid a déjà été créé et initialisé, et que vous disposez des fonctions up() et down() décrites dans le cours. 1. Complétez le code ci-dessus pour que les voitures passent une à une sur le tronçon, sans créer de famine d’un côté ou de l’autre. Sol. : Voir listing 14 2. Etendez votre solution pour faire passer les voitures par paquets de 20. Vous pourrez supposer que 2 processus supplémentaires jouent le rôle d’agent de circulation, et exécutent agent(0) ou agent(1) selon le côté du tronçon sur lequel ils se trouvent. Donnez le code de agent(int droite). Sol. : Voir listing 15 3. Donnez une nouvelle solution qui n’utilise pas d’agents. Sol. : Voir listing 16 Sol. : Listing 14 – Voitures passant une par une #include #include < s t d l i b. h> 24 #include #include #include #include #include #include #include #include /∗ nombre de v o i t u r e s ∗/ #define N 10 int semid ; /∗ c l e du t a b l e a u de semaphores ∗/ /∗ o p e r a t i o n up ( ou V) de D i j k s t r a ∗/ int up ( int semnum ) { struct sembuf op ; op. sem_num = semnum ; op. sem_op = +1; op. sem_flg = 0 ; return semop ( semid , &op , 1 ) ; } /∗ o p e r a t i o n down ( ou P) de D i j k s t r a ∗/ int down ( int semnum ) { struct sembuf op ; op. sem_num = semnum ; op. sem_op = −1; op. sem_flg = 0 ; return semop ( semid , &op , 1 ) ; } void p a s s e r ( int c o t e ) { p r i n t f ( " V o i t u r e ␣ e s t ␣ a r r i v e ␣du␣ c o t e ␣%d\n" , c o t e ) ; } void v o i t u r e ( int d r o i t e ) { char ∗ s [ ] = { " gauche " , " d r o i t e " } ; int c o t e = d r o i t e ; p r i n t f ( " V o i t u r e ␣%d␣ i n i t i a l e m e n t ␣a ␣%s \n" , g e t p i d ( ) , s [ c o t e ] ) ; fo r ( ; ; ) 25 { down ( c o t e ) ; p r i n t f ( " V o i t u r e ␣%d␣ p a s s e ␣ de ␣%s ␣a ␣%s \n" , g e t p i d ( ) , s [ c o t e ] , s [1− c o t e ] ) ; p a s s e r (1− c o t e ) ; up(1− c o t e ) ; c o t e= 1− c o t e ; } } int main ( ) { int p i d s [N] , i ; /∗ c r e a t i o n d ’ un t a b l e a u de 2 semaphores ∗/ semid= semget (IPC_PRIVATE, 2 , IPC_CREAT | 0 6 0 0 ) ; a s s e r t ( semid > 0 ) ; /∗ c r e a t i o n d e s v o i t u r e s : 1 v o i t u r e = 1 f i l s ∗/ fo r ( i= 0 ; i < N; i ++) { pids [ i ] = fork ( ) ; i f ( p i d s [ i ] == 0 ) voiture ( i % 2); } /∗ i n i t i a l i s e r l ’ e n s e m b l e : +1 j e t o n a gauche ∗/ up ( 0 ) ; /∗ q u i t t e r au b o u t de 5 s : t u e r t o u s l e s f i l s , s u p p r i m e r l e s semaphores ∗/ sleep (5); fo r ( i= 0 ; i < N; i ++) k i l l ( p i d s [ i ] , SIGKILL ) ; s e m c t l ( semid , 0 , IPC_RMID ) ; return 0 ; } Listing 15 – Voitures passant par paquets de 20, avec agents et 4 sémaphores #include #include < s t d l i b. h> #include #include #include #include #include #include #include #include 26 /∗ nombre de v o i t u r e s ∗/ #define N 60 int semid ; /∗ c l e du t a b l e a u de semaphores ∗/ /∗ o p e r a t i o n up ( ou V) de D i j k s t r a ∗/ int up ( int semnum ) { struct sembuf op ; op. sem_num = semnum ; op. sem_op = +1; op. sem_flg = 0 ; return semop ( semid , &op , 1 ) ; } /∗ o p e r a t i o n down ( ou P) de D i j k s t r a ∗/ int down ( int semnum ) { struct sembuf op ; op. sem_num = semnum ; op. sem_op = −1; op. sem_flg = 0 ; return semop ( semid , &op , 1 ) ; } void a g e n t ( int d r o i t e ) { int i ; fo r ( ; ; ) { fo r ( i= 0 ; i < 2 0 ; i ++) down(2+ d r o i t e ) ; fo r ( i= 0 ; i < 2 0 ; i ++) up ( d r o i t e ) ; } } void v o i t u r e ( int d r o i t e ) { char ∗ s [ ] = { " gauche " , " d r o i t e " } ; int c o t e = d r o i t e ; p r i n t f ( " V o i t u r e ␣%d␣ i n i t i a l e m e n t ␣a ␣%s \n" , g e t p i d ( ) , s [ c o t e ] ) ; fo r ( ; ; ) 27 { down ( c o t e ) ; p r i n t f ( " V o i t u r e ␣%d␣ p a s s e ␣ de ␣%s ␣a ␣%s \n" , g e t p i d ( ) , s [ c o t e ] , s [1− c o t e ] ) ; // p a s s e r (1− c o t e ) ; up(3− c o t e ) ; c o t e= 1− c o t e ; } } int main ( ) { int p i d s [N+2] , i ; /∗ c r e a t i o n d ’ un t a b l e a u de 4 semaphores ∗/ semid= semget (IPC_PRIVATE, 4 , IPC_CREAT | 0 6 0 0 ) ; a s s e r t ( semid > 0 ) ; /∗ c r e a t i o n d e s v o i t u r e s : 1 v o i t u r e = 1 f i l s ∗/ fo r ( i= 0 ; i < N; i ++) { pids [ i ] = fork ( ) ; i f ( p i d s [ i ] == 0 ) voiture ( i % 2); } /∗ c r e a t i o n d e s a g e n t s : 1 a g e n t = 1 f i l s ∗/ fo r ( i= 0 ; i < 2 ; i ++) { p i d s [N+i ] = f o r k ( ) ; i f ( p i d s [N+i ] == 0 ) agent ( i ) ; } /∗ i n i t i a l i s e r l ’ e n s e m b l e : +20 j e t o n s a gauche ∗/ fo r ( i= 0 ; i < 2 0 ; i ++) up ( 0 ) ; /∗ q u i t t e r au b o u t de 5 s : t u e r t o u s l e s f i l s , s u p p r i m e r l e s semaphores ∗/ sleep (5); fo r ( i= 0 ; i < N+2; i ++) k i l l ( p i d s [ i ] , SIGKILL ) ; s e m c t l ( semid , 0 , IPC_RMID ) ; return 0 ; } Listing 16 – Voitures passant par paquets de 20, sans agents, 3 sémaphores, mais mémoire partagée #include 28 #include < s t d l i b. h> #include #include #include #include #include #include #include #include #include /∗ nombre de v o i t u r e s ∗/ #define N 50 int semid= −1; /∗ c l e du t a b l e a u de semaphores ∗/ int ∗ pc= NULL; /∗ p o i n t e u r s u r compteur en memoire p a r t a g e e ∗/ /∗ o p e r a t i o n up ( ou V) de D i j k s t r a ∗/ int up ( int semnum ) { struct sembuf op ; op. sem_num = semnum ; op. sem_op = +1; op. sem_flg = 0 ; return semop ( semid , &op , 1 ) ; } /∗ o p e r a t i o n down ( ou P) de D i j k s t r a ∗/ int down ( int semnum ) { struct sembuf op ; op. sem_num = semnum ; op. sem_op = −1; op. sem_flg = 0 ; return semop ( semid , &op , 1 ) ; } void v o i t u r e ( int d r o i t e ) { char ∗ s [ ] = { " gauche " , " d r o i t e " } ; int c o t e = d r o i t e , i ; p r i n t f ( " V o i t u r e ␣%d␣ i n i t i a l e m e n t ␣a ␣%s \n" , g e t p i d ( ) , s [ c o t e ] ) ; fo r ( ; ; ) { 29 down ( c o t e ) ; p r i n t f ( " V o i t u r e ␣%d␣ p a s s e ␣ de ␣%s ␣a ␣%s \n" , g e t p i d ( ) , s [ c o t e ] , s [1− c o t e ] ) ; /∗ a c c e d e r e x c l u s i v e m e n t au compteur ∗/ down ( 2 ) ; ( ∗ pc )++; i f ( ∗ pc == 2 0 ) /∗ c ’ e s t l a d e r n i e r e => +20 j e t o n s , r e i n i t. compteur ∗/ { p r i n t f ( " pc=0␣ a␣%s \n" , s [1− c o t e ] ) ; ∗ pc= 0 ; f o r ( i= 0 ; i < 2 0 ; i ++) up(1− c o t e ) ; } /∗ r e n d r e l ’ a c c e s au compteur ∗/ up ( 2 ) ; c o t e= 1− c o t e ; } } int main ( ) { int p i d s [N] , i , shmid ; /∗ c r e a t i o n d ’ un t a b l e a u de 3 semaphores ∗/ semid= semget (IPC_PRIVATE, 3 , IPC_CREAT | 0 6 0 0 ) ; a s s e r t ( semid > 0 ) ; /∗ c r e a t i o n + i n i t i a l i s a t i o n du compteur en memoire p a r t a g e e ∗/ shmid= shmget (IPC_PRIVATE, s i z e o f ( int ) , 0 6 0 0 ) ; a s s e r t ( shmid > 0 ) ; pc= shmat ( shmid , NULL, 0 ) ; a s s e r t ( pc != NULL ) ; ∗ pc= 0 ; /∗ c r e a t i o n d e s v o i t u r e s : 1 v o i t u r e = 1 f i l s ∗/ fo r ( i= 0 ; i < N; i ++) { pids [ i ] = fork ( ) ; i f ( p i d s [ i ] == 0 ) { pc= shmat ( shmid , NULL, 0 ) ; a s s e r t ( pc != NULL ) ; voiture ( i % 2); } } /∗ i n i t i a l i s e r l ’ e n s e m b l e : +1 j e t o n pour l e mutex , +20 j e t o n s a gauche ∗/ 30 up ( 2 ) ; fo r ( i= 0 ; i < 2 0 ; i ++) up ( 0 ) ; /∗ q u i t t e r au b o u t de 5 s : t u e r t o u s l e s f i l s , s u p p r i m e r l e s semaphores e t l e segment de memoire p a r t a g e e ∗/ sleep (5); fo r ( i= 0 ; i < N; i ++) k i l l ( p i d s [ i ] , SIGKILL ) ; s e m c t l ( semid , 0 , IPC_RMID ) ; s e m c t l ( shmid , 0 , IPC_RMID ) ; return 0 ; } Exercice 4.4 – Barrière Une barrière est un rendez-vous temporel que s’imposent mutuellement N processus : aucun ne doit pouvoir poursuivre son code sans avoir attendu tous les autres. Par exemple, dans le code suivant, qui est exécuté par les N processus, aucun ne doit pouvoir exécuter B() tant que A() n’a pas été terminée par tous les processus. void code() { A(); barriere(); B(); } Expliquer comment implémenter une barrière en n’utilisant soit que des signaux, soit que des sémaphores, et dans les deux cas, le moins possible. 31