Resumé Cours
Resumé Cours
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 Coefficient : 3, Crédit : 5
Plan du cours
1. Introduction
Introduction aux S. E
Processus et threads
2. Synchronisation
· Synchronisation
o Evénements, Verrous
o Sémaphores
o Moniteurs
3. Communication
4. Interblocage
2
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).
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.
• Gestion des processeurs : Gérer l'allocation des processeurs entre les différents
• 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.
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.
4. Les valeurs des registres lors de la dernière suspension (CO, sommet de Pile...) ;
5. Une priorité ;
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....
Prêt : le processus est placé dans la file d’attente des processus prêts, en attente d’affectation du
processeur.
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.
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
#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:
#include <unistd.h>
#include <stdio.h>
void main()
int p_id1,p_id2;
p_id1 = fork();
5
if (p_id1 == 0)
p_id2 = fork() ;
p_id2 = fork() ;
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*);
L'appel renvoie 0 s'il réussit, sinon il renvoie une valeur non nulle identifiant l’erreur qui s'est produite
• Attend la fin d’un thread. L’équivalent de waitpid des processus sauf qu’on doit spécifier le tid du
thread à attendre.
pthread_t pthread_self(void);
Exemple :
#include <stdio.h> {
return 0 ; return 0;
} }
for(i=0;i<6;i++) {
7
n++;
sleep(1);
return 0;
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
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.
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.
➢ 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
• Sinon, il attend (par une attente active) que le verrou devienne égal à 0, c.-à-d. :
while(verrou ≠0);
❑ 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
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.
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) ;
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 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.
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:
Utilisation de R1 et de R2 ; Utilisation de R1 et de R2 ;
} }
=> P1 est en attente de R2 utilisée par P2 , P2 est en attente de la ressource R1 utilisée par P1.
Exemple 2 :
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.
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.
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 :
} V(SB) ; V(SC) ;
} }
12
V(SD) ; V(SE) ;
} } }
➢ 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 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.
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
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.
Philosophe i (i=0,4)
Début
Cycle
Penser
FinCycle
Fin
Solution :
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
etat[Droite(i)] := mangeant
V(S[Droite(i)])
etat[Gauche(i)] := mangeant
V(S[Gauche(i)])
Finsi
V(mutex)
FinCycle
Fin
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
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);}
//cast du paramètre
sem_post(&mutex);
sem_wait(&S[j]);
}else
{
e[j]= Mange;
sem_post(&mutex);
}
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;
return 0;
}
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
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) :
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 :
• Toutes les procédures commencent par l'acquisition du mutex et finissent par sa libération.
Exemple 1 : Producteur/consommateur
❑ 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
bool nplein, nvide ; //variable de condition pour non plein et non vide
void mettre (int objet) // section critique pour le void retirer (int* objet) //section critique pour le
dépôt retrait
{ {
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
void Prendre_Baguette(i){
etat[i] = attendant;
Etat[i] = mangeant ;
}else
Wait(philo[i]) ;
void Poser_Baguette(i){
etat[i] = pensif;
Etat[Gauche(i) ] = mangeant ;
Signal(philo[Gauche(i)]
Etat[Droite(i) ] = mangeant ;
Signal(philo[Droite(i)]
22
} // end Moniteur
Processus Philosophe( i) //
Cycle
Penser ;
Prendre_Baguette(i)
<Manger> ;
Poser_Baguette(i) ;
Fincycle
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.
Solution :
Si on note : nm = nombre de messages contenus dans le tampon, le schéma des processus considérés
Début Début
nm := nm + 1 nm := 0
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é ;
nm : entier init 0 ;
Début Début
P(mutex) P(Spriv)
24
nm := nm + 1 nm := 0
finsi Fin
Fin
Exercice 2 :
Dans une base de données, afin de conserver une certaine cohérence, on ne peut pas
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( );
On se place dans le cas où l'on donne la priorité aux lecteurs de la base de données.
l'entier
25
int nb_lec
bool ecr
qui indique si quelqu'un est en train décrire dans la base de données ou non.
debut_lire( ){
nb_lec ++;
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( ){
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é
int nb_lec_att
int nb_ecr_att
bool ecr_base
debut_lire( ){
nb_lec_att ++;
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 --;
f_ecr.signal( );
debut_ecrire( ){
nb_ecr_att ++;
f_ecr.wait( );
ecr_base = true;
nb_ecr_att --;
fin_ecrire( ){
ecr_base = false;
if ( nb_ecr_att >0){
f_ecr.signal( );
f_lec.signal( );