0% ont trouvé ce document utile (0 vote)
692 vues

Resumé Cours

Ce document présente un résumé du cours de système d'exploitation II. Il décrit les objectifs du cours, les prérequis, le plan du cours et introduit des concepts clés comme les processus, les threads, la synchronisation et la communication.

Transféré par

BENYAHIA NESRINE
Copyright
© © All Rights Reserved
Formats disponibles
Téléchargez aux formats PDF, TXT ou lisez en ligne sur Scribd
0% ont trouvé ce document utile (0 vote)
692 vues

Resumé Cours

Ce document présente un résumé du cours de système d'exploitation II. Il décrit les objectifs du cours, les prérequis, le plan du cours et introduit des concepts clés comme les processus, les threads, la synchronisation et la communication.

Transféré par

BENYAHIA NESRINE
Copyright
© © All Rights Reserved
Formats disponibles
Téléchargez aux formats PDF, TXT ou lisez en ligne sur Scribd
Vous êtes sur la page 1/ 27

1

Résumé du cours de système d’exploitation II

Objectifs du cours :

Les objectifs du cours de système d’exploitation II, est de compléter les connaissances acquises dans le cours
SEI de la deuxième année concernant la notion de processus et de threads ainsi qu’une introduction à la
problématique du parallélisme dans les systèmes centralisés. Il s’agit donc d’étudier la mise en œuvre des
mécanismes de synchronisation, de communication et d’Interblocage dans l’environnement centralisé

Bon à savoir !
o Prérequis : Cours (algorithmique, SE1, 2ème année Licence).

o Volume horaire hebdomadaire : 1.5H Cours + 1.5H TD + 1.5H TP.

o Évaluation : continu + Examen

o Coefficient : 3, Crédit : 5

Plan du cours
1. Introduction

Introduction aux S. E

Processus et threads

2. Synchronisation

· Problème de l’exclusion mutuelle

· Synchronisation

o Evénements, Verrous

o Sémaphores

o Moniteurs

3. Communication

4. Interblocage
2

I. Introduction aux S.E :


Le matériel (hardware) d'un système informatique est composé d’unités de traitement (processing) qui
exécutent les instructions, de mémoire centrale qui contient les données et les instructions à exécuter (en
binaire) et de mémoire secondaire qui sauvegarde les informations et de périphériques d'Entrées/Sorties
(clavier, souris, écran, modem, etc.) pour introduire ou récupérer des informations. Les logiciels (software),
d'un système informatique, sont à leur tour divisés en programmes système qui fait fonctionner l'ordinateur :
le système d’exploitation et les utilitaires (compilateurs, éditeurs, interpréteurs de commandes, etc.) et en
programmes d'application qui résolvent des problèmes spécifiques des utilisateurs.

Définition

Le Système d‘Exploitation (noté SE ou OS, abréviation du terme anglais Operating System) représente
l’interface entre l'utilisateur, et la machine physique. Il s’agit donc, d’un programme informatique qui pilote
les dispositifs matériels et reçoit des instructions de l’utilisateur ou d’autres logiciels (ou applications).

Les composants du S.E:

Le noyau (en anglais kernel) : représentant les fonctions fondamentales du système d’exploitation telles que
la gestion de la mémoire, des processus, des fichiers, des entrées-sorties principales, et des fonctionnalités de
communication.

L'interpréteur de commande (en anglais shell) permettant la communication avec le système d'exploitation
par l'intermédiaire d'un langage de commandes.

Le système de fichiers (en anglais « file system », noté FS), permettant d’enregistrer les fichiers dans le
disque sous forme d’une arborescence.

Les pilotes : Les pilotes sont fournis par l'auteur du système d’exploitation ou le fabricant du périphérique.

Rôle du système d'exploitation

• Gestion des processeurs : Gérer l'allocation des processeurs entre les différents

Programmes grâce à un algorithme d'ordonnancement.

• Gestion des processus (programmes en cours d'exécution) : Gérer l’exécution des processus en leur
affectant les ressources nécessaires à leur bon fonctionnement.

• Gestion des mémoires : Gérer l'espace mémoire alloué à chaque processus.

• Gestion des fichiers : Gère l’organisation du disque dur et du système de fichiers

I.1 Définition d’un processus

Un processus est l’entité dynamique représentant l’exécution d’un programme sur un processeur

Un processus est l'activité résultant de l'exécution d'un programme séquentiel, avec ses données, par un
processeur.

Un processus est caractérisé par :

1. Un numéro d’identification unique (PID) ;

2. Un espace d’adressage (code, données, piles d’exécution) ;

3. Un état principal (prêt, en cours d’exécution (élu), bloqué, …) ;


3

4. Les valeurs des registres lors de la dernière suspension (CO, sommet de Pile...) ;

5. Une priorité ;

6. Les ressources allouées (fichiers ouverts, mémoires, périphériques …) ;

7. Les signaux à capter, à masquer, à ignorer, en attente et les actions associées ;

8. Autres informations indiquant le processus père, les processus fils, le groupe, les variables
d’environnement, les statistiques et les limites d’utilisation des ressources....

États d'un processus

Plusieurs processus peuvent se trouver simultanément en cours d’exécution (multiprogrammation et temps


partagé), si un système informatique ne comporte qu’un seul processeur, alors, à un instant donné, un seul
processus aura accès à ce processeur. En conséquence, un programme en exécution peut avoir plusieurs états.
Ces états peuvent être résumés comme suit :

Nouveau : création d'un processus dans le système

Prêt : le processus est placé dans la file d’attente des processus prêts, en attente d’affectation du
processeur.

En exécution : Le processus est en cours d'exécution.

Bloqué : Le processus attend qu’un événement se produise, comme l’achèvement d’une opération d’E/S ou
la réception d’un signal.

Fin: terminaison de l'exécution

Le bloc de contrôle et le contexte d'un processus

Pour suivre son évolution, le SE maintient pour chaque processus une structure de données particulière
appelée bloc de contrôle de processus (PCB : Process Control Bloc) et dont le rôle est de reconstituer le
contexte du processus.

Le contexte d'un processus est l'ensemble des informations dynamiques qui représente l‘état d‘ exécution
d'un processus

Le PCB contient aussi des informations sur l'ordonnancement du processus (priorité du processus, les
pointeurs sur les files d'attente)

L'appel système fork() est une façon qui permet de créer des processus aussi appelés processus lourds.
4

Exemple 1 : Instruction de création :

#include <unistd.h>

int fork();

fork() est le moyen de créer des processus, par duplication d'un processus existant. L'appel système fork()
crée une copie exacte du processus original.

Pour distinguer le processus père du processus fils on regarde la valeur de retour de fork(), qui peut être:

➢ La valeur 0 dans le processus fils.

➢ Positive pour le processus père et qui correspond au PID du processus fils

➢ Négative si la création de processus a échoué ;

Lors du démarrage de Unix, deux processus sont crées :

1. le Swapper (pid = 0) qui gère la mémoire;

2. le Init (pid = 1) qui crée tous les autres processus.

Exemple : Un programme qui affiche les entiers de 0 à 3 par 4 processus différents.

#include <unistd.h>

#include <stdio.h>

void main()

int p_id1,p_id2;

printf("Processus ID=%i affiche la valeur 0\n", getpid());

p_id1 = fork();
5

if (p_id1 == 0)

printf("Processus ID=%i affiche la valeur 1\n",getpid());

p_id2 = fork() ;

if (p_id2==0) printf("Processus ID=%i affiche la valeur 2\n",getpid());

}else if(p_id1 > 0 )

p_id2 = fork() ;

if (p_id2==0) printf("Processus ID=%i affiche la valeur 3\n",getpid());

En exécutant ce code, en obtient :

charikhi@charikhi-X550JX:~/Bureau/Trav 21/teste$ ./td1

Processus ID=3186 affiche la valeur 0

Processus ID=3187 affiche la valeur 1

Processus ID=3189 affiche la valeur 2

Processus ID=3188 affiche la valeur 3

I.2 Définition des threads :

Les threads appelés aussi processus légers, comme les processus, est un mécanisme permettant a un
programme de faire plus d'une chose à la fois (c.-à-d. exécution simultanée des parties d'un programme).

Un thread est une unité d'exécution rattachée à un processus permettent son exécution.

Comme les processus, les threads semblent s'exécuter en parallèle ; le noyau du SE les ordonnance,
interrompant chaque thread pour donner aux autres une chance de s'exécuter.

Lorsqu'un programme crée un nouveau thread, dans ce cas, rien n’est copie. Le thread créateur et le thread
crée partagent tous deux le même espace mémoire, les mêmes descripteurs de fichiers et autres ressources.

Création de threads :
La fonction pthread_create crée un nouveau thread.

Syntaxe : int pthread_create ( pthread_t *thread , pthread_attr_t *attr, void *nomFonct, void *arg);

Paramètres du thread :
6

• Un pointeur (*thread) vers une variable pthread_t, dans laquelle l'identifiant (TID)du thread sera
stocke;

• Un pointeur (*attr) vers un objet d'attribut de thread `(taille de la pile, priorité….). Par défaut, il
prend NULL

• Un pointeur (* nomFonct) vers la fonction de thread. Il s'agit d'un pointeur de fonction ordinaire de
type: void* (*funct) (void*);

• Une valeur d'argument de thread de type void*.

L'appel renvoie 0 s'il réussit, sinon il renvoie une valeur non nulle identifiant l’erreur qui s'est produite

Fonctions pthread_join(), pthread_self() et pthread_exit() :


void pthread_join( pthread_t tid, void * *status);

• Attend la fin d’un thread. L’équivalent de waitpid des processus sauf qu’on doit spécifier le tid du
thread à attendre.

• status sert à récupérer la valeur de retour et l’état de terminaison.

pthread_t pthread_self(void);

• Retourne le TID du thread.

void pthread_exit( void * status);

• Termine l'exécution du thread

Exemple :

#include <pthread.h> int main ()

#include <stdio.h> {

#include <unistd.h> /* Le programme principal. Processus*/

int i=0,n = 0 ; pthread_t thread_id1,thread_id2;

void* A (void *data){ /* Cree un nouveau thread. Le nouveau thread

for(i=0;i<6;i++) { exécutera la fonction A et B*/

n++; pthread_create (&thread_id1, NULL, &A, NULL);

printf("Hello world de A %d\n",n); pthread_create (&thread_id2, NULL, &B, NULL);

sleep(1); pthread_join (thread_id1, NULL);

} pthread_join (thread_id2, NULL);

return 0 ; return 0;

} }

void* B (void *data){

for(i=0;i<6;i++) {
7

n++;

sleep(1);

printf("Hello world de B %d\n",n);

return 0;

Le programme affiche le résultat ci-dessous. Expliquez pourquoi ?

charikhi@charikhi-X550JX:~/Bureau/Trav 21$ ./tst2

Hello world de A 1

Hello world de A 3

Hello world de B 3

Hello world de A 5

Hello world de B 5

Hello world de B 6

En effet, le compteur i ainsi que la variable n sont des variables partagées par les deux threads crées. Le
compteur va être incrémenté par les deux threads.

A cause de l’accès concurrent des threads aux données partagées et l’emplacement de la commande sleep()
avant la commande printf() dans le thread B, les valeurs 2 et 4 ne sont pas affichées.

II Synchronisation
II.1 Introduction

Lorsque plusieurs processus s'exécutent dans un système d'exploitation multitâches, en pseudo-parallèle ou


en parallèle et en temps partagé, ils partagent des ressources (mémoires, imprimantes, etc.). Cependant, le
partage d'objets sans précaution particulière peut conduire a des résultats incohérents. La solution a ce
problème s'appelle synchronisation des processus.

Ressources critiques :

Les ressources partagées, comme des zones mémoire, cartes d'entrées/sorties, etc., sont dites
critiques si elles ne peuvent être utilisées simultanément que par un seul processus.

On appelle ressource critique tout objet : variable, table, chier, périphérique, ... qui peut faire l'objet
d'un accès concurrent (ou simultané) par plusieurs processus.

Section critique :

Une section critique (SC) est un ensemble d'instruction d'un programme qui peuvent engendrer des
résultats imprévisibles (ou incohérents) lorsqu'elles sont exécutées simultanément par des processus
différents.
8

Une SC est une suite d'instructions qui opèrent sur un ou plusieurs ressources partagées (critiques) et
qui nécessitent une utilisation exclusive de ces ressources.

Exclusion mutuelle :

Les problèmes d'incohérences des résultats, posé par les accès concurrents, montrent que la solution
consiste à exécuter les sections critiques en exclusion mutuelle. C'est à dire qu'une section critique (SC) ne
peut être entamée (ou exécutée) que si aucune autre SC du même ensemble n'est en exécution.

Le principe général d'une solution garantissant que l'exécution simultanée de plusieurs processus ne
conduirait pas à des résultats imprévisibles est : avant d'exécuter une SC, un processus doit s'assurer
qu'aucun autre processus n'est en train d'exécuter une SC du même ensemble. Dans le cas contraire, il ne
devra pas avancer tant que l'autre processus n'aura pas terminé sa SC.

Conditions nécessaires pour réaliser une exclusion mutuelle

Pour réaliser une exclusion mutuelle utile on admet que certaines conditions doivent être respectées :

1. Le déroulement : Le fait qu'un processus qui ne demande pas à entrer en section critique ne
doit pas empêcher un autre processus d'y entrer. En plus, aucun processus suspendu en
dehors de sa section critique ne doit bloquer les autres processus.

2. L'attente infinie : Si plusieurs processus sont en compétition pour entrer en SC, le choix de
l'un d'eux ne doit pas être repoussé indéfiniment. Autrement dit, la solution proposée doit
garantir que tout processus n'attend pas indéfiniment.

3. Deux processus ne peuvent être en même temps dans leurs sections critiques.

4. Tous les processus doivent être égales vis à vis de l'entrée en SC.

Solutions pour réaliser une exclusion mutuelle :

➢ Attente active

Utiliser une variable de verrouillage partagée verrou, unique, initialisée a 0. Pour rentrer en section critique,
un processus doit tester la valeur de verrou

• Si elle est égale à 0, le processus modifie la valeur du verrou ¬1 et exécute sa


section critique. A la fin de la section critique, il remet le verrou a 0.

• Sinon, il attend (par une attente active) que le verrou devienne égal à 0, c.-à-d. :
while(verrou ≠0);

Problèmes : Cette méthode n'assure pas l'exclusion mutuelle

❑ Si un processus P1 est suspendu juste après avoir lu la valeur du verrou qui est égal
à 0 (par exemple : mov verrou).
9

❑ Ensuite, si un autre processus P2 est élu et il teste le verrou qui est toujours égal à 0,
met verrou ¬1 et entre dans sa section critique.

❑ Si P2 est suspendu avant de quitter la section critique et que P1 est réactivé et entre
dans sa section critique et met le verrou¬1

Résultats : Les deux processus P1 et P2 sont en même temps en section critique.

Susceptible de consommer du temps en bouclant inutilement.

II.2 Sémaphore
Pour contrôler les accès a un objet partage, Dijkstra (en 1965) a suggéré l'utilisation d'un nouveau type de
variables appelées les sémaphores.

Définition : Un sémaphore S est un ensemble de deux variables :

1. Une valeur (ou compteur) entier notée value désigne le nombre d’autorisations
d’accès à une section critique. Cette valeur est manipulable au moyen des opérations P (ou
wait) et V (ou signal) ;

Une file F des processus en attente.

L'opération P(S) décrémente la valeur du sémaphore S. Puis, si cette dernière est inférieure à 0 alors le
processus appelant est mis en attente. Sinon le processus appelant accède à la section critique.

L'opération V(S) incrémente la valeur du sémaphore S. Puis si cette valeur est supérieure ou égale à 0 alors
l'un des processus bloqués par l'opération P(S) sera choisi et redeviendra prêt.

L'existence d'un mécanisme de file d'attente (FIFO ou LIFO), permettant de mémoriser le nombre et les
demandes d'opération P(S) non satisfaites et de réveiller les processus en attente.
10

Remarque 1 : Le test du sémaphore, le changement de sa valeur et la mise en attente éventuelle sont


effectués en une seule opération atomique indivisible.

Remarque 2 : La valeur initiale du champ value d'un sémaphore doit être un nombre non négatif. La valeur
initiale d'un sémaphore est le nombre d'unités de ressource.

Ainsi, on peut proposer un schéma de synchronisation de n processus voulant entrer simultanément en SC,
en utilisant les deux opérations P(S) et V(S).

En effet, il suffit de faire partager les n processus un sémaphore S , initialise a 1, appelé sémaphore
d'exclusion mutuelle.

Chaque processus Pi a la structure suivante :

Interblocage :

C’est un problème conduisant un ensemble de processus, lors de son exécution, à un état de blocage où
chaque processus est en attente d’une condition qui ne deviendra jamais vrai.

Exemple 1:

Processus P1{ Processus P2{

Allocation de la ressource R1 ; Allocation de la ressource R2 ;

Allocation de la ressource R2 ; Allocation de la ressource R1 ;

Utilisation de R1 et de R2 ; Utilisation de R1 et de R2 ;

} }

Séquence d’exécution conduisant à un interblocage :

R1 est alloué au processus P1; R2 est alloué au processus P2 ;

=> P1 est en attente de R2 utilisée par P2 , P2 est en attente de la ressource R1 utilisée par P1.

Exemple 2 :

Sémaphore S1, S2 initialisés à 0, 0 ;

Processus P1{ Processus P2{

P(S2) ; P(S1) ;

// SC // SC

V(S1);} V(S2);}
11

=> P1 est en attente de sa libération par P2. De même P2 est attente sa sortie de F par P1.

Précédence des tâches :

Dans quelques applications, notamment les applications temps réel, certaines tâches (processus) doivent
avoir une relation de précédence : une tâche ne peut entamer son exécution que lorsque d’autres tâches ont
terminé leurs exécutions.

Considérons deux processus P1 et P2 exécutent respectivement deux instructions

S1 = A+2 B ; et S2 = S1+10.

Si nous souhaitons que S2 ne doit s'exécuter qu'après l'exécution de S1, nous pouvons implémenter ce
schéma en faisant partager P1 et P2 un sémaphore commun S, initialise à 0 et en insérant les primitives P(S)
et V(S) de la façon suivante :

Exemple : On considère un ensemble de six tâches séquentielles {A, B, C, D, E ,F}. La tâche A doit précéder
les tâches B, C, D. Les tâches B et C doivent précéder la tâche E. Les tâches D et E doivent précéder la tâche
F. Réaliser la synchronisation de ces tâches en utilisant les sémaphores. Solution :

On peut représenter le graphe de précédence des tâches {A,B,C,D,E,F.} comme suit :

La synchronisation des tâches en question peut s’exprimer comme suit :

Sémaphore SA, SB, SC, SD, SE : initialisés à 0,0,0,0,0 ;

Tâche A { Tâche B{ Tâche C {

Exécution ; P(SA) ; P(SA) ;

V(SA) ; V(SA) ; V(SA) ; Exécution ; Exécution ;

} V(SB) ; V(SC) ;

} }
12

Tâche D { Tâche E{ Tâche F {

P(SA) ; P(SB) ; P(SC) ; P(SE) ; P(SD) ;

Exécution ; Exécution ; Exécution ;

V(SD) ; V(SE) ;

} } }

Problèmes classiques de synchronisation de processus :

➢ Lecteur– rédacteur :

Exclusion mutuelle entre lecteurs et rédacteurs : si un lecteur demande à lire (Une base de
données partagée par exemple) et qu’il y a une écriture en cours, la demande est mise en attente. De
même que si un rédacteur demande à écrire et qu'il y a au moins une lecture en cours, la demande est
mise en attente.
Exclusion mutuelle entre rédacteurs : si un rédacteur demande à écrire et qu'il y a une écriture en
cours, la demande est mise en attente.
13

➢ Producteur – consommateur

Le problème de Producteur-Consommateur est un problème de synchronisation classique qui permet de


représenter une classe de situations où un processus, appelé Producteur, délivre des messages (informations)
à un processus consommateur dans un tampon (par exemple, un programme qui envoie des données sur le
spool de l’imprimante).

Le Producteur produit un message dans la ZoneP, puis le dépose dans le buffer. Le Consommateur
prélève un message du Buffer et le place dans la ZoneC où il peut le consommer.

On considérera que le buffer est de N cases, de 0 à N-1. Le Producteur dépose les messages par un bout du
buffer alors que le consommateur les consomme au fur et à mesure par l’autre bout.

Une solution en utilisant un sémaphore si N est illimitée.

Semaphore Mutex = 1 ;
Message tampon[ ];

Consommateur( )
Producteur ( ) { Message m ;
{ Message m ; Tantque Vrai faire
Tantque Vrai faire
m = creemessage() ; Mutex.P() ;
Mutex.P() ; m = lectureTampon() ;
ÉcritureTampon() ; Mutex.V() ;
Mutex.V() ; FinTantque
FinTantque }
}

Adaptation pour que l'échange des message soit synchronisé (consommateurs bloqués si le tampon est vide).
Semaphore Mutex = 1, Plein = 0 ;
Message tampon[ ];

Producteur ( ) Consommateur( )
{ Message m ; { Message m ;
Tantque Vrai faire Tantque Vrai faire
m = creemessage() ; Plein.P() ;
Mutex.P() ; Mutex.P() ;
ÉcritureTampon() ; m = lectureTampon() ;
Mutex.V() ; Mutex.V() ;
Plein.V() ; FinTantque
FinTantque }
}
14

Si N est limitée, la solution doit prendre en compte le cas où le buffer est plein

Semaphore Mutex = 1, Plein = 0 , Vide = N;


Message tampon[ ];
int Depot = 0, info = 0

Consommateur( )
Producteur ( ) { Message m ;
{ Message m ; Tantque Vrai faire
Tantque Vrai faire Plein.P() ;
m = creemessage() ; Mutex.P() ;
Vide.P() ; m = lectureTampon(info) ;
Mutex.P() ; info = info + 1 mod N ;
ÉcritureTampon(depot) ; Mutex.V() ;
depot = depot + 1 mod N Vide.V() ;
Mutex.V() ; FinTantque
Plein.V() ; }
FinTantque
}

➢ Les 5 Philosophes

Cinq philosophes sont assis sur des chaises autour d’une table ronde pour philosopher et manger des
spaghettis. Sur la table sont disposées cinq assiettes, cinq fourchettes et un plat de spaghettis qui est toujours
plein.

Chaque philosophe passe son temps à penser puis manger. Pour manger il doit utiliser les deux fourchettes
situées de part et d’autre de son assiette. Après avoir mangé, le philosophe repose les deux fourchettes sur la
table et se remet à penser. Et ainsi de suite.

C'est-à-dire que le schéma d’un philosophe est :


15

Philosophe i (i=0,4)

Début

Cycle

Penser

Manger /* nécessite deux fourchettes */

FinCycle

Fin

La solution à écrire doit éviter l’interblocage.

Solution :

typedef enum{ pensif,attendant,mangeant} e_ph ;

e_ph etat[5] = {pensif, pensif, pensif, pensif, pensif}; // de 0..4

Sémaphore S[5] init (0,0,0,0,0) ;

Sémaphore mutex init 1 ;

int Droite(int i){ if (i == 0) return 4; else return i-1;}

int Gauche(int i) { return ((i+1) % N);}

Processus Philosophe i // 5 threads de 0 à 4

Début

Cycle

<Penser>

P(mutex)

Si (etat[Droite(i)]=mangeant) ou (etat[Gauche(i)]=mangeant)

Alors

etat[i] := attendant

V(mutex)

P(S[i])

Sinon

etat[i] := mangeant

V(mutex)
16

Finsi

<Manger>

P(mutex)

etat[i] := pensif

si (etat[Droite(i)]=attendant) et (etat[Droite(Droite(i))]<>mangeant) alors

etat[Droite(i)] := mangeant

V(S[Droite(i)])

Si (etat[Gauche(i)]=attendant) et (etat[Gauche(Gauche(i))]<>mangeant) alors

etat[Gauche(i)] := mangeant

V(S[Gauche(i)])

Finsi

V(mutex)

FinCycle

Fin

Utilisation des sémaphores avec le langage C-POSIX :

La librairie POSIX expose plusieurs fonctions aux utilisateurs concernant l’utilisation des sémaphores.

Une implémentation des sémaphores dans la bibliothèque <semaphore.h> se compose en quatre fonctions :

• int sem_init(sem_t *sem, int pshared, unsigned int value) : une fonction d'initialisation qui
permet de créer le sémaphore et de lui attribuer une valeur initiale nulle ou positive.
• int sem_destroy(sem_t *sem) : une fonction permettant de détruire un sémaphore et de
libérer les ressources qui lui sont associées.
• int sem_post(sem_t *sem) : une fonction post qui est utilisée par les threads pour
modifier la valeur du sémaphore. S'il n'y a pas de thread en attente dans la queue
associée au sémaphore, sa valeur est incrémentée d'une unité. Sinon, un des threads en
attente est libéré et passe à l'état prêt(Ready).
• int sem_wait(sem_t *sem) : une fonction qui est utilisée par les threads pour tester la
valeur d'un sémaphore. Si la valeur du sémaphore est positive, elle est décrémentée
d'une unité et la fonction réussit. Si le sémaphore a une valeur nulle, le thread est
bloqué jusqu'à ce qu'un autre thread le débloque en appelant la fonction post.
17

Exemple d’utilisation pour le problème des philosophes :


#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <ctype.h>
#include <semaphore.h>
#include <errno.h>

#define N 5 /* Nombre de philosophes */

typedef enum{Pense, Mange, Attend} T_etat;


T_etat e[N];

sem_t S[N]; // sémaphore privé, sémaphore pour les ressources fourchettes (1 par philosophe)
sem_t mutex; // sémaphore public, sémaphore pour changement d'état (1 pour tous les philosophes)
pthread_t th[N]; // threads philosophes
int cycles = 0;
int GAUCHE(int i){ if (i == 0) return 4; else return i-1;}
int DROITE(int i) { return ((i+1) % N);}

void * philosophe(void * arg) // fonctions d'exe des threads


{ int j = (int) *((int*) arg);

//cast du paramètre

while (1) // à Faire


{
// Pense et va Manger (prendre fourchettes) ou Attendre
sem_wait(&mutex); // bloque, décrémente le semaphore public (= 0)
if ((e[GAUCHE(j)] == Mange) || (e[DROITE(j)] == Mange))
{
e[j] = Attend;
printf(" %d : Attend \n",j);

sem_post(&mutex);
sem_wait(&S[j]);
}else
{
e[j]= Mange;
sem_post(&mutex);
}

printf(" %d : Mange \n",j);


sleep(1);
sem_wait(&mutex);
e[j] = Pense;

if ((e[GAUCHE(j)]== Attend ) && (e[GAUCHE(GAUCHE(j))] != Mange))


{
18

e[GAUCHE(j)] = Mange ;
sem_post(&S[GAUCHE(j)]);
}
else if ((e[DROITE(j)]== Attend ) && (e[DROITE(DROITE(j))] != Mange))
{
e[DROITE(j)] = Mange ;
sem_post(&S[DROITE(j)]);
}

sem_post(&mutex);
}
}

int main ()
{
int i;

for(i=0; i<N; i++){


e[i] = Pense; // au début TOUS les philosophes pensent
sem_init(&S[i], 0, 0); // initialisation du sémaphore privé (=1)
}

sem_init(&mutex, 0, 1); // initilialisation du sémaphore public (=1)

for (i = 0; i<N; i++)


{
printf("\nCreation du Thread Philosophe %d\n", i);
pthread_create(&th[i], NULL, philosophe, (void *) &i); // création des threads philosophes
}

// afficher(); // on affiche l'état des philosophes

for(i=0; i<N; i++){


pthread_join(th[i], NULL); // attend fin des threads
sem_destroy(&S[i]);
}

return 0;
}

Utilisation des sémaphores avec le langage JAVA :


import java.util.concurrent.*;
public class ex2
{
public static void main(String args[]) throws InterruptedException
{
// création d’un sémaphore avec nombre d’accès = 1
Semaphore sem = new Semaphore(1);
sem.acquire(); // P(sem)
//SC
sem.release(); // V(sem)
}
}

II.3 Les Moniteurs

L'idée des moniteurs, inventé en 1973, est de regrouper dans un module spécial, appelé moniteur, toutes les
sections critiques d'un même problème.
19

Un moniteur est un ensemble de procédures, de variables et de structures de données.

Les processus peuvent appeler (ou utiliser) les procédures du moniteur mais ils ne peuvent pas accéder aux
variables et aux structures de données interne du moniteur à partir des procédures externes.

C’est le compilateur qui assure l'accès aux ressources critiques en exclusion mutuelle. Il y ait à tout instant
pas plus d'un processus actif dans le moniteur, c'est-a-dire que les procédures du moniteur ne peuvent être
exécutées que par un seul processus à la fois.

Pour cela, le compilateur rajoute, au début de chaque procédure du moniteur un code qui réalise ce qui suit :

➢ S'il y a un processus P1 actif dans le moniteur, alors le processus P2 appelant est suspendu
jusqu'àce que le processus P1 se bloque en exécutant un wait sur une variable conditionnelle
(wait(c)) ou quitte le moniteur.

➢ Le processus bloqué est réveillé par un autre processus en lui envoyant un signal sur la
variable conditionnelle (signal(c)).

Une variable de condition (des moniteurs) est une condition manipulée au moyen de deux opérations wait et
signal :

wait(x) :

❑ Suspend l'exécution du processus (thread) appelant (le met en attente de x) ;

❑ Autorise un autre processus en attente du moniteur à y entrer.

signal(x) : débloqué un processus en attente de la condition x.

L'utilisation de moniteurs est plus simple que les sémaphores puisque le programmeur n'a pas à se
préoccuper de contrôler les accès aux sections critiques.

Mais, malheureusement, a l'exception de JAVA (avec le mot clé synchronized) et C#, la majorité des
compilateurs utilisés actuellement ne supportent pas les moniteurs. Par contre, on peut simuler les moniteurs
avec d'autres langages à partir des mutex comme suit :

• Le moniteur contient un sémaphore binaire (par exemple : mutex)

• Toutes les procédures commencent par l'acquisition du mutex et finissent par sa libération.

Exemple 1 : Producteur/consommateur

❑ Les sections critiques du problème du producteur et du consommateur sont les opérations de


dépôt et de retrait dans le tampon partagé.

❑ Le consommateur est actif dans le moniteur et le tampon est vide => il devrait se mettre en
attente et laisser place au producteur.

❑ Le producteur est actif dans le moniteur et le tampon est plein => il devrait se mettre en
attente et laisser place au consommateur.
20

Moniteur ProducteurConsommateur (){

bool nplein, nvide ; //variable de condition pour non plein et non vide

int compteur =0, ic=0, ip=0, N ;

void mettre (int objet) // section critique pour le void retirer (int* objet) //section critique pour le
dépôt retrait

{ {

if (compteur==N) wait(nplein) ; //attendre if (compteur ==0) wait(nvide) ; objet = tampon[ic] ;


jusqu'a ce que le tampon soit non plein
ic = (ic+1)%N ; compteur - ;
tampon[ip] = objet ;
// si le tampon était plein, envoyer un signal pour
ip = (ip+1)%N ; réveiller le producteur.

compteur++ ; if(compteur==N-1) signal(nplein) ;

// si le tampon était vide, envoyer un signal pour }


réveiller le consommateur.
}
if (compteur==1) signal(nvide) ;

Règles d'utilisation des moniteurs


An d‘éviter que tous les processus réveillés se retrouvent au même temps dans le moniteur, différentes
règles ont été établies pour définir ce qui se passe a l'issue d'un signal.

1. Règle de Hoare : ne laisser entrer dans le moniteur que le processus qui a été suspendu le
moins longtemps;

2. Regle de Brinch Hansen : exiger du processus qui fait Signal de sortir immédiatement du
moniteur. Il laisse ainsi la place a tous ceux qui étaient en attente. L'ordonnanceur choisira
un parmi ceux ci. Si un Signal est réalise sur une variable conditionnelle et qu'aucun
processus ne l'attend, alors ce signal est perdu.
21

Application au problème des philosophes :


Moniteur diner_phylosophes() {

typedef enum{ pensif, attendant, mangeant} e_ph ;

e_ph etat[5] = {pensif, pensif, pensif, pensif, pensif}; // de 0..4

bool philo[5] of condition;

int Droite(int i){ if (i == 0) return 4; else return i-1;}

int Gauche(int i) { return ((i+1) % N);}

void Prendre_Baguette(i){

etat[i] = attendant;

if((etat[gauche(i) != mangeant) and et (etat[Droite(i) != mangeant))

Etat[i] = mangeant ;

}else

Wait(philo[i]) ;

void Poser_Baguette(i){

etat[i] = pensif;

if((etat[gauche(i) == attendant) and et (etat[Gauche(Gauche(i))] != mangeant))

Etat[Gauche(i) ] = mangeant ;

Signal(philo[Gauche(i)]

if((etat[Droite(i) == attendant) and et (etat[Droite(Droite(i))] != mangeant))

Etat[Droite(i) ] = mangeant ;

Signal(philo[Droite(i)]
22

} // end Moniteur

Processus Philosophe( i) //

Cycle

Penser ;

Prendre_Baguette(i)

<Manger> ;

Poser_Baguette(i) ;

Fincycle

Exemple Moniteur C-POSIX:

IV Exercices :
Exercice 1 :

Soit un processus Ps et N processus P i (i=1..N). Les processus P i (i=1..N) remplissent une zone tampon
pouvant contenir M messages dont un seul à la fois étant autorisé à déposer son message.
23

Le processus P i qui remplit la dernière case du tampon active le processus P s qui fait alors l'impression de
tous les messages déposés dans le tampon. Durant cette impression, les processus P i (i=1..N) ne sont pas
autorisés à accéder au tampon.

Ecrire les algorithmes des processus P i (i=1..N) et P s .

Solution :

Si on note : nm = nombre de messages contenus dans le tampon, le schéma des processus considérés

peut s’exprimer comme suit :


Processus Pi ( i = 1..N ) Processus Ps

Début Début

<Fabriquer un message> Cycle

<si le tampon est occupé <attendre jusqu’à être

alors attendre sinon réveillé par un des

bloquer l’accès au tampon> processus Pi>

<Déposer le message> <Imprimer tous les messages>

nm := nm + 1 nm := 0

<si nm=M alors activer Ps <libérer l’accès au tampon>

sinon libérer l’accès au FinCycle

tampon> Fin

Fin

On peut assimiler l’attente d’un processus au blocage de celui-ci dans une file de sémaphore.

Soit Spriv un sémaphore privé au processus Ps qui y se bloquera en attente d’être réveillé ;

et mutex un sémaphore d’exclusion mutuelle pour l’accès au tampon.

On peut écrire les algorithmes des processus Pi et Ps comme suit :

var Spriv,mutex : sémaphore init 0,1 ;

nm : entier init 0 ;

Processus Pi ( i = 1..N Processus Ps

Début Début

<Fabriquer un message> Cycle

P(mutex) P(Spriv)
24

<Déposer le message> <Imprimer tous les messages>

nm := nm + 1 nm := 0

si nm=M alors V(Spriv) V(mutex)

sinon V(mutex) FinCycle

finsi Fin

Fin

Exercice 2 :

Dans une base de données, afin de conserver une certaine cohérence, on ne peut pas

– lire pendant une écriture

– écrire pendant une écriture

– écrire pendant une lecture

Les utilisateurs d'une base de données utiliseront les fonctions lecteur() et ecrivain() respectivement pour lire
et écrire dans la base de données.

lecteur( ){

debut_lire( );

lire(BD);

fin_lire( );

ecrivain( ){

debut_ecrire( );

ecrire(BD);

fin_ecrire( );

Donner la solution algorithmique en utilisant des moniteurs :

1. Priorité au lecteur de la base de données


2. Priorité au rédacteur de la base de données.

On se place dans le cas où l'on donne la priorité aux lecteurs de la base de données.

Il s'agit donc d'implémenter les fonctions debut_lire, fin_lire, debut_ecrire et fin_ecrire

On utilise un moniteur comportant ces fonctions ainsi que les conditions :

cond f_lect , f_ecr

l'entier
25

int nb_lec

qui indique le nombre de lecteurs dans la base de données, et enfin le booléen :

bool ecr

qui indique si quelqu'un est en train décrire dans la base de données ou non.

debut_lire( ){

nb_lec ++;

if (ecr){ //s'il y a un écrivain on attend

f_lect.wait( );

f_lect.signal( ); // si on peut lire alors tous ceux qui sont en attente aussi

fin_lire( ){

nb_lec - - ;

if (nb_lec ==0){

f_ecr.signal( );

debut_ecrire( ){

if (nb_lec > 0 || ecr){

f_ecr.wait( );

ecr = true;

fin_ecrire( ){

ecr = false;

if ( nb_lec > 0){ //priorité aux lecteurs : on regarde d'abord si il y en a qui veulent lire

f_lect.signal( );

else {

f_ecr.signal( );
26

On se place maintenant dans le cas où ce sont les écrivains qui ont la priorité

On utilise un moniteur comportant :

int nb_lec_base // compte le nombre de lecteurs dans la base de données

int nb_lec_att

// compte le nombre de lecteurs en attente d'une lecture dans la BD

int nb_ecr_att

// compte le nombre d'écrivains en attente d'écriture

bool ecr_base

// indique si on est en train d'écrire dans la base

cond f_lect , f_ecr

debut_lire( ){

nb_lec_att ++;

if (ecr_base || nb_ecr_att > 0){ //priorité aux écrivains

f_lect.wait( );

nb_lec_att --;

l_lect.signal( ); //pareil si on peut lire alors tous les autre en attente de lecture aussi

nb_lec_base ++;

fin_lire( ){

nb_lec_base --;

if (nb_lec_base == 0){ //on réveille 1 écrivain

f_ecr.signal( );

debut_ecrire( ){

nb_ecr_att ++;

if (nb_lec_base > 0 || ecr_base){


27

f_ecr.wait( );

ecr_base = true;

nb_ecr_att --;

fin_ecrire( ){

ecr_base = false;

if ( nb_ecr_att >0){

f_ecr.signal( );

elsif ( nb_lec_att > 0){

f_lec.signal( );

Vous aimerez peut-être aussi