Vous êtes sur la page 1sur 229

Concepts des systèmes

d’exploitation et mise en oeuvre


sous Unix

Coordonnateur : François Trahay

module CSC4508/M2

Avril 2016
Concepts des systèmes d’exploitation et mise en oeuvre sous Unix

TELECOM SudParis — Coordonnateur : François Trahay — Avril 2016 — module CSC4508/M2 0


Concepts des systèmes d’exploitation et mise en oeuvre sous Unix

Contents
Licence ix

Présentation du cours 1

Plan du document 2

1 Objectifs du cours 2

2 Public visé et pré-requis 3

3 Déroulement 4

Introduction : rôle d’un système d’exploitation 5

Plan du document 6

1 Systèmes informatiques 6

2 Machine virtuelle 6

3 Objectifs d’un système d’exploitation 7

4 Différents types de systèmes d’exploitation 7


4.1 Temps réel souple versus temps réel strict . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
4.2 Caractéristiques des applications temps réel . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
4.3 Transactionnel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9

Interactions entre système multi-tâche et processus 11

Plan du document 12

1 Point de vue processus 12


1.1 Les applications et le système . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
1.2 La programmation système . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
1.3 Les fonctions de la libc . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
1.4 Utilisation des appels systèmes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
1.5 Utilisation des fonctions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
1.6 Test du retour des appels système et des fonctions . . . . . . . . . . . . . . . . . . . . . . . 15
1.7 Que faire en cas d’erreur système ? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16

2 Point de vue système 18


2.1 Généralités sur l’exécution des tâches système . . . . . . . . . . . . . . . . . . . . . . . . . . 18
2.2 Traitement des appels système . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
2.3 Prise en compte des interruptions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20

3 Ordonnancement des processus sous Linux 21


3.1 Priorité statique . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
3.2 Politique d’ordonnancement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
3.3 Ordonnancement temps-réel Linux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
3.3.1 Inversion de priorité : énoncé du problème . . . . . . . . . . . . . . . . . . . . . . . . 24
3.3.2 Inversion de priorité : exemple de solution . . . . . . . . . . . . . . . . . . . . . . . . 24
3.4 Ordonnancement standard . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24

Gestion de la mémoire 27

Plan du document 28

TELECOM SudParis — Coordonnateur : François Trahay — Avril 2016 — module CSC4508/M2 i


Concepts des systèmes d’exploitation et mise en oeuvre sous Unix

1 Point de vue système 28


1.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
1.2 Besoins des processus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
1.3 Pagination . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
1.3.1 Généralités . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
1.3.2 Adresse logique . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
1.3.3 Table des pages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
1.3.4 Accélérateurs (caches d’adresses) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
1.3.5 Écroulement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
1.4 Segmentation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
1.4.1 Généralités . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
1.4.2 Descripteur de segments . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
1.5 Pagination versus Segmentation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
1.6 Algorithmes pour la gestion des pages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
1.6.1 Chargement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
1.6.2 Remplacement (déchargement) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36

2 Point de vue processus 37


2.1 Espace d’adressage d’un processus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
2.2 Observation et contrôle de l’utilisation de la mémoire . . . . . . . . . . . . . . . . . . . . . 38
2.2.1 Observation de l’utilisation des pages . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
2.2.2 Observation du type d’accès à la mémoire . . . . . . . . . . . . . . . . . . . . . . . . 39
2.2.3 Être attentif à l’alignement des structures . . . . . . . . . . . . . . . . . . . . . . . . 41
2.2.4 Contrôle des défauts de page . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
2.3 Allocation/Désallocation dynamique de mémoire . . . . . . . . . . . . . . . . . . . . . . . . 44
2.3.1 Allocation/Désallocation mémoire standard . . . . . . . . . . . . . . . . . . . . . . . 44
2.3.2 Digression : algorithmes pour malloc . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
2.3.3 Digression : algorithmes pour free . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
2.3.4 Désallocation automatique avec alloca . . . . . . . . . . . . . . . . . . . . . . . . . 47
2.3.5 Mécanisme d’allocation/désallocation dédié . . . . . . . . . . . . . . . . . . . . . . . 48
2.3.6 Allocation/Désallocation mémoire au niveau système . . . . . . . . . . . . . . . . . . 48
2.4 Déverminage des accès mémoire . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
2.4.1 Détecter statiquement les accès erronés à des zones allouées dynamiquement . . . . . 49
2.4.2 Détecter dynamiquement les accès erronés à des zones allouées dynamiquement . . . 51
2.4.3 Visualiser quand une zone mémoire est accédée/modifée . . . . . . . . . . . . . . . . 53

Bibliographie du chapitre 54

Les fichiers (et les entrées-sorties) 55

Plan du document 56

1 Primitives Unix d’entrées-sorties 57


1.1 Primitives de base . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
1.1.1 Ouverture/fermeture de fichier . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
1.1.2 Lecture sur descripteur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
1.1.3 Écriture sur descripteur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
1.2 Duplication de descripteur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
1.3 Contrôle des entrée-sorties . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
1.3.1 Primitive fcntl . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
1.3.2 Verrouillage de fichier . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
1.3.3 Conseil au noyau pour les lectures . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
1.4 Manipulation de l’offset . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
1.5 Gestion de la lenteur ou du blocage des entrées-sorties* . . . . . . . . . . . . . . . . . . . . 65

TELECOM SudParis — Coordonnateur : François Trahay — Avril 2016 — module CSC4508/M2 ii


Concepts des systèmes d’exploitation et mise en oeuvre sous Unix

2 Bibliothèque C d’entrées-sorties 66
2.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
2.2 Ouverture/fermeture de fichier . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
2.3 Lecture de fichier . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
Lecture de fichier (suite) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
2.4 Écriture de fichier . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
Écriture de fichier (suite) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
2.5 Contrôle du tampon . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
2.6 Divers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71

3 Projection des fichiers en mémoire 72

4 Manipulation des i-noeuds du système de fichiers Unix 73


Manipulation des i-noeuds du système de fichiers Unix (suite) . . . . . . . . . . . . . . . . . . . 74

5 Entrées-sorties sur répertoires 74

6 Limitations de NFS 75

Bibliographie du chapitre 76

Communications inter-processus 77

Plan du document 78

1 Gestion des processus 78


1.1 Environnement des processus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
1.2 Informations concernant un processus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
1.3 Création de processus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
1.4 Terminaison de processus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
1.5 Exécution d’un nouveau programme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83

2 Communications à l’aide de tubes 84


2.1 Principe des tubes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
2.2 Tubes ordinaires (ou locaux) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
2.3 Tubes nommés . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87

3 Communication à l’aide des IPC POSIX 89


3.1 Inter Process Communication POSIX . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
3.2 IPC System V versus POSIX IPC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
3.3 Files de messages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
3.4 Mémoire partagée . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
3.5 Sémaphores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
3.5.1 Introduction aux sémaphores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
3.5.2 Introduction aux sémaphores : analogie . . . . . . . . . . . . . . . . . . . . . . . . . 98
3.5.3 Introduction aux sémaphores : algorithmes P ET V . . . . . . . . . . . . . . . . . . . 99
3.5.4 Sémaphores POSIX : initialisation . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
3.5.5 Sémaphores POSIX : utilisation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100

4 Comparaison des mécanismes de synchronisation 102

Synchronisation entre processus 105

Plan du document 106

1 Introduction 106
1.1 Correspondance problèmes vie courante/informatique . . . . . . . . . . . . . . . . . . . . . 106

TELECOM SudParis — Coordonnateur : François Trahay — Avril 2016 — module CSC4508/M2 iii
Concepts des systèmes d’exploitation et mise en oeuvre sous Unix

2 Sémaphore = Outil de base 107


2.1 Généralités . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107
2.2 Analogie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108
2.3 Algorithmes P ET V . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109

3 Résolution de problèmes de synchronisation typiques 109


3.1 Exclusion mutuelle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110
3.2 Cohorte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111
3.3 Passage de témoin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111
3.3.1 Envoi de signal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111
3.3.2 Rendez-vous entre deux processus . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112
3.3.3 Appel procédural entre processus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113
3.4 Producteurs/Consommateurs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113
3.4.1 Objectif . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114
3.4.2 Principe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114
3.4.3 Déposer et extraire . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115
3.4.4 Exemple . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115
3.4.5 K producteurs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117
3.4.6 Exemple de problème avec 2 producteurs . . . . . . . . . . . . . . . . . . . . . . . . 117
Exemple de problème avec 2 producteurs (suite) . . . . . . . . . . . . . . . . . . . . . . . 117
3.4.7 Solution complète . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119
3.5 Lecteurs/rédacteurs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119
3.5.1 Objectif . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119
3.5.2 Solution de base . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 120
3.5.3 Analyse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 120
3.5.4 Solution avec priorités égales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121

4 Interblocage 122
4.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122
4.2 Généralités . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122

5 Mise en oeuvre dans un système d’exploitation 123

Bibliographie du chapitre 124

Threads ou processus légers 125

Plan du document 126

1 Présentation 126
1.1 Bibliographie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126
1.2 Threads . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127
1.3 Détacher le flot d’exécution des ressources . . . . . . . . . . . . . . . . . . . . . . . . . . . . 128
1.3.1 Vision traditionnelle d’un processus . . . . . . . . . . . . . . . . . . . . . . . . . . . 128
1.3.2 Autre vision d’un processus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129
1.3.3 Processus multi-thread . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129

2 Création/destruction de threads 130


2.1 Pthread “Hello world” . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130
2.2 Ensemble de threads pairs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131
2.3 Threads POSIX: création/destruction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132
2.3.1 Identification . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132
2.3.2 Utilisation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133
2.3.3 Attributs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134
Attributs (2/2) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134

TELECOM SudParis — Coordonnateur : François Trahay — Avril 2016 — module CSC4508/M2 iv


Concepts des systèmes d’exploitation et mise en oeuvre sous Unix

3 Partage des données 135


3.1 Notion de variable partagée . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136
3.2 Partage non-intentionnel des données . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136
3.3 Code réentrant et thread-safe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 138
3.4 Thread-Local Storage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 141

4 Synchronisation 143
4.1 Synchronisation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143
4.2 Exclusions mutuelles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144
Exclusions mutuelles (2/2) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144
4.3 Sémaphores POSIX (rappel) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146
4.4 Attente de conditions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148
Attente de conditions (2/2) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148

5 Utilisation et limitations des threads 153


5.1 Utilisation des threads . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153
5.2 Limitations des threads . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155

6 Autres fonctions de la bibliothèque POSIX threads 156


6.1 Annulation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157
Annulation (2/2) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159
6.2 Nettoyage des ressources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159
6.3 Initialisation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 160

Architecture 161

Plan du document 162

1 Introduction 162
1.1 Loi de Moore . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 162

2 Processeur séquentiel 163

3 Pipeline 163
3.1 Micro architecture d’un pipeline . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 164
3.2 Processeurs superscalaires . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 164
3.3 Processeurs superscalaires . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165
3.4 Dépendance entre instructions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165
3.5 Gestion des branchements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166
3.6 Prédiction de branchement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166
3.7 Instructions vectorielles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 167

4 Parallel Processing 168


4.1 Hyperthreading / SMT . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 169
4.2 Processeurs multi-cœurs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 169
4.3 Architectures SMP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 170
4.4 Architectures NUMA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 170

5 Hiérarchie mémoire 171


5.1 Enjeux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171
5.2 Caches . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 172
5.3 Memory Management Unit (MMU) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173
5.3.1 Fully-associative caches . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 174
5.3.2 Direct-mapped caches . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 174
5.3.3 Set-associative caches . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 175
5.3.4 Cohérence de cache . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 175
5.4 Bibliographie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 176

TELECOM SudParis — Coordonnateur : François Trahay — Avril 2016 — module CSC4508/M2 v


Concepts des systèmes d’exploitation et mise en oeuvre sous Unix

Éléments d’architecture client-serveur 177

Plan du document 178

1 Introduction 178
1.1 Définition d’une architecture client/serveur . . . . . . . . . . . . . . . . . . . . . . . . . . . 178
1.2 Objectif de cette présentation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 179
1.3 À propos des communications . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 180

2 Serveur mono-tâche gérant un client à la fois 180


2.1 Principe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 181
2.2 Analyse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 182

3 Serveur avec autant de tâches que de clients 182


3.1 Principe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 182
3.2 Variante . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 184
3.3 Analyse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 184
3.4 Réduction du temps de connexion des clients . . . . . . . . . . . . . . . . . . . . . . . . . . 185

4 Serveur avec N tâches gérant tous les clients 186


4.1 Principe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 186
4.2 Analyse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 187

5 Serveur mono-tâche gérant tous les clients à la fois 187


5.1 Principe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 188
5.2 Aperçu de la programmation événementielle . . . . . . . . . . . . . . . . . . . . . . . . . . . 189
5.3 Analyse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 194

6 Conclusion 196

Bibliographie du chapitre 196

Récapitulatif des outils 197

Plan du document 198

1 Infos sur l’architecture matérielle 198

2 Debugging 198

3 Entrées/Sorties – Bases de données 199

4 IPC 199

5 Traitement de tâches parallèles 200

Bibliographie 201

Plan du document 202

1 Concepts des systèmes d’exploitation 202

2 Ouvrages dédiés à Unix 202

3 Documents dédiés à Linux 203

4 Documents spécifiques threads 203

5 Divers 204

TELECOM SudParis — Coordonnateur : François Trahay — Avril 2016 — module CSC4508/M2 vi


Concepts des systèmes d’exploitation et mise en oeuvre sous Unix

6 Bibliographie 205

Index 209

TELECOM SudParis — Coordonnateur : François Trahay — Avril 2016 — module CSC4508/M2 vii
Concepts des systèmes d’exploitation et mise en oeuvre sous Unix

TELECOM SudParis — Coordonnateur : François Trahay — Avril 2016 — module CSC4508/M2 viii
Concepts des systèmes d’exploitation et mise en oeuvre sous Unix
' $
Licence

Ce document est une documentation libre, placée sous la Licence de Documentation Libre GNU (GNU
Free Documentation License).
Copyright (c) 2003-2016 Frédérique Silber-Chaussumier, Michel Simatic et François
Trahay
Permission est accordée de copier, distribuer et/ou modifier ce document selon les
termes de la Licence de Documentation Libre GNU (GNU Free Documentation License),
version 1.2 ou toute version ultérieure publiée par la Free Software Foundation; avec
#1 les Sections Invariables qui sont ‘Licence’ ; avec les Textes de Première de Couverture
qui sont ‘Concepts des systèmes d’exploitation et mise en oeuvre sous Unix’
et avec les Textes de Quatrième de Couverture qui sont ‘Help’.
Une copie de la présente Licence peut être trouvée à l’adresse suivante :
http://www.gnu.org/copyleft/fdl.html.

Remarque : La licence comporte notamment les sections suivantes : 2. COPIES VERBATIM, 3. COPIES
EN QUANTITÉ, 4. MODIFICATIONS, 5. MÉLANGE DE DOCUMENTS, 6. RECUEILS DE
DOCUMENTS, 7. AGRÉGATION AVEC DES TRAVAUX INDÉPENDANTS et 8. TRADUCTION.

& %
Ce document est préparé avec des logiciels libres :
• LATEX : les textes sources sont écrits en LATEX (http://www.latex-project.org/, le site du Groupe
francophone des Utilisateurs de TEX/LATEX est http://www.gutenberg.eu.org). Une nouvelle classe
et une nouvelle feuille de style basées sur la classe seminar ont été tout spécialement dévélop-
pées: newslide (projet picoforge newslide, http://picoforge.int-evry.fr/projects/slideint)
et slideint (projet picoforge slideint, http://picoforge.int-evry.fr/projects/slideint);
• emacs: tous les textes sont édités avec l’éditeur GNU emacs (http://www.gnu.org/software/emacs);
• dvips: les versions PostScript (PostScript est une marque déposée de la société Adobe Systems Incor-
porated) des transparents et des polycopiés à destination des élèves ou des enseignants sont obtenues
à partir des fichiers DVI (« DeVice Independent ») générés à partir de LaTeX par l’utilitaire dvips
(http://www.ctan.org/tex-archive/dviware/dvips);
• ps2pdf et dvipdfm: les versions PDF (PDF est une marque déposée de la société Adobe Sys-
tems Incorporated) sont obtenues à partir des fichiers Postscript par l’utilitaire ps2pdf (ps2pdf
étant un shell-script lançant Ghostscript, voyez le site de GNU Ghostscript http://www.gnu.org/-
software/ghostscript/) ou à partir des fichiers DVI par l’utilitaire dvipfm;
• makeindex: les index et glossaire sont générés à l’aide de l’utilitaire Unix makeindex
(http://www.ctan.org/tex-archive/indexing/makeindex);
• TeX4ht: les pages HTML sont générées à partir de LaTeX par TeX4ht (http://www.cis.ohio-
-state.edu/~gurari/TeX4ht/mn.html);
• Xfig: les figures sont dessinées dans l’utilitaire X11 de Fig xfig (http://www.xfig.org);
• fig2dev: les figures sont exportées dans les formats EPS (« Encapsulated PostScript ») et PNG
(« Portable Network Graphics ») grâce à l’utilitaire fig2dev (http://www.xfig.org/userman/-
installation.html);
• convert: certaines figures sont converties d’un format vers un autre par l’utilitaire convert
(http://www.imagemagick.org/www/utilities.html) de ImageMagick Studio;
• HTML TIDY: les sources HTML générés par TeX4ht sont « beautifiés » à l’aide de HTML TIDY
(http://tidy.sourceforge.net) ; vous pouvez donc les lire dans le source;
Nous espérons que vous regardez cette page avec un navigateur libre: Mozilla ou Firefox par exemple. Comme
l’indique le choix de la licence GNU/FDL, tous les éléments permettant d’obtenir ces supports sont libres.

TELECOM SudParis — Coordonnateur : François Trahay — Avril 2016 — module CSC4508/M2 ix


Concepts des systèmes d’exploitation et mise en oeuvre sous Unix

TELECOM SudParis — Coordonnateur : François Trahay — Avril 2016 — module CSC4508/M2 x


Concepts des systèmes d’exploitation et mise en oeuvre sous Unix

Avant-propos
Ce cours est préparé et dispensé depuis de nombreuses années par les enseignants de l’Institut Na-
tional des Télécommunications pour le module de deuxième année ASR3 de Télécom INT. Que soient
remerciées ici toutes les personnes qui ont contribuées à ce cours : Christian Bac, Djamel Belaïd, Olivier
Berger, Guy Bernard, Dominique Bouillet, Denis Conan, Daniel Millot, Christian Schüller, Frédérique
Silber-Chaussumier, Michel Simatic et Eric Renault.

Certaines parties de ce cours ne seraient pas sans les nombreux échanges avec Catherine Coquery et
Claude Kaiser du Département Informatique du CNAM : qu’ils en soient ici également remerciés.

Et puis un grand merci aux personnes qui ont permis au logo CSC4508 d’exister : George W. Hart
(http://www.georgehart.com) qui a autorisé l’utilisation de photos de sa sculpture Knot structured pour
ce logo et Steeve Jouannet et Marie-Christine Monget qui ont travaillé à l’intégration de ces photos dans le
logo.

Les transparents et le polycopié ont été réalisés grâce à LaTeX et aux travaux de Philippe Lalevée
et Denis Conan.

TELECOM SudParis — Coordonnateur : François Trahay — Avril 2016 — module CSC4508/M2 xi


Concepts des systèmes d’exploitation et mise en oeuvre sous Unix

TELECOM SudParis — Coordonnateur : François Trahay — Avril 2016 — module CSC4508/M2 xii
Présentation du cours

Michel Simatic

module CSC4508/M2

Avril 2016

1
Présentation du cours
' $
Plan du document

#2 1 Objectifs du cours . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
2 Public visé et pré-requis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
3 Déroulement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5

& %

' $
1 Objectifs du cours

 « Ceux qui sont férus de pratique sans posséder la science sont comme le pilote qui
s’embarquerait sans timon, ni boussole et ne saurait jamais où il va » (Léonard de
Vinci)
 Objectifs
 (Re)découvrir les concepts de base d’un système d’exploitation (processus,
#3
gestion de la mémoire, fichiers, threads)
I Prendre conscience des hypothèses choisies par les concepteurs/développeurs
de système d’exploitation
I Comprendre, en expérimentant sous Linux, les impacts de ces hypothèses
 Étudier les mécanismes de communication entre processus d’une même machine
 Comprendre les problèmes de synchronisation de processus et les patrons de
conception associés

& %

À la place de patron de conception, certains auteurs parlent de paradigme, c’est-à-dire un modèle théorique
de pensée qui oriente la recherche et la réflexion scientifique.
Ce cours fait la transition entre la première et la troisième année :
• 1ère année :

– CSC3002 : Initiation à l’algorithmie, la structuration des données et la programmation C


– CSC3502 : Interactions entre un programme et la machine sur laquelle il s’exécute

• 2è année :
– CSC4508 (partie interaction entre les programmes et le système d’exploitation) : Interactions :

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 2


Présentation du cours

∗ entre un programme et le système d’exploitation de la machine sur laquelle il tourne


∗ entre plusieurs programmes sur la même machine
– CSC4509 : Algorithmique et communications des applications réparties (Interactions entre pro-
cessus sur des machines différentes)
• 3è année :
– Suite de la Voie d’APprofondissement ASR : Prise de hauteur dans les interactions entre processus
sur des machines différentes

' $
2 Public visé et pré-requis

 Public visé
 Futurs ingénieurs (développeurs, spécifieurs, architectes. . .) dont le cœur de
métier (Télécoms, spatial. . .) utilise l’outil informatique
 Futurs chercheurs en systèmes (répartis)
#4
 Pré-requis
 Algorithmie (notions)
 Architectures matérielles (notions)
 Langage C (bonne pratique)
 Unix utilisateur (bonne pratique)

& %
Si un participant veut parfaire ses connaissances liées aux pré-requis, il pourra se reporter avec profit
vers les sites suivants :
• Algorithmie : cours Algorithmique et programmation (CSC3002, http://cours.it-sudparis.eu/
moodle)
• Architectures matérielles : cours Architecture matérielle et logicielle d’un ordinateur (CSC3501, http:
//cours.it-sudparis.eu/moodle)
• Langage C :

– Cours C (http://picolibre.int-evry.fr/projects/coursc/)
– Cours Algorithmique, Langage C et Structures de Données (CSC3002, http://cours.
it-sudparis.eu/moodle)
• Unix utilisateur : cours Initiation à Unix (http://www-inf.it-sudparis.eu/cours/UNIX/CSC3001.
html)

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 3


Présentation du cours
' $
3 Déroulement
 Cours
 Introduction
 Interactions entre système multi-tâche et processus
 Gestion de la mémoire
 Entrées/Sorties
 Communications inter-processus
#5  Synchronisation entre processus
 Threads
 Architecture
 Éléments d’architecture client-serveur
 TPs (sous Linux)
 Exercices d’application
 Exercice de synthèse ou d’approfondissement
 Travail personnel
& %
Les « signaux » sont abordés très brièvement dans le cadre de CSC4508.
Les outils suivants facilitant la compréhension du comportement d’une application sont évoqués, voire
utilisés :

• Observation externe : utilisation de ps, vmstat, top, time, getrusage()


• Observation interne : utilisation du profiling gprof
• Analyse accès mémoire :
– Logiciels libres : mtrace, mpatrol, splint et valgrind
– Produits commerciaux : Insure++ (société Parasoft) et Rational Purify (société IBM )
En complément, quelques outils utiles sont également référencés dans la partie Récapitulatif des outils, à
la fin du poly.
En ce qui concerne le travail personnel, voici les résultats d’un sondage réalisé sur des étudiants de la
session 2012-2013 (9 réponses sur 14 étudiants) :

• Pas de préparation nécessaire avant une séance de 3 heures de Cours Intégré.


• En moyenne, 44 heures de travail personnel (minimum de 5 heures, maximum de 100 heures).
Les TPs auront lieu sur les machines des salles de TP sous Linux, mais vous pouvez bien sûr travailler sur
votre machine personnelle (sous Linux/Mac, ou en faisant tourner Linux dans une machine virtuelle). Dans
ce cas, vous aurez besoin d’installer quelques logiciels. Voici la liste des principaux packages (pour Debian
et dérivées) à installer pour ce module :
• virtualbox
• hwloc, hwloc-devel, hwloc-libs, hwloc-gui

• papi, papi-devel
• valgrind

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 4


Introduction : rôle d’un système
d’exploitation

Michel Simatic

module CSC4508/M2

Avril 2016

5
Introduction : rôle d’un système d’exploitation
' $
Plan du document

2 Systèmes informatiques . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
#2 2 Machine virtuelle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
4 Objectifs d’un système d’exploitation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
4 Différents types de systèmes d’exploitation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6

& %

' $
1 Systèmes informatiques

 But : résoudre un problème de la vie quotidienne


 Deux entités : le matériel et le logiciel
 Matériel : architecture classique
I Unité Centrale (U.C.) chargée du traitement
#3 I Mémoire Centrale (M.C.) chargée du stockage
I Unités d’échanges (U.E.) chargée de l’adaptation
→ Périphériques chargés des interfaces
 Logiciel : deux niveaux :
I Logiciel de base
I Logiciel d’application
→ Notion de machine virtuelle

& %

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 6


Introduction : rôle d’un système d’exploitation
' $
2 Machine virtuelle

 Objectif : offrir aux utilisateurs des fonctionnalités adaptées à leurs besoins


 Principe : masquer les caractéristiques physiques du matériel
 Solution : structure en couches, chacune offrant des services de plus en plus évolués
au niveau supérieur

#4

& %

' $
3 Objectifs d’un système d’exploitation

 Deux objectifs majeurs


 Transformer une machine matérielle en une machine utilisable, c’est-à-dire fournir
des outils adaptés aux besoins indépendamment des caractéristiques physiques
 Optimiser l’utilisation du matériel principalement pour des raisons économiques.
 Mais il faut également la garantie d’un bon niveau en matière de :
#5  Sécurité : intégrité, contrôle des accès, confidentialité. . .
 Fiabilité : degré de satisfaction des utilisateurs même dans des conditions hostiles
et imprévues
 Efficacité : performances du système
→ Optimisations pour éviter tout surcoût (overhead) en terme de temps et place
consommés par le système au détriment de l’application
→ Compromis
→ Différents types de systèmes d’exploitation
& %

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 7


Introduction : rôle d’un système d’exploitation 4 Différents types de systèmes d’exploitation
' $
4 Différents types de systèmes d’exploitation

 Problèmes différents : pas de système universel


 À l’origine (et encore aujourd’hui pour certaines applications), traitement par lots ou
batch (enchaînement automatique des exécutions)

#6  Aujourd’hui trois grandes catégories de systèmes


1. Systèmes temps réel : contrôle de processus industriels (notion de respect de
temps de réponse prépondérante)
2. Systèmes transactionnels : traitements à distance (nombreux accès interactifs,
opérations prédéfinies, grande quantité d’informations)
3. Systèmes temps partagé : développement d’applications et activités avec moins
de contraintes. Mode interactif avec un maximum de fonctionnalités

& %

' $
4.1 Temps réel souple versus temps réel strict

 Deux catégories de problèmes temps réel en fonction des conséquences du


non-respect des contraintes de temps
1. Temps réel souple (ou doux)
 Temps à l’échelle humaine et un retard ne provoque que des désagréments
mineurs (impatience de l’utilisateur)
 Informatique interactive, réservation de places, gestion, traitement d’appel
#7
dans un central téléphonique. . .
2. Temps réel strict (ou dur)
 Systèmes autonomes de contrôle de processus industriels avec des exigences
très fortes au niveau du respect des contraintes de temps : tout retard entraîne
de graves conséquences telles qu’une perte d’information (un message sur un
réseau), un accident (crash d’un avion, explosion d’une raffinerie). . .
 Robotique, pilotage d’avions, surveillance médicale, acheminement de la voix
dans un central téléphonique, contrôle de raffineries, systèmes embarqués. . .
& %

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 8


Introduction : rôle d’un système d’exploitation 4 Différents types de systèmes d’exploitation
' $
4.2 Caractéristiques des applications temps réel

 Contraintes
 Réagir impérativement dans un laps de temps déterminé (durée fonction du
domaine)
 Sûreté de fonctionnement : il s’agit d’assurer un service permanent fiable car un
arrêt (partiel ou total) aurait des conséquences désastreuses.
#8
 Axiomes de base lors de la spécification/conception
 Choix de solutions sans aucun risque (par exemple en termes de blocages)
 Service minimum pour les opérations critiques
 Redondance : matérielle (doublement des organes vitaux, dont l’unité centrale)
et logicielle (procédures de contrôle, reprise. . .)
I Mode maître/esclave
I Mode partage de charge

& %

' $
4.3 Transactionnel

 Caractéristiques
 Gestion d’informations en grande quantité
 Exécution simultanée d’opérations prédéfinies
 Accès au service de façon interactive
 Grand nombre de terminaux raccordés
#9  Garantie au niveau performance (temps de réponse, sécurité, fiabilité. . .).
 Solutions
 Ajouter la gestion des communications à une application existante (vente par
correspondance)
 Développer une application intégrant les communications (réservation de place)
 Moniteurs transactionnels d’origine constructeurs ou tierce-partie comme Tuxedo
(société BEA) : optimiser la charge, faciliter la programmation, prise en compte
des aspects session et communication par le moniteur, fiabilité. . .
& %

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 9


Introduction : rôle d’un système d’exploitation

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 10


Interactions entre système
multi-tâche et processus

Michel Simatic

module CSC4508/M2

Avril 2016

11
Interactions entre système multi-tâche et processus 1 Point de vue processus
' $
Plan du document

#2 1 Point de vue processus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4


2 Point de vue système. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .12
3 Ordonnancement des processus sous Linux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16

& %

' $
1 Point de vue processus

1.1 Les applications et le système . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4


1.3 La programmation système . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
#3 1.3 Les fonctions de la libc . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.5 Utilisation des appels systèmes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.5 Utilisation des fonctions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.6 Test du retour des appels système et des fonctions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
1.7 Que faire en cas d’erreur système ? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10

& %

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 12


Interactions entre système multi-tâche et processus 1 Point de vue processus
' $
1.1 Les applications et le système

 Les applications en cours et le système résident en mémoire centrale


 La mémoire est divisée en deux parties
 L’espace système : le noyau
 L’espace utilisateur : où résident les applications

#4

& %

' $
1.2 La programmation système

 C’est le développement d’applications en utilisant les ressources et les outils fournis


par le système
 Utilisation de fonctions standards fournies avec le langage : incluses dans la
libc, bibliothèque standard du langage C pour Unix
 Ces fonctions dialoguent avec le noyau et contrôlent ce dialogue
 Les applications utilisent ainsi des ressources du noyau
#5

& %

Certaines fonctions de la libc rendent un service à l’application sans pour autant faire appel au système.
C’est le cas, par exemple, de strcmp, qsort. . .

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 13


Interactions entre système multi-tâche et processus 1 Point de vue processus
' $
1.3 Les fonctions de la libc

 Deux types fondamentaux :


1. Les appels système
#6  Ce sont les fonctions permettant la communication avec le noyau
 Exemples : open, read, write, fcntl. . .
2. Les fonctions
 Ce sont les fonctions standard du langage C
 Exemples : printf, fopen, fread, strcmp. . .

& %

' $
1.4 Utilisation des appels systèmes

 Travaillent en relation directe avec le noyau


 Retournent un entier positif ou nul en cas de succès et -1 en cas d’échec
#7  Par défaut le noyau peut bloquer les appels systèmes et ainsi bloquer l’application si
la fonctionnalité demandée ne peut pas être servie immédiatement
 Ne réservent pas de la mémoire dans le noyau. Les résultats sont obligatoirement
stockés dans l’espace du processus (dans l’espace utilisateur), il faut prévoir cet
espace par allocation de variable (statique, pile) ou de mémoire (malloc(). . .)

& %

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 14


Interactions entre système multi-tâche et processus 1 Point de vue processus
' $
1.5 Utilisation des fonctions

 Retournent une valeur de type divers (entier, caractère, pointeur. . .). Voir le manuel
de référence pour chacune d’entre elles

#8  Lorsqu’elles rendent un pointeur, celui-ci est le pointeur NULL en cas d’échec


 Certaines peuvent utiliser un appel système (fopen s’appuie sur open, fread sur
read. . .)
 Les fonctions rendant un pointeur ont généralement alloué de la mémoire dans
l’espace du processus et le pointeur rendu y donne accès

& %

' $
1.6 Test du retour des appels système et des fonctions

 Il faut toujours tester la valeur de retour d’un appel système


Si valeur rendue est égale à −1
 Il faut gérer le problème
 Une variable externe de nom errno est positionnée à une valeur indiquant
l’erreur (cette variable vaut 0 s’il n’y a pas d’erreur)
#9
 Il faut presque toujours tester la valeur de retour d’une fonction
Pour les fonctions rendant un pointeur, si la valeur rendue est NULL
 Il faut gérer le problème
 Envoi de messages d’erreurs à l’aide des fonctions
 perror() (ou strerror())
 fprintf() (ou fputs)

& %
Le fichier errno.h associe des mnémoniques à chaque erreur « standard ».
Le man de chaque appel système et de chaque fonction explique, dans la section ERRORS, les différents
codes d’erreur qui peuvent être renvoyés.
Un exemple de « presque toujours » est la fonction printf(3). Pourquoi ?
Quelques questions pour s’entraîner sur les appels systèmes et les fonctions :
• Voyez la page du manuel de l’appel système stat(2)
– Que fait cet appel système ?
– Pourquoi faut-il lui passer un pointeur sur une structure stat en paramètre ?
– Avant d’appeler stat, ce pointeur doit être initialisé pour pointer sur une zone mémoire, pourquoi ?
Sinon que se passe-t-il ?

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 15


Interactions entre système multi-tâche et processus 1 Point de vue processus

• Voyez la page du manuel de la fonction gethostbyname(3)

– Quel est le rôle de cette fonction ?


– Comment récupère-t-on son résultat ?
– Où est réalisée l’allocation de l’espace mémoire nécessaire pour stocker son résultat ?

Témoignage d’un ancien ASR : « Sans insistance de votre [NDLR : Michel Simatic] part, cela ne nous
aurait pas sauté si vite aux yeux que les problèmes (en début de la coupe robotique) venait d’un manque de
gestion des erreurs sur un code qui n’avait pas été relu par suffisamment de monde. »
' $
1.7 Que faire en cas d’erreur système ?

 Utiliser perror("message") pour afficher le message indiqué suivi de « : » et du


message système correspondant à l’erreur
 Utiliser la macro assert()
rc = appelSysteme(...);
assert(rc >= 0);
# 10  (Spécifique compilateur gcc ) Utiliser les fonctions error et error_at_line
 Définir et utiliser une macro ERROR_AT_LINE
#define ERROR_AT_LINE(status,errnum,filename,linenum,...) { \
fprintf(stderr,"%s:%d:", filename,linenum); \
fprintf(stderr,__VA_ARGS__); \
fprintf(stderr,":%s\n", strerror(errnum));\
abort(); \
}
& %

La fonction strerror() permet d’obtenir la chaîne de caractères affichée par perror() sans pour autant
provoquer d’affichage.
__FILE__, __LINE__ et __func__ sont des macros définies par le standard C99 et sont donc valides pour
tout compilateur.
La fonction abort() termine de manière anormale le processus en cours. Un core dump est généré
(analysable ultérieurement par un débuggueur), si la limite fixée par le shell l’autorise. Par exemple sous bash,
ulimit -c permet de voir la taille limite du core dump (il faut taper la commande ulimit -c unlimited
pour n’avoir aucune limite).
Voici un exemple d’utilisation de la fonction assert :
/*******************/
/* exempleAssert.c */
/*******************/

/* Ce programme a pour objectif d’illustrer le role de la macro assert */

#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

#include <assert.h>

int main(){
struct stat buf;
int rc;

rc = stat("unFichierQuiNExistePas", &buf);
assert(rc >= 0);

return EXIT_SUCCESS;
}

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 16


Interactions entre système multi-tâche et processus 1 Point de vue processus

Le message d’erreur obtenu à l’exécution est le suivant :


exempleAssert: exempleAssert.c:19: main: Assertion ‘rc >= 0’ failed.
Abandon (core dumped)

assert permet de détecter le problème, mais il faut passer sous debugger pour mieux comprendre l’origine
de l’erreur lors de l’appel système (en affichant le contenu de la valeur errno). Notez qu’il faut aussi que le
programme ait été compilé avec l’option -g, que ulimit -c valait unlimited au moment de l’erreur, et que
la variable errno soit accessible au debugger.
De ce fait, un programmeur, utilisant le compilateur gcc, peut préférer error ou error_at_line dont
voici un exemple :
/**************************/
/* exempleErroc_at_line.c */
/**************************/

/* Ce programme a pour objectif d’illustrer le role de la fonction */


/* error_at_line qui est *specifique* a gcc */

#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

#include <errno.h>
#include <error.h>

#define NOM_FICHIER "unFichierQuiNExistePas"

int main(){
struct stat buf;
int rc;

rc = stat(NOM_FICHIER, &buf);
if (rc < 0){
error_at_line(EXIT_FAILURE, errno, __FILE__, __LINE__,
"Pb au moment de l’appel a stat sur fichier \"%s\"",
NOM_FICHIER);
}

return EXIT_SUCCESS;
}

Le message d’erreur obtenu à l’exécution est le suivant :


./exempleError_at_line:exempleError_at_line.c:24: Pb au moment de l’appel a stat sur fichier "unFichierQuiNExistePas": No such file or directory

Le programmeur dispose ainsi d’un affichage plus clair quant à l’origine de l’erreur. Toutefois, error et
error_at_line présentent 2 inconvénients. Tout d’abord, elles sont spécifiques à gcc : elles ne sont donc pas
exploitables avec d’autres compilateurs. Surtout, elles ne déclenchent pas la création d’un core. De ce fait,
il n’est pas possible d’analyser le core généré au moment de l’erreur, de manière à comprendre comment le
programme est arrivé au niveau de cette erreur.
C’est pourquoi nous recommandons l’utilisation de la macro ERROR_AT_LINE dont voici un exemple :
/**************************/
/* exempleERROR_AT_LINE.c */
/**************************/

/* Ce programme a pour objectif d’illustrer le role de la macro ERROR_AT_LINE */

#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

#include <stdio.h>
#include <string.h>
#include <errno.h>
#define ERROR_AT_LINE(status,errnum,filename,linenum,...) { \
fprintf(stderr,"%s:%d:", filename,linenum); \
fprintf(stderr,__VA_ARGS__); \
fprintf(stderr,":%s\n", strerror(errnum));\
abort(); \
}

#define NOM_FICHIER "unFichierQuiNExistePas"

int main(){
struct stat buf;

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 17


Interactions entre système multi-tâche et processus 2 Point de vue système

int rc;

rc = stat(NOM_FICHIER, &buf);
if (rc < 0){
ERROR_AT_LINE(EXIT_FAILURE, errno, __FILE__, __LINE__,
"Pb au moment de l’appel a stat sur fichier \"%s\"",
NOM_FICHIER);
}

return EXIT_SUCCESS;
}

Le message d’erreur obtenu à l’exécution est le suivant :

exempleERROR_AT_LINE.c:32:Pb au moment de l’appel a stat sur fichier "unFichierQuiNExistePas":No such file or directory
Abandon (core dumped)

Cette macro pallie les inconvénients identifiés précédemment. De plus, le programmeur dispose d’un core
pour une analyse fine de l’erreur.

Notez que, dans le cas d’une erreur de certaines fonctions de la librairie C (malloc par exemple), la
variable errno n’est pas positionnée. Donc, si votre programme peut utiliser la macro assert() (comme
pour les appels système), l’utilisation de la macro ERROR_AT_LINE a moins de sens (puisque la variable errno
n’est pas positionnée). Cela peut amener le programmeur à définir une autre macro qui ne prend pas en
compte le errno.

' $
2 Point de vue système

# 11 2.1 Généralités sur l’exécution des tâches système . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12


2.2 Traitement des appels système . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
3.0 Prise en compte des interruptions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14

& %

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 18


Interactions entre système multi-tâche et processus 2 Point de vue système
' $
2.1 Généralités sur l’exécution des tâches système

 L’exécution des tâches système s’effectue en général sur le compte des différents
processus hébergés par le système
# 12
 Le système ne se déroule pour son propre compte que dans très peu de cas
 On distingue deux types d’actions
 Le traitement des appels système
 La prise en compte des interruptions

& %

' $
2.2 Traitement des appels système

# 13

& %

• Les espaces système et utilisateur sont séparés


→ Un processus ne peut pas accéder aux ressources du système
• Les processus réalisent les appels système en passant dans le mode système et en exécutant l’appel
système dans le noyau
Sur une machine 32 bits, le changement d’espace se fait par une trappe (écrite en assembleur)
Sur une machine 64 bits, le changement d’espace se fait grâce à l’instruction syscall
• Précisions sur le déroulement des appels système

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 19


Interactions entre système multi-tâche et processus

1. Préparation
– Algorithme de base
∗ Vérifier que la requête est valide
∗ Commuter la pile
∗ Sauter à la fonction rendant le service. Cette fonction récupère les arguments et les met
dans une structure de travail.
– Si la requête ne concerne pas une Entrée/Sortie, le processus déroule le code du noyau sans
blocage et termine l’appel système
– En ce qui concerne les requêtes d’E/S, on distingue :
∗ Les E/S interruptibles (elles peuvent être avortées par un signal)
∗ Les E/S ininterruptibles (elles ne peuvent pas être avortées)
– Pour ces deux types, le processus
∗ Prépare un environnement pour se mettre en attente de la réalisation
∗ Demande au gérant de périphériques de réaliser l’E/S.
∗ S’endort
2. Terminaison
– L’E/S est acquittée par un autre processus qui indique que le processus endormi est désormais
prêt pour exécution
– Le système donne la main au processus prêt pour exécution selon l’algorithme d’ordonnance-
ment

– Un processus réalisant une E/S interruptible peut sortir de l’attente lors de la réception d’un
signal
– Il défait alors sa demande au niveau noyau et retourne en mode utilisateur avec un compte-
rendu d’erreur
3. Retour au mode utilisateur
– Les appels système retournent un code
– En cas d’erreur, la variable errno est modifiée pour expliciter l’erreurg
∗ Les valeurs de errno sont décrites dans le fichier <errno.h>.
∗ Une courte phrase peut être obtenue par la fonction char *strerror (int errnum).
Cette phrase peut être affichée par void perror (const char *s) .

' $
2.3 Prise en compte des interruptions

 Les interruptions sont acquittées par le processus en cours


# 14
 Il peut être amené à passer du mode utilisateur au mode noyau
 Il fait passer les processus qui attendaient l’événement lié à cette interruption dans
le mode prêt pour exécution

& %

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 20


Interactions entre système multi-tâche et processus 3 Ordonnancement des processus sous Linux
' $
3 Ordonnancement des processus sous Linux

3.1 Priorité statique . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16


# 15 3.3 Politique d’ordonnancement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
3.3 Ordonnancement temps-réel Linux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
3.4 Ordonnancement standard . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21

& %

Sous Linux, on a 4 notions liées à l’ordonnancement :

• La priorité statique,

• La politique d’ordonnancement,

• La priorité d’ordonnancement,

• La priorité dynamique.

Ces notions permettent :

• Un ordonnancement temps-réel « souple »

• Un ordonnancement standard privilégiant une exécution équitable des différents processus

NB : Sous Linux, on n’a aucune garantie sur le temps d’exécution d’une tâche (axiome de base du
temps-réel « strict »).

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 21


Interactions entre système multi-tâche et processus 3 Ordonnancement des processus sous Linux
' $
3.1 Priorité statique

 int sched_setscheduler(pid_t pid, int policy, const struct


sched_param *p);

# 16 permet d’affecter à un processus une priorité statique (entre 0 et 99) stipulant la file
d’attente de l’ordonnanceur (scheduler ) où il doit être placé.
 Pour décider du processus à exécuter, l’ordonnanceur prend le premier processus
prêt dans la file de plus haute priorité
 NB : Valeurs fortes de priorité statique = Fortes priorités

& %

' $
3.2 Politique d’ordonnancement
 int sched_setscheduler(pid_t pid, int policy, const struct
sched_param *p);
permet d’affecter à un processus une politique d’ordonnancement
 SCHED_FIFO : Le processus n’arrête son exécution que si
I Il fait une E/S
I Il fait un appel à int sched_yield(void) (il laisse alors passer les autres
processus prêts de même priorité)
# 17
I Il change sa politique d’ordonnancement
I Il est préempté par des processus plus prioritaires (il reprend ensuite son
exécution)
 SCHED_RR : une condition d’arrêt supplémentaire est définie
I La durée d’exécution atteint un quantum de temps (déterminable via la
fonction int sched_rr_get_interval(pid_t pid, struct timespec
*tp))
 SCHED_OTHER : ordonnancement en temps partagé (réservé aux processus de
priorité statique 0)
& %

Le quantum de temps pour un processus SCHED_RR est identique quelle que soit la priorité statique du
processus.

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 22


Interactions entre système multi-tâche et processus 3 Ordonnancement des processus sous Linux
' $
3.3 Ordonnancement temps-réel Linux

# 18 3.4.0 Inversion de priorité : énoncé du problème . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21


3.4.0 Inversion de priorité : exemple de solution . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21

& %
Les priorités statiques et les politiques d’ordonnancement SCHED_FIFO et SCHED_RR de Linux permettent
de gérer du temps-réel « souple ».
Toutefois, il faut garder en mémoire que Linux est sujet à l’inversion de priorité présentée dans les
transparents suivants.

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 23


Interactions entre système multi-tâche et processus 3 Ordonnancement des processus sous Linux
' $
3.3.1 Inversion de priorité : énoncé du problème

# 19

& %

' $
3.3.2 Inversion de priorité : exemple de solution

# 20

& %

Ce transparent présente un exemple de solution qui ne peut être qu’implémenté manuellement sous
Linux. En effet, Linux ne fournit aucun mécanisme pour détecter et corriger automatiquement le problème
d’inversion de priorité.

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 24


Interactions entre système multi-tâche et processus 3 Ordonnancement des processus sous Linux
' $
3.4 Ordonnancement standard

 La politique SCHED_OTHER est la politique utilisée par défaut par Linux


 Elle s’appuie sur l’ordonnanceur CFS (Completely Fair Scheduler )
 Le processus à exécuter est choisi dans la liste des processus de priorité statique
nulle, en utilisant une priorité dynamique qui ne s’applique que dans cette liste.
NB : Valeurs fortes = Faibles priorités
# 21  La priorité dynamique est fonction, entre autres
 d’une priorité d’ordonnancement (caractère « gentil » du processus fixé avec les
appels système int nice(int inc) ou int setpriority(int which, int
who, int prio))
 du fait que le processus ait relâché le processeur avant expiration de son délai
 Le processus le plus prioritaire (dynamiquement) est exécuté jusqu’à ce qu’il
 Fasse une E/S
 Devienne moins prioritaire (dynamiquement) qu’un autre processus
& %
Pour approfondir l’ordonnancement standard et prendre connaissance des pistes de recherche envisagées,
le lecteur tirera profit de la référence : C. S. Wong, I. Tan, R. D. Kumari, and F. Wey. Towards achieving
fairness in the linux scheduler. SIGOPS Oper. Syst. Rev., 42(5) :34-43, 2008.

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 25


Interactions entre système multi-tâche et processus

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 26


Gestion de la mémoire

Michel Simatic

module CSC4508/M2

Avril 2016

27
Gestion de la mémoire 1 Point de vue système
' $
Plan du document

#2 1 Point de vue système . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4


2 Point de vue processus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21

& %

' $
1 Point de vue système

1.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.2 Besoins des processus. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .5
#3 1.3 Pagination . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.4 Segmentation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
1.5 Pagination versus Segmentation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
1.6 Algorithmes pour la gestion des pages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19

& %

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 28


Gestion de la mémoire 1 Point de vue système
' $
1.1 Introduction

 Un processus a besoin d’être présent en mémoire centrale pour s’exécuter


 Mémoire centrale divisée en deux parties :
 L’espace réservé au système d’exploitation
 L’espace alloué aux processus

#4  La Gestion mémoire concerne l’espace processus


 Capacités mémoire augmentent, mais les besoins aussi → Nécessité de plusieurs
niveaux
 Mémoire(s) rapide(s) (cache(s))
 Mémoire centrale
 Mémoire auxiliaire (disque)
Principe d’inclusion pour limiter les mises à jour entre les différents niveaux

& %

À propos du principe d’inclusion, dans une architecture Intel, on a le cache L1 (Level 1) qui est inclus
dans le cache L2 (Level 2), lui-même inclus dans la RAM, elle-même incluse dans le swap (disque).
Voici les temps d’accès typiques à des données situées dans les différents types de mémoire sur une
machine “classique” en 2014 :

• donnée dans le cache L1 : 0,5 ns

• donnée dans le cache L2 : 7 ns (15 fois plus lent que L1)

• donnée dans la RAM : 100 ns (200 fois plus lent que L1)

• donnée sur un disque SSD : 150 µs (150 000 ns : 300 000 fois plus lent que L1)

• donnée sur un disque dur : 10 ms (10 000 000 ns : 20 millions de fois plus lent que L1)

Le tableau suivant montre les différences de coût entre les types de mémoire (et l’évolution avec les
années) :

Année 2008 2009 2010 2014


RAM (en €/Go) – 37,00 21,85 8.75
Disque SSD (en €/Go) – – – 0,50
Clé USB 16 Go (en €/Go) – – 1,64 0.62
Disque dur, 7200 tr/mn (en €/Go) 0,50 0,32 0,10 0,04

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 29


1 Point de vue système 1.3 Pagination
' $
1.2 Besoins des processus
Allocation Trouver une zone mémoire disponible pour y stocker un programme
Libération quand un programme se termine, récupérer les espaces (en faisant des
regroupements s’il y a eu fragmentation) qu’il occupait pour les consacrer à d’autres
processus.
Protection Garantir l’intégrité de l’espace mémoire associé à chaque processus →
 Dispositif (matériel) de contrôle empêchant tout accès en dehors de l’espace
#5
attribué
 Toute détection d’une violation mémoire est récupérée par le système
(déroutement)
Adressage Traduction des adresses logiques en adresses physiques : fonction appelée
topographie ou mapping
 À la traduction/édition des liens
 Au chargement initial
 À l’exécution
& %

Les systèmes multi-utilisateurs et multi-tâches actuels tels Unix, Windows-NT (et ses successeurs
Windows-2000, Windows-XP, etc.). . . ont ces 4 besoins. Comme la gestion mémoire à base de pagination
et de segmentation y répond pleinement, ces systèmes d’exploitation utilisent ce type de gestion mémoire.
C’est pourquoi nous les détaillons dans la suite de ce cours.
Toutefois des systèmes d’exploitation (notamment ceux animant certains super-calculateurs) n’ont pas
certains de ces besoins (par exemple, la protection ou bien l’adressage). Ils utilisent donc une gestion mémoire
plus frustre, mais beaucoup moins gourmande en ressources CPU. Les problèmes spécifiques de ce type de
gestion mémoire (allocation d’une zone libre, libération de zone en évitant de créer une fragmentation. . .)
sont similaires à ceux posés par malloc/free (qui seront étudiés dans la suite de ce cours). Les algorithmes
sont donc très proches.
' $
1.3 Pagination

1.3.1 Généralités . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
#6 1.3.2 Adresse logique . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
1.3.4 Table des pages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
1.3.4 Accélérateurs (caches d’adresses) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
1.4.0 Écroulement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13

& %

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 30


1 Point de vue système 1.3 Pagination
' $
1.3.1 Généralités

#7

 Objectif : offrir un espace adressable plus grand que la mémoire physique


 Principe
 Espace adressable de chaque programme découpé en pages
 Mémoire physique divisée en cadres de pages
 À l’exécution, un processus possède
 Peu de pages en mémoire centrale (pages actives)
 Le reste est
& %

' $
I Inexistant en mémoire (pages inactives jamais écrites)
I En mémoire secondaire (pages inactives qui ont déjà été écrites)

 Le dispositif de pagination
 Effectue la correspondance d’adresse
 Charge les pages nécessaires (déroutement par défaut de page)
 (Éventuellement) décharge des pages actives en mémoire secondaire

 Donc, le programme n’est plus présent intégralement en mémoire centrale et il n’y a plus
de continuité physique
#8

& %

Sous Linux, les cadres de page ont une taille de 4 Ko (taille définie par les constantes PAGE_SIZE et
PAGE_SHIFT dans le fichier page.h).

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 31


1 Point de vue système 1.3 Pagination
' $
1.3.2 Adresse logique
 Espace adressable divisé à partir des bits de poids forts de l’adresse
Adresse logique sur k bits
Numéro de Page Déplacement dans la page
( p bits ) ( (k − p) bits )
Np d

#9 → 2p pages et une page contient 2k−p octets


 Taille d’une page
 À l’origine : 512 octets ou 1 Ko
 Aujourd’hui : 2 Ko, 4 Ko (k-p = 12 bits, donc p = 20 bits) et plus
Choix = compromis entre divers critères opposés
 Dernière page à moitié gaspillée
 Temps de transfert d’une page faible par rapport au temps d’accès total
 Mémoire de petite capacité : petites pages
& %

' $
1.3.3 Table des pages
 La correspondance entre adresse logique et adresse physique se fait avec une table
des pages contenant
 Numéro de cadre de page
 Bits d’information (présence, accès, référence, écriture, date de chargement. . .)

# 10

& %

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 32


Gestion de la mémoire 1 Point de vue système
' $
1.3.4 Accélérateurs (caches d’adresses)
 Problème : tout accès à une information nécessite deux accès mémoire
 Solution : utiliser des mémoires associatives (registres d’accès rapide)
 Principe
 On a un certain nombre de registres à disposition
 Numéro de page logique Np comparé au contenu de chaque registre
 Égalité trouvée → en sortie numéro Nc du cadre correspondant
 Sinon utilisation de la table des pages
# 11

& %

Dans une architecture Intel, on dispose d’un Translation Look-aside Buffer (TLB) muni de 32, 64, voire
256 entrées. NB : on parle également de cache de traduction d’adresse.
Pour anecdote, en décembre 2007, AMD a constaté un bug dans ses processeurs quadri-cœurs Opteron
and Phenom. Ce bug était lié à la TLB et au cache de niveau 3. Un patch a été proposé : il entraînait une
dégradation de performances de 10 à 40% (http://techreport.com/articles.x/13741).

' $
1.3.5 Écroulement
 Effet secondaire de la pagination : écroulement (thrashing) dû à un taux de défauts de page trop
important

 Deux facteurs principaux : taille mémoire et degré de multiprogrammation (nombre de processus)

# 12

 Solutions
 Augmentation de la capacité mémoire
 Ordre des modules utilisés
 Prise en compte lors du développement
 Régulation de la charge (load levelling) : limitation du nombre de processus
 Définition d’un espace vital (working set) nécessaire
 Gestion du taux de défauts de page : moduler le nombre de pages allouées
& %

La commande appletviewer ˜simatic/Cours/ASR/Demo/vm.html affiche une démonstration de la mé-


moire virtuelle.

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 33


1 Point de vue système 1.4 Segmentation
' $
1.4 Segmentation

# 13 1.4.2 Généralités . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
1.4.2 Descripteur de segments . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15

& %

' $
1.4.1 Généralités
 Objectif : refléter en mémoire centrale la structure des programmes (code, données, pile)

 L’utilisateur définit des segments (entités logiques)

 Programme = ensemble de segments ayant une taille et des attributs propres (lecture
seule, écriture, partage)

 Pour disposer d’une segmentation, il faut un dispositif chargé de


 Effectuer la correspondance entre une adresse logique et l’adresse physique
# 14
 Assurer la protection
 Charger les segments en mémoire

 Correspondance réalisée par une table des descripteurs de segments contenant


 Adresse de base d’implantation
 Longueur du segment
 Informations de contrôle d’accès

 Protection assurée par un double contrôle : si le déplacement est négatif ou supérieur à la


longueur du segment, alors il y a violation mémoire et déroutement
& %

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 34


Gestion de la mémoire 1 Point de vue système
' $
1.4.2 Descripteur de segments

# 15

 Doublement des accès mémoire → Mêmes dispositifs accélérateurs (caches


d’adresses) que pour la pagination.
& %

' $
1.5 Pagination versus Segmentation

 Mise en oeuvre analogue, mais concepts différents


 Pagination = division physique vs Segmentation = division logique
 Pagination transparente à l’utilisateur vs Segmentation déterminée par l’utilisateur
 Taille des pages fixes vs Taille des segments variable (avec contrôle de
non-dépassement)

# 16  On utilise donc pagination et segmentation !

 Avantage : mémoire virtuelle (segments chargés en partie) et organisation logique


 Inconvénient : Surcoût (limité par l’utilisation de caches d’adresses dans le processeur)

& %
Les commandes Unix suivantes donnent des informations sur la pagination et la segmentation des pro-
cessus de la machine :
• Pagination
– La commande ps -u affiche
∗ La taille mémoire virtuelle utilisée par le processus (VSZ)
∗ La taille mémoire résidant en mémoire centrale (RSS)
– La commande top affiche
∗ la mémoire centrale totale/utilisée/disponible
∗ le swap total/utilisé/disponible

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 35


1 Point de vue système 1.6 Algorithmes pour la gestion des pages

– top a un affichage complexe qui peut requérir trop de temps quand le processeur est très utilisé.
Dans ce cas, la commande vmstat -n 1 est préférable : elle affiche également d’autres indications
sur la charge de la machine

• Segmentation

– La commande size nomExécutable liste la taille des différentes sections de nomExécutable

' $
1.6 Algorithmes pour la gestion des pages

# 17 1.6.2 Chargement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
1.6.2 Remplacement (déchargement) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19

& %

' $
1.6.1 Chargement

 Un programme démarre. On décide des pages à charger :


 À la demande c’est-à-dire en cas de besoin
 Par anticipation
# 18
Exemple : un programme travaille en général (principe de la séquentialité des
programmes) sur des pages contiguës en y accédant de manière croissante : il
accède à une page, puis à la suivante (dans la plage d’adresse), puis à la
suivante. . .. De ce fait, quand un programme accède à une page, certains
systèmes (Exemple : Sun) anticipent le fait qu’il accédera bientôt à la suivante :
il la précharge.

& %

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 36


Gestion de la mémoire 2 Point de vue processus
' $
1.6.2 Remplacement (déchargement)
 Pour les systèmes paginés lorsqu’il n’y a plus de page libre
FIFO (First In First Out) Remplacer la page la plus ancienne
LRU (Least Recently Used) Remplacer la page la moins récemment utilisée
LFU (Least Frequently Used) Remplacer la page la moins fréquemment
utilisée
Algorithme de Belady dit « optimal » (OPT ou MIN) Remplacer la
# 19 page qui ne sera plus utilisée s’il en existe une et sinon celle qui le sera le plus
tardivement
Random Remplacer une page choisie au hasard
Algorithme « seconde chance » Compromis tant au niveau surcoûts qu’en
termes de transferts
 Variante de l’algorithme LRU couplée au principe FIFO
 Un bit U associé à chaque page et mis à un lors de chaque accès
 L’algorithme parcourt la liste des pages dans l’ordre FIFO et met à 0 tout bit
U qui vaut 1 ou retire la première page dont le bit U est à 0
& C’est l’algorithme utilisé par Linux %

L’algorithme « seconde chance » est également connu sous les noms :

• FINUFO (First In Not Used First Out)

• horloge (clock)

' $
2 Point de vue processus

2.1 Espace d’adressage d’un processus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21


# 20 2.2 Observation et contrôle de l’utilisation de la mémoire . . . . . . . . . . . . . . . . . . . . . . . . . . 23
2.3 Allocation/Désallocation dynamique de mémoire . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
2.4 Déverminage des accès mémoire . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37

& %

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 37


2 Point de vue processus 2.2 Observation et contrôle de l’utilisation de la mémoire
' $
2.1 Espace d’adressage d’un processus

# 21

& %

' $
2.2 Observation et contrôle de l’utilisation de la mémoire

2.2.1 Observation de l’utilisation des pages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23


# 22 2.2.2 Observation du type d’accès à la mémoire . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
2.2.3 Être attentif à l’alignement des structures. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .26
2.2.4 Contrôle des défauts de page . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27

& %

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 38


2 Point de vue processus 2.2 Observation et contrôle de l’utilisation de la mémoire
' $
2.2.1 Observation de l’utilisation des pages

 ps -u et top donnent des indications


 int getrusage (int who, struct rusage *usage)
permet de connaître
 ru_minflt : nombre de défauts de page mineurs, c’est-à-dire pages inactives
# 23
jamais écrites finalement chargées en mémoire
 ru_majflt : nombre de défauts de page majeurs, c’est-à-dire pages inactives
déjà écrites et déchargées (swappées) sur disque
 /usr/bin/time nomExécutable
affiche toutes ces informations pour le processus nomExécutable lancé en mode
commande

& %

• getrusage(2) permet aussi de connaître

– ru_nswap : nombre de fois où le processus a été entièrement swappé


– ru_utime : temps passé par le processus en mode utilisateur
– ru_stime : temps passé par le processus en mode noyau

Noter que la struct rusage renvoyée par getrusage contient d’autres champs, mais ces champs ne
sont pas renseignés par Linux

• time nomExécutable utilise la fonction built-in du shell qui fournit beaucoup moins d’informations
que /usr/bin/time nomExécutable

' $
2.2.2 Observation du type d’accès à la mémoire

# 24  L’outil valgrind (logiciel libre, http://valgrind.org) permet d’observer tous les


types d’accès à la mémoire effectués par un programme
valgrind --tool=cachegrind nomProgramme

& %

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 39


2 Point de vue processus 2.2 Observation et contrôle de l’utilisation de la mémoire

Voici un exemple d’utilisation de valgrind sur les programmes gentil et mechant étudiés dans l’exer-
cice 1 (et pour lesquels on constate une différence de temps d’exécution d’un facteur 10 en l’absence de
swap).
Rappelons le contenu de gentil.c :
/************/
/* gentil.c */
/************/

#include <stdlib.h>
#include "constantes.h"

char t[NBRE][PAGE];

int main() {
int i,j,k;
for (i=0 ; i<2 ; i++) {
for (j=0 ; j<NBRE ; j++) {
for (k=0 ; k<PAGE ; k++) {
t[j][k] = 1;
}
}
}
return EXIT_SUCCESS;
}

Et celui de mechant.c :
/*************/
/* mechant.c */
/*************/

#include <stdlib.h>
#include "constantes.h"

char t[NBRE][PAGE];

int main() {
int i,j,k;
for (i=0 ; i<2 ; i++) {
for (k=0 ; k<PAGE ; k++) {
for (j=0 ; j<NBRE ; j++) {
t[j][k] = 1;
}
}
}
return EXIT_SUCCESS;
}

Pour ne pas avoir des temps d’exécution trop longs avec valgrind, le fichier constantes.h définit un
tableau de « seulement » 5 Mo :
/* constantes.h */

#define NBMEG 5

#define BYTES (NBMEG * 1<<20)


#define PAGE 4096
#define NBRE (BYTES/PAGE)

valgrind --̇tool=cachegrind gentil affiche :


==5534== Cachegrind, an I1/D1/L2 cache profiler.
==5534== Copyright (C) 2002-2005, and GNU GPL’d, by Nicholas Nethercote et al.
==5534== Using LibVEX rev 1471, a library for dynamic binary translation.
==5534== Copyright (C) 2004-2005, and GNU GPL’d, by OpenWorks LLP.
==5534== Using valgrind-3.1.0, a dynamic binary instrumentation framework.
==5534== Copyright (C) 2000-2005, and GNU GPL’d, by Julian Seward et al.
==5534== For more details, rerun with: -v
==5534==
--5534-- warning: Pentium 4 with 12 KB micro-op instruction trace cache
--5534-- Simulating a 16 KB I-cache with 32 B lines
==5534==
==5534== I refs: 94,485,037
==5534== I1 misses: 921
==5534== L2i misses: 526
==5534== I1 miss rate: 0.00%
==5534== L2i miss rate: 0.00%
==5534==
==5534== D refs: 52,486,117 (41,985,225 rd + 10,500,892 wr)
==5534== D1 misses: 165,217 ( 1,183 rd + 164,034 wr)
==5534== L2d misses: 164,703 ( 694 rd + 164,009 wr)
==5534== D1 miss rate: 0.3% ( 0.0% + 1.5% )

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 40


2 Point de vue processus 2.2 Observation et contrôle de l’utilisation de la mémoire

==5534== L2d miss rate: 0.3% ( 0.0% + 1.5% )


==5534==
==5534== L2 refs: 166,138 ( 2,104 rd + 164,034 wr)
==5534== L2 misses: 165,229 ( 1,220 rd + 164,009 wr)
==5534== L2 miss rate: 0.1% ( 0.0% + 1.5% )

valgrind --̇tool=cachegrind mechant affiche :

==5560== Cachegrind, an I1/D1/L2 cache profiler.


==5560== Copyright (C) 2002-2005, and GNU GPL’d, by Nicholas Nethercote et al.
==5560== Using LibVEX rev 1471, a library for dynamic binary translation.
==5560== Copyright (C) 2004-2005, and GNU GPL’d, by OpenWorks LLP.
==5560== Using valgrind-3.1.0, a dynamic binary instrumentation framework.
==5560== Copyright (C) 2000-2005, and GNU GPL’d, by Julian Seward et al.
==5560== For more details, rerun with: -v
==5560==
--5560-- warning: Pentium 4 with 12 KB micro-op instruction trace cache
--5560-- Simulating a 16 KB I-cache with 32 B lines
==5560==
==5560== I refs: 94,524,470
==5560== I1 misses: 921
==5560== L2i misses: 526
==5560== I1 miss rate: 0.00%
==5560== L2i miss rate: 0.00%
==5560==
==5560== D refs: 52,508,647 (42,002,123 rd + 10,506,524 wr)
==5560== D1 misses: 10,487,135 ( 1,183 rd + 10,485,952 wr)
==5560== L2d misses: 10,486,621 ( 694 rd + 10,485,927 wr)
==5560== D1 miss rate: 19.9% ( 0.0% + 99.8% )
==5560== L2d miss rate: 19.9% ( 0.0% + 99.8% )
==5560==
==5560== L2 refs: 10,488,056 ( 2,104 rd + 10,485,952 wr)
==5560== L2 misses: 10,487,147 ( 1,220 rd + 10,485,927 wr)
==5560== L2 miss rate: 7.1% ( 0.0% + 99.8% )

En écriture, on constate qu’avec gentil, les données ne sont pas trouvées dans le cache dans 1, 5% des
cas. Avec mechant, cette valeur monte à 99, 8% des cas, ce qui explique la différence de performances des
deux programmes (sans compter qu’avec gentil, la TLB est mise pleinement à profit).

NB :

• Pour mechant, on fait globalement 2 × N BRE × P AGE = 2 × BY T ES = 10.485.760 écritures sur le


tableau t, ce qui explique le nombre d’écritures observées avec valgrind.

• Pour gentil, on fait aussi 2 × N BRE × P AGE = 2 × BY T ES = 10.485.760 écritures sur le tableau t.
Pourtant les D1 misses en écriture sont nettement plus réduits que pour mechant ! L’explication réside
dans l’exécution de l’outil cg_annotate (fourni avec valgrind, cg_annotate –<numéroPID> gentil,
[numéroPID] pouvant être retrouvé avec l’extension du fichier cachegrind.out.[numéroPID] généré
par l’exécution de valgrind --̇tool=cachegrind). Il montre que valgrind considère que le processeur
est muni d’un cache en écriture de 64 octets (cf. 64 B dans la ligne D1 cache). Par conséquent, dans
le cas de gentil qui fait des accès contigus, le processeur n’a besoin d’accéder à la mémoire physique
que tous les 64 octets. Or 10.485.760/64 = 163.840 : on retrouve le nombre de D1 misses en écriture
de gentil.

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 41


2 Point de vue processus 2.2 Observation et contrôle de l’utilisation de la mémoire
' $
2.2.3 Être attentif à l’alignement des structures

 Par défaut, le compilateur aligne sur des frontières de 4 octets (2 octets pour les
short).
 Le compilateur peut donc générer des octets inutilisés au sein d’une structure.
Pour récupérer cet espace perdu
 Le mieux est de réordonnancer les feuilles des structures
# 25  On peut aussi compacter toutes les structures d’un source
gcc -fpack-struct
 Ou bien compacter juste une structure donnée
typedef struct {
...
} __attribute__((packed)) uneStructure;
 Cet alignement peut être insuffisant (besoin d’aligner sur 16 octets, par exemple).
Pour forcer l’alignement :
& %
' $
 typVariable nomVariable __attribute__((aligned (nombre)));
 Utilisation de posix_memaligned pour les variables de type pointeur

# 26

& %

Par défaut, le compilateur aligne sur des frontières de 4 octets, car certains processeurs ne savent pas
accéder à des entiers qui sont sur une frontière de 4 octets (ou alors ils sont moins performants). C’état notam-
ment le cas des premières générations de processeurs ARM. Aujourd’hui, la plupart des processeurs évolués
(Intel, AMD. . .) n’ont plus ce genre de problème. Pourtant certains compilateurs (dont gcc) continuent à
faire par défaut cet alignement sur des frontières de 4 octets.
Dans le cas où le compactage d’une structure donne lieu à un alignement inefficace (par exemple, int à che-
val sur une frontière de 4 octets), le compilateur ne produit aucun warning du type WARNING : inefficient
alignment.
L’exercice Mémoire/3 illustre la place et les performances perdues si une structure contient des éléments
qui ne sont pas rangés dans le bon ordre.
posix_memaligned correspond à _aligned_malloc sur les compilateurs Microsoft et Intel.
L’exercice Mémoire/8 illustre l’utilisation d’instructions vectorielles SSE des processeurs Intel/AMD qui

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 42


2 Point de vue processus 2.2 Observation et contrôle de l’utilisation de la mémoire

imposent des données alignées sur des frontières de 16 octets. Les instructions SSE permettent de faire des
opérations sur 4 flottants (au sens float du langage C) en parallèle, mais requièrent que chaque groupe de
4 flottants soit aligné sur une frontière de 16 octets. Intel propose désormais une extension de SSE appelée
AVX (Advanced Vector Extensions). Elle permet de travailler sur 256 bits au lieu de 128 et n’impose pas
d’alignement sur une frontière d’octets.
La macro offsetof (définie dans stddef.h) permet de connaître la position (offset) d’un champs dans
une structure. Par exemple, le programme suivant affiche la position du champs field_b (ici : 4) par rapport
au début de la structure :

struct my_struct {
int field_a;
int field_b;
};

printf("%d\n", offsetof(field_b, struct my_struct));

Cette macro est, par exemple, utilisée dans le noyau Linux pour retrouver l’adresse d’une structure à
partir d’un pointeur sur un de ses champs.
' $
2.2.4 Contrôle des défauts de page
 Lutte contre les défauts de page mineurs
 Il suffit d’écrire un octet dans la page
 Lutte contre les défauts de page majeurs
 int mlock(const void *addr, size_t len)
permet de verrouiller en mémoire centrale len octets (à partir de addr) de la
mémoire virtuelle du processus
# 27  int munlock(const void *addr, size_t len)
déverrouille
 int mlockall(int flags)
permet de tout verrouiller en mémoire centrale. flags vaut une combinaison de
I MCL_CURRENT : Verrouiller toutes les pages correspondant actuellement à
l’espace d’adressage du processus
I MCL_FUTURE : Verrouiller toutes les pages qui seront dans l’espace d’adressage
 int munlockall(void)
déverrouille tout
& %
Sous Linux, la quantité de mémoire physique que l’on peut verrouiller est fixée par RLIMIT_MEMLOCK

• mlock(2) et mlockall(2) peuvent donc échouer.


• Dans le cas où ils réussissent, un malloc(3) subséquent peut échouer.

La quantité de mémoire physique verrouillable par un processus peut être changée en appelant setrlimit.
Un exemple d’utilisation est disponible dans le fichier exemple_setrlimit.c :
#include <sys/time.h>
#include <sys/resource.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
struct rlimit limits;

if(getrlimit(RLIMIT_MEMLOCK, &limits))
abort();

printf("Number of lockable memory pages:\n");

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 43


2 Point de vue processus 2.3 Allocation/Désallocation dynamique de mémoire

printf("\tcurrent: %ld\n", limits.rlim_cur);


printf("\tmaximum: %ld\n", limits.rlim_max);

limits.rlim_cur = limits.rlim_cur / 2;

printf("Setting limits to %ld\n", limits.rlim_cur);

if(setrlimit(RLIMIT_MEMLOCK, &limits))
abort();

if(getrlimit(RLIMIT_MEMLOCK, &limits))
abort();

printf("Number of lockable memory pages:\n");


printf("\tcurrent: %ld\n", limits.rlim_cur);
printf("\tmaximum: %ld\n", limits.rlim_max);

return 0;
}

' $
2.3 Allocation/Désallocation dynamique de mémoire

2.3.1 Allocation/Désallocation mémoire standard . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30


2.3.2 Digression : algorithmes pour malloc . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
# 28 2.3.4 Digression : algorithmes pour free . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
2.3.4 Désallocation automatique avec alloca . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
2.3.6 Mécanisme d’allocation/désallocation dédié . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
2.3.6 Allocation/Désallocation mémoire au niveau système . . . . . . . . . . . . . . . . . . . . . . . . . 35

& %

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 44


2 Point de vue processus 2.3 Allocation/Désallocation dynamique de mémoire
' $
2.3.1 Allocation/Désallocation mémoire standard
 void *malloc (size_t size)
renvoie un pointeur sur une zone de size octets
 void *realloc (void *ptr, size_t size)
change la taille d’une zone mémoire réservée précédemment par malloc(3)
 void *calloc (size_t nmemb, sizeoi_t size)
# 29
Même rôle que malloc, mais avec initialisation de la mémoire à 0
 int posix_memalign(void **memptr, size_t alignment, size_t size)
Même rôle que malloc, l’adresse de la zone mémoire renvoyée étant un multiple de
alignment
_aligned_malloc est la fonction correspondante sur compilateur Microsoft ou Intel
 void free (void *ptr)
Libération de zone
& %
' $
 void _aligned_free (void *ptr)
Libération de zone mémoire allouée par _aligned_malloc
 int mallopt (int parametre, int valeur)
Contrôle de paramètres de fonctionnement des fonctions précédentes

# 30

& %

• Toutes ces fonctions sont des fonctions de la bibliothèque C (qui font dans certains cas des appels
système).

• L’algorithme de malloc(3) est très performant. Il n’est donc pas nécessaire en général de chercher à
l’optimiser.

• Toutefois :

– Lors d’une allocation d’une zone mémoire qui doit être initialisée à 0, on privilégiera calloc(3)
(il est plus efficace qu’un malloc(3) suivi d’un memset(3)).
– Si besoin, on peut affiner les paramètres de fonctionnement de malloc(3) avec mallopt(3).

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 45


2 Point de vue processus 2.3 Allocation/Désallocation dynamique de mémoire

– De plus, en affectant __malloc_hook, __realloc_hook et __free_hook, on peut personnaliser le


comportement des routines d’allocation/libération standard.

– http://blog.pavlov.net/2007/11/21/malloc-replacements évoque des alternatives à l’algo-


rithme standard de malloc.

• Quand on libère une zone avec free, il est vivement conseillé d’affecter NULL au pointeur qu’on avait
sur cette zone. Cela permet un plantage net si, par erreur, dans la suite du programme, on cherche à
nouveau à accéder à cette zone (désormais libérée) à l’aide de ce pointeur.

• [Blaess, 2002] propose d’aller encore plus loin : il suggère de tester systématiquement la valeur d’un
pointeur avant de faire malloc, c’est-à-dire d’avoir systématiquement la séquence : assert(p !=
NULL); p = malloc(taille); ...

Le programme suivant illustre comment comment cette affectation à NULL permet d’avoir un plantage
net et comment le passage sous debugger permet de retrouver rapidement l’origine de l’erreur.

/**********************/
/* ilFautMettreNULL.c */
/**********************/

/* Ce programme illustre l’interet d’affecter a NULL une variable qui */


/* contient un pointeur vers une zone non allouee. */
/* En effet, il fait une segmentation fault, ce qui permet de reperer */
/* qu’on a commis une operation illicite. Utiliser un debugger permet */
/* de comprendre le probleme. */
/* */
/* 1) Compiler le programme avec l’option -g */
/* cc -g -o ilFautMettreNULL ilFautMettreNULL.c */
/* 2) ./ilFautMettreNULL */
/* ==> Segmentation fault */
/* 3) ulimit -c unlimited */
/* 4) ./ilFautMettreNULL */
/* ==> Segmentation fault (core dumped) */
/* 5) ddd ./ilFautMettreNULL core */

#include <stdlib.h>
#include <assert.h>

void h(char *p){


*p = ’a’;
}

void g(char *p){


h(p);
}

void f(char *p){


g(p);
}

int main(){
char *p = NULL;

f(p);

p = malloc(1);
assert(p != NULL);

f(p);

free(p);
p = NULL;

f(p);

return EXIT_SUCCESS;
}

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 46


2 Point de vue processus 2.3 Allocation/Désallocation dynamique de mémoire
' $
2.3.2 Digression : algorithmes pour malloc

 Dans la suite, on appelle « Trou », une zone libre de taille xi (supérieure à un


minimum) et d’adresse ai
 Meilleur choix (best fit) : trous dans l’ordre croissant des tailles
(x1 < x2 < · · · < xn ) et choix du plus petit trou de taille suffisante
 Plus mauvais choix (worst fit) : trous dans l’ordre décroissant des tailles
# 31 (x1 > x2 > · · · > xn ) et allocation de tout ou partie du premier trou
 Premier trouvé (first fit) : trous dans l’ordre croissant des adresses
(a1 < a2 < · · · < an ) et choix du premier trou de taille suffisante
 Frères siamois (Buddy system) basé sur l’allocation de zones ayant une taille
multiple d’une puissance de 2
 Principe : si demande de taille T, recherche de la puissance i telle que
2i−1 < T ≤ 2i , puis allocation d’un trou de taille 2i
 Algorithme récursif : si liste 2i vide, recherche d’un trou de taille 2i+1
& %

' $
2.3.3 Digression : algorithmes pour free
 Fragmentation : demande refusée car plus de zone de taille suffisante mais somme
des tailles des zones libres supérieure à la taille demandée
 Solution : retassement ou compactage : regrouper les zones libres pour en créer une
la plus grande possible
 Objectif : reconstruire la liste des zones libres en cherchant à reconstruire le plus
grand trou possible
# 32
 Plusieurs cas à envisager
 Zone libérée entre 2 zones occupées
 Zone libérée entre une zone occupée et une zone libre
 Zone libérée entre 2 zones libres
 Un algorithme performant au niveau de l’allocation peut s’avérer être plus complexe
pour la libération
 Algorithme des frères siamois : la libération d’une zone (un buddy ) est récursive
& %

Le site http://g.oswego.edu/dl/html/malloc.html présente les algorithmes d’allocation/désallocation


utilisés par gcc (NB : cette page html explique qu’elle est désormais obsolète ; toutefois, elle contient des
indications pour comprendre l’implémentation visible dans les sources mmalloc.c, mfree.c et mmprivate.h
de gcc) :

• La recherche d’une zone libre se fait selon un ordre best-fit.

• L’utilisation d’index permet de traiter une demande d’allocation en une douzaine d’instructions.

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 47


2 Point de vue processus 2.3 Allocation/Désallocation dynamique de mémoire
' $
2.3.4 Désallocation automatique avec alloca

 void *alloca (size_t size)

# 33 alloue size octets dans l’espace de pile de l’appelant


 Donc, quand on retourne de la fonction qui a appelé alloca, l’espace alloué est
automatiquement libéré
 Problème : On risque de déborder de la pile. . . sans aucun avertissement du système

& %

' $
2.3.5 Mécanisme d’allocation/désallocation dédié

 Principe
 Au démarrage du programme, on construit une liste chaînée de blocs de N octets

# 34  Quand on a besoin d’allouer un bloc de N octets, on récupère le premier


élément de cette liste
 Quand on veut désallouer un bloc de N octets, on remet ce bloc en tête de liste
 Avantage : Vitesse d’allocation/désallocation
 Inconvénient : Les blocs alloués/désalloués ont tous la même taille

& %

L’algorithme de malloc étant très efficace, le mécanisme présenté ici n’est en général pas nécessaire.

Il peut toutefois s’imposer dans le cas où l’on doit faire une allocation/désallocation dans un gestionnaire
de signal (dans lequel les opérations de malloc/free sont proscrites).

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 48


2 Point de vue processus 2.4 Déverminage des accès mémoire
' $
2.3.6 Allocation/Désallocation mémoire au niveau système

 void *sbrk(ptrdiff_t incrément)


incrémente l’espace de données du programme de incrément octets

# 35  void * mmap(void *start, size_t length, int prot, int flags, int
fd, off_t offset)
Permet de mapper un fichier en mémoire. En plus des options qui seront étudiées
dans le chapitre « Entrées/sorties », flags peut prendre la valeur MAP_ANON (ou
MAP_ANONYMOUS) pour indiquer qu’on ne souhaite pas réellement travailler sur un
fichier, mais sur une zone mémoire vierge emplie de zéros

& %

sbrk n’est pas un appel système, juste une fonction de la bibliothèque C qui invoque l’appel système int
brk(void *fin_segment_donnée).

' $
2.4 Déverminage des accès mémoire

# 36 2.4.1 Détecter statiquement les accès erronés à des zones allouées dynamiquement . . 37
2.4.2 Détecter dynamiquement les accès erronés à des zones allouées dynamiquement38
2.4.3 Visualiser quand une zone mémoire est accédée/modifée . . . . . . . . . . . . . . . . . . . . . . 39

& %

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 49


2 Point de vue processus 2.4 Déverminage des accès mémoire
' $
2.4.1 Détecter statiquement les accès erronés à des zones
allouées dynamiquement

 Objectif : quand un programme accède à une zone allouée dynamiquement, vérifier,


au moment de la compilation, que
 Il n’accède pas en dehors de la zone (ou que la zone n’est pas libérée)
 Il ne lit pas un octet jamais initialisé
# 37
 ...
 Outils
 insure++ (société Parasoft) : propose un outil d’analyse des sources
 splint (logiciel libre, http://www.splint.org/) : outre des contrôles
« paranoïaques » sur le source, vérifie certains problèmes d’accès mémoire
 Limite = Les vérifications ne peuvent dépasser le cadre d’un source

& %

Pour illustrer la puissance de splint (et valgrind au slide suivant), étudiez le programme C suivant
(qui s’exécute correctement). Il contient 3 erreurs. Les voyez-vous ?
/********************/
/* jeuDes3Erreurs.c */
/********************/

/* Ce programme a pour objectif d’illustrer la fonction strcpy */


/* Il s’execute correctement. Pourtant, il contient 3 erreurs ! */
/* Pouvez-vous les retrouver ? */
#include <string.h>
#include <stdlib.h>
#include <stdio.h>

#define CITATION "Faites que le rêve dévore votre vie, afin que la vie ne dévore pas votre rêve." // Antoine de Saint-Exupery

int main(){
char *p = NULL;

/* Reservation d’une zone pour accueillir la citation */


p = malloc(strlen(CITATION));
if (p == NULL){
fprintf(stderr,"malloc p NOK");
return EXIT_FAILURE;
}

/* On met ’\0’ au niveau du premier caractere pointe par p pour que */


/* le printf qui suit n’affiche pas n’importe quoi */
p[1] = ’\0’;
printf("Contenu pointe par p avant strcpy : \"%s\"\n", p);

/* C’est parti pour le strcpy */


strcpy(p,CITATION);

/* On regarde l’affichage */
printf("Contenu pointe par p apres strcpy : \"%s\"\n", p);

/* On libere la zone pointee par p puisqu’elle est desormais inutile */


free(p);

// A T T E N T I O N
// S’il avait ecoute les conseils de ce poly, le programmeur de ce
// code source aurait mis la ligne
// p = NULL;
// apres ce free. Mais certaines personnes ne peuvent s’empecher de
// ne pas ecouter ce qu’on leur dit... A moins que cet oubli ne soit
// volontaire...
// A T T E N T I O N

/* On verifie que l’affichage de p conduit desormais a une erreur */


printf("Contenu pointe par p apres free : \"%s\"\n", p);

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 50


2 Point de vue processus 2.4 Déverminage des accès mémoire

return EXIT_SUCCESS;
}

L’exécution de splint (splint jeuDes3Erreurs.c) donne l’affichage suivant (qui devrait contribuer à la
localisation de certaines des 3 erreurs ; pour les autres, il faudra utiliser des outils de détection dynamique) :
Splint 3.1.1 --- 21 Apr 2006

jeuDes3Erreurs.c: (in function main)


jeuDes3Erreurs.c:25:58: Passed storage p not completely defined (*p is
undefined): printf (..., p, ...)
Storage derivable from a parameter, return value or global is not defined.
Use /*@out@*/ to denote passed or returned storage which need not be defined.
(Use -compdef to inhibit warning)
jeuDes3Erreurs.c:16:3: Storage *p allocated
jeuDes3Erreurs.c:37:56: Variable p used after being released
Memory is used after it has been released (either by passing as an only param
or assigning to an only global). (Use -usereleased to inhibit warning)
jeuDes3Erreurs.c:34:8: Storage p released

Finished checking --- 2 code warnings

Noter le premier warning (peu explicite :-()˙ qui correspond au fait qu’on a écrit p[1] = ’\0’; dans le source
(si on met p[0], l’erreur disparaît).
' $
2.4.2 Détecter dynamiquement les accès erronés à des zones
allouées dynamiquement

 Objectif : quand un programme accède à une zone allouée dynamiquement, vérifier, à


l’exécution, que
 Il n’accède pas en dehors de la zone (ou que la zone n’est pas libérée)
 ...

# 38  Outils
 insure++ (société Paradox ) : au moment de la compilation, cet outil ajoute aux sources
(.c) des instructions de contrôle au niveau des différentes instructions d’affectation
 Rational Purify (société IBM) : au moment du link, cet outil ajoute aux objets (.o)
des instructions de contrôle au niveau des différentes instructions (assembleur)
d’affectation
 valgrind (logiciel libre, http://valgrind.org) : l’exécutable est lu par un simulateur
de processeur x86 qui se charge de détecter toutes les anomalies d’accès

 Limite = On n’est jamais sûr d’être passé par toutes les branches
& %

NB :

1. Dans la pratique, il est recommandé d’utiliser splint (avant de se lancer dans les tests unitaires et les
tests d’intégration), puis d’utiliser insure++, rational Purify ou valgrind.

2. Il existe d’autres outils (évoqué ci-dessous)). La page http://valgrind.org/gallery/survey_03/q4.


html contient une liste assez exhaustive avec un comparatif (objectif ?) par rapport à valgrind.

3. valgrind ne s’exécute qu’en environnement Linux(/Mac). La page http://wiki.winehq.org/Wine_


and_Valgrind explique comment analyser une application Windows avec valgrind.

4. Linux propose d’autres outils nettement moins puissants qu’insure++, rational Purify et valgrind,
mais envisageables dans des cas extrêmes où les outils présentés jusqu’à présent ne seraient pas utili-
sables (contrainte de temps d’exécution, par exemple) :

• mpatrol utilise une version spéciale de malloc et free (ainsi que d’autres fonctions travaillant
sur la mémoire, comme memset par exemple) de manière à repérer des débordements de zone.

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 51


2 Point de vue processus 2.4 Déverminage des accès mémoire

• mtrace est une extension GNU (man 3 mtrace) qui permet un suivi intégré des alloca-
tions/désallocations
On peut ainsi repérer des fuites mémoire
• On peut citer enfin electric fence qui est complètement obsolète par rapport aux outils précé-
dents.

5. Pour information :

• la société Free utilise également les outils stanse (http://stanse.fi.nuini.cz), clang


(clang-analyzer.llvm.org) et smatch (http://smatch.sourceforge.net) ;
• la société Airbus utilise également l’outil frama-C.

L’investigation de ces outils sort du cadre de ce cours.

Pour illustrer la puissance de ces outils, reprenons le programme C (qui s’exécute correctement) présenté
en commentaire du transparent précédent . Il contient 3 erreurs. Les voyez-vous ?
L’exécution de valgrind (valgrind --̇tool=memcheck --̇leak-check=yes jeuDes3Erreurs) sur un
exécutable compilé avec l’option -g donne l’affichage suivant (qui devrait contribuer à la localisation de
ces 3 erreurs) :
==2402== Memcheck, a memory error detector for x86-linux.
==2402== Copyright (C) 2002-2005, and GNU GPL’d, by Julian Seward et al.
==2402== Using valgrind-2.4.0, a program supervision framework for x86-linux.
==2402== Copyright (C) 2000-2005, and GNU GPL’d, by Julian Seward et al.
==2402== For more details, rerun with: -v
==2402==
==2402== Conditional jump or move depends on uninitialised value(s)
==2402== at 0xB2A4CF: vfprintf (in /lib/libc-2.3.5.so)
==2402== by 0xB2F942: printf (in /lib/libc-2.3.5.so)
==2402== by 0x80484DC: main (jeuDes3Erreurs.c:25)
==2402==
==2402== Invalid write of size 1
==2402== at 0x1B90A6BA: memcpy (mac_replace_strmem.c:285)
==2402== by 0x80484F9: main (jeuDes3Erreurs.c:28)
==2402== Address 0x1B92B073 is 0 bytes after a block of size 75 alloc’d
==2402== at 0x1B909222: malloc (vg_replace_malloc.c:130)
==2402== by 0x8048499: main (jeuDes3Erreurs.c:16)
==2402==
==2402== Invalid read of size 1
==2402== at 0xB2A4CF: vfprintf (in /lib/libc-2.3.5.so)
==2402== by 0xB2F942: printf (in /lib/libc-2.3.5.so)
==2402== by 0x804850C: main (jeuDes3Erreurs.c:31)
==2402== Address 0x1B92B073 is 0 bytes after a block of size 75 alloc’d
==2402== at 0x1B909222: malloc (vg_replace_malloc.c:130)
==2402== by 0x8048499: main (jeuDes3Erreurs.c:16)
==2402==
==2402== Invalid read of size 1
==2402== at 0xB2A4CF: vfprintf (in /lib/libc-2.3.5.so)
==2402== by 0xB2F942: printf (in /lib/libc-2.3.5.so)
==2402== by 0x804852D: main (jeuDes3Erreurs.c:37)
==2402== Address 0x1B92B028 is 0 bytes inside a block of size 75 free’d
==2402== at 0x1B909743: free (vg_replace_malloc.c:152)
==2402== by 0x804851A: main (jeuDes3Erreurs.c:34)
==2402==
==2402== Invalid read of size 1
==2402== at 0xB4B94D: _IO_file_xsputn@@GLIBC_2.1 (in /lib/libc-2.3.5.so)
==2402== by 0xB2A140: vfprintf (in /lib/libc-2.3.5.so)
==2402== by 0xB2F942: printf (in /lib/libc-2.3.5.so)
==2402== by 0x804852D: main (jeuDes3Erreurs.c:37)
==2402== Address 0x1B92B072 is 74 bytes inside a block of size 75 free’d
==2402== at 0x1B909743: free (vg_replace_malloc.c:152)
==2402== by 0x804851A: main (jeuDes3Erreurs.c:34)
==2402==
==2402== Invalid read of size 1
==2402== at 0xB4B95F: _IO_file_xsputn@@GLIBC_2.1 (in /lib/libc-2.3.5.so)
==2402== by 0xB2A140: vfprintf (in /lib/libc-2.3.5.so)
==2402== by 0xB2F942: printf (in /lib/libc-2.3.5.so)
==2402== by 0x804852D: main (jeuDes3Erreurs.c:37)
==2402== Address 0x1B92B071 is 73 bytes inside a block of size 75 free’d
==2402== at 0x1B909743: free (vg_replace_malloc.c:152)
==2402== by 0x804851A: main (jeuDes3Erreurs.c:34)
==2402==
==2402== Invalid read of size 1
==2402== at 0xB57465: mempcpy (in /lib/libc-2.3.5.so)
==2402== by 0xB2A140: vfprintf (in /lib/libc-2.3.5.so)
==2402== by 0xB2F942: printf (in /lib/libc-2.3.5.so)
==2402== by 0x804852D: main (jeuDes3Erreurs.c:37)

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 52


2 Point de vue processus 2.4 Déverminage des accès mémoire

==2402== Address 0x1B92B028 is 0 bytes inside a block of size 75 free’d


==2402== at 0x1B909743: free (vg_replace_malloc.c:152)
==2402== by 0x804851A: main (jeuDes3Erreurs.c:34)
==2402==
==2402== Invalid read of size 2
==2402== at 0xB5746A: mempcpy (in /lib/libc-2.3.5.so)
==2402== by 0xB2A140: vfprintf (in /lib/libc-2.3.5.so)
==2402== by 0xB2F942: printf (in /lib/libc-2.3.5.so)
==2402== by 0x804852D: main (jeuDes3Erreurs.c:37)
==2402== Address 0x1B92B029 is 1 bytes inside a block of size 75 free’d
==2402== at 0x1B909743: free (vg_replace_malloc.c:152)
==2402== by 0x804851A: main (jeuDes3Erreurs.c:34)
==2402==
==2402== Invalid read of size 4
==2402== at 0xB5746C: mempcpy (in /lib/libc-2.3.5.so)
==2402== by 0xB2A140: vfprintf (in /lib/libc-2.3.5.so)
==2402== by 0xB2F942: printf (in /lib/libc-2.3.5.so)
==2402== by 0x804852D: main (jeuDes3Erreurs.c:37)
==2402== Address 0x1B92B02B is 3 bytes inside a block of size 75 free’d
==2402== at 0x1B909743: free (vg_replace_malloc.c:152)
==2402== by 0x804851A: main (jeuDes3Erreurs.c:34)
==2402==
==2402== ERROR SUMMARY: 174 errors from 9 contexts (suppressed: 13 from 1)
==2402== malloc/free: in use at exit: 0 bytes in 0 blocks.
==2402== malloc/free: 1 allocs, 1 frees, 75 bytes allocated.
==2402== For counts of detected errors, rerun with: -v
==2402== No malloc’d blocks -- no leaks are possible.

NB : si vous faites une édition de lien statique de jeuDes3Erreurs, puis que vous exécutez valgrind
--̇tool=memcheck --̇leak-check=yes jeuDes3Erreurs, vous constaterez que valgrind détecte des erreurs
avant le main de votre programme. La version statique de la libc comtient donc des problèmes d’accès
mémoire.

$ cc -g -static jeuDes3Erreurs.c -o jeuDes3Erreurs


$ valgrind ./jeuDes3Erreurs
==19314== Memcheck, a memory error detector
==19314== Copyright (C) 2002-2013, and GNU GPL’d, by Julian Seward et al.
==19314== Using Valgrind-3.10.0.SVN and LibVEX; rerun with -h for copyright info
==19314== Command: ./jeuDes3Erreurs
==19314==
==19314== Conditional jump or move depends on uninitialised value(s)
==19314== at 0x437396: __linkin_atfork (in /tmp/jeuDes3Erreurs)
==19314== by 0x415783: ptmalloc_init.part.7 (in /tmp/jeuDes3Erreurs)
==19314== by 0x415B0D: malloc_hook_ini (in /tmp/jeuDes3Erreurs)
==19314== by 0x468072: _dl_get_origin (in /tmp/jeuDes3Erreurs)
==19314== by 0x437A3E: _dl_non_dynamic_init (in /tmp/jeuDes3Erreurs)
==19314== by 0x438837: __libc_init_first (in /tmp/jeuDes3Erreurs)
==19314== by 0x401301: (below main) (in /tmp/jeuDes3Erreurs)
==19314==
==19314== Conditional jump or move depends on uninitialised value(s)
==19314== at 0x4111B9: _int_free (in /tmp/jeuDes3Erreurs)
==19314== by 0x461B80: fillin_rpath (in /tmp/jeuDes3Erreurs)
==19314== by 0x46225E: _dl_init_paths (in /tmp/jeuDes3Erreurs)
==19314== by 0x437F1C: _dl_non_dynamic_init (in /tmp/jeuDes3Erreurs)
==19314== by 0x438837: __libc_init_first (in /tmp/jeuDes3Erreurs)
==19314== by 0x401301: (below main) (in /tmp/jeuDes3Erreurs)
==19314==
==19314== Conditional jump or move depends on uninitialised value(s)
==19314== at 0x41121F: _int_free (in /tmp/jeuDes3Erreurs)
==19314== by 0x461B80: fillin_rpath (in /tmp/jeuDes3Erreurs)
==19314== by 0x46225E: _dl_init_paths (in /tmp/jeuDes3Erreurs)
==19314== by 0x437F1C: _dl_non_dynamic_init (in /tmp/jeuDes3Erreurs)
==19314== by 0x438837: __libc_init_first (in /tmp/jeuDes3Erreurs)
==19314== by 0x401301: (below main) (in /tmp/jeuDes3Erreurs)

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 53


Gestion de la mémoire
' $
2.4.3 Visualiser quand une zone mémoire est accédée/modifée

 Watchpoint sous gdb


 gdb offre la possibilité de mettre en place des watchpoint sur des zones
mémoire, en lecture ou en lecture/écriture.
 Si une telle zone mémoire est accédée, gdb stoppe le programme.
# 39
 Contrôle de l’accès à la mémoire
 int mprotect(const void *addr, size_t *len, int prot)
contrôle les autorisations d’accès (lecture et/ou écriture) à une portion de la
mémoire. Si un accès interdit se produit, le programme reçoit SIGSEGV
 Utilisation : toute situation où une donnée est modifiée et le contexte ne permet
pas d’utiliser des outils de type valgrind ou gdb

& %
mprotect(2) impose la contrainte suivante : l’adresse de début de zone doit être alignée sur une frontière
de page.
Bibliographie du chapitre
[Blaess, 2002] Blaess, C. (2002). Programmation système en C sous Linux : signaux, processus, threads, IPC
et sockets. Eyrolles, Paris, Paris, France.

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 54


Les fichiers (et les entrées-sorties)

Michel Simatic

module CSC4508/M2

Avril 2016

55
Les fichiers (et les entrées-sorties)
' $
Plan du document

1 Primitives Unix d’entrées-sorties . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5


2 Bibliothèque C d’entrées-sorties . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
#2 3 Projection des fichiers en mémoire . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
4 Manipulation des i-noeuds du système de fichiers Unix . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
5 Entrées-sorties sur répertoires . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
6 Limitations de NFS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28

& %

Lors de ce cours, on parlera surtout de fichiers, car c’est l’exemple d’entrées-sorties le plus facilement
manipulable. Toutefois, bien noter que le contenu des 3 premières sections s’applique aux entrées-sorties
autres que les fichiers.
Rappel sur les fichiers (notions vues en première année) :

• Un fichier est une suite d’octets contigus stockés dans un support (par exemple, un disque) sous un
nom (le « nom du fichier »).

• On distingue les fichiers :

– Texte : contenant des octets affichables à l’écran. Ce type de fichiers est constitué de lignes
identifiées par le caractère de fin de ligne (sous Unix, caractère de code ASCII 10 alors que sous
Windows, caractère de code ASCII 10 suivi d’un caractère de code ASCII 13)
– Binaire : contenant des octets non affichables à l’écran.

Sous Unix, les commandes hexdump -C nomFichier et bless nomFichier permettent de visualiser le
contenu d’un fichier de manière précise. Utilisez-les pour comparer le contenu de helloWorldUnix.c
et helloWorldWindows.c.

• Quand on « ouvre » un fichier, le système d’exploitation fournit une notion de position courante
(appelée parfois offset dans la suite de ce cours) de lecture/écriture.

– Cette position courante détermine le rang de l’octet dans le fichier à partir duquel les primitives
système liront ou bien écriront des octets dans le fichier.
– Cet offset avance à chaque fois qu’on effectue une lecture ou une écriture.
– Le système d’exploitation met à disposition de l’utilisateur des primitives pour changer explicite-
ment cette position courante (sans lire ou écrire des octets).

• La « fin d’un fichier » correspond à l’endroit situé derrière le dernier octet du fichier. Quand on est
à la fin du fichier, on ne peut pas lire d’octets. En revanche, on peut écrire des octets (selon le mode
dans lequel on a ouvert le fichier).

• Il existe 3 modes d’accès à un fichier :

– Séquentiel : Les octets sont lus les uns à la suite des autres à partir du début du fichier.
– Direct : On peut peut positionner l’offset sans avoir besoin de lire les octets situés avant l’offset.

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 56


1 Primitives Unix d’entrées-sorties 1.1 Primitives de base

– Séquentiel indexé : Le fichier contient des enregistrements, chaque enregistrement étant identifié
par une clé (unique ou non). À l’aide de la clé, on peut se positionner au début d’un enregistrement.
On peut également lire les enregistrements dans l’ordre définis par leur clé.

Le système Linux et la librairie C offrent les modes d’accès séquentiels et directs. Pour un mode d’accès
séquentiel indexé, il faut s’appuyer sur une librairie (Unix NDBM, GDBM, Oracle Berkeley DB. . .).

' $
1 Primitives Unix d’entrées-sorties

1.1 Primitives de base . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5


#3 1.3 Duplication de descripteur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
1.3 Contrôle des entrée-sorties . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
1.4 Manipulation de l’offset . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
1.5 Gestion de la lenteur ou du blocage des entrées-sorties* . . . . . . . . . . . . . . . . . . . . . . . . 14

& %

' $
1.1 Primitives de base

#4 1.1.1 Ouverture/fermeture de fichier . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5


1.1.2 Lecture sur descripteur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.1.3 Écriture sur descripteur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7

& %

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 57


1 Primitives Unix d’entrées-sorties 1.1 Primitives de base
' $
1.1.1 Ouverture/fermeture de fichier
 int open(const char *path, int flags, mode_t mode) : retour = f_id
flags peut prendre l’une des valeurs suivantes :
 O_RDONLY : lecture seulement
 O_WRONLY : écriture seulement
 O_RDWR : écriture et lecture
associée à :
#5
 O_APPEND : écritures en fin de fichier
 O_TRUNC : remise à vide du fichier
 O_CREAT : création si le fichier n’existe pas. Les droits sont (mode &˜umask)
 O_SYNC : ouverture du fichier en écriture synchronisée
 O_NONBLOCK (ou O_NDELAY) : open et les opérations ultérieures faites sur le
descripteur ne pourront pas se bloquer.

 int close(int desc)


& %

A propos de l’option O_SYNC dans open :

• Pour gagner en performance, par défaut, lors d’une opération d’écriture, le système d’exploitation
n’écrit pas physiquement les octets sur le disque (ils sont gardés dans des caches du noyau en attente
d’un moment considéré comme favorable par le système pour réaliser l’écriture sur disque)

• De ce fait, en cas d’arrêt brutal de la machine (exemple : coupure de courant) :

– Des données qu’on croyait avoir écrites sur disque peuvent être perdues car elles étaient en fait
en mémoire

– Il y aussi risque d’incohérence sur les données qu’on trouve dans le fichier

• Solutions pour synchroniser des données de fichier en mémoire avec le disque :

– Synchronisation implicite (i.e. à chaque écriture) : ajout de l’option O_SYNC au moment du open

– Synchronisation explicite (i.e. sur décision de l’application) via la primitive

int fsync(int fd)

L’utilisation de O_NONBLOCK est détaillée en section 1.5.


Noter qu’on peut aussi créer un fichier à l’aide de la primitive creat :

int creat(const char *path, mode_t mode) : retour = f_id

qui est équivalent à l’appel suivant de open :

open(path, O_WRONLY|O_CREAT|O_TRUNC, mode).

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 58


1 Primitives Unix d’entrées-sorties 1.1 Primitives de base
' $
1.1.2 Lecture sur descripteur

 ssize_t read(int fd, void *buf, size_t count) : retour = nombre d’octets
lus
 Au retour de read, la zone buf est modifiée avec le résultat de la lecture
 Dans le cas d’un fichier, le nombre d’octets lus peut ne pas être égal à count :
#6 I On se trouve à la fin du fichier
I On a fait une lecture non bloquante et les données étaient verrouillées de
manière exclusive

 ssize_t readv(int fd, const struct iovec *iov, int iovcnt)


 Au retour de readv, les zones référencées par les iovcnt éléments du tableau
iov sont modifiées avec le résultat de la lecture

& %

Dans le cas où la fonction read est utilisée sur un descripteur autre qu’un fichier (tube, socket), le fait
que le nombre d’octets lus peut ne pas être égal à count peut avoir d’autres significations :
• Pour un tube de communication (cf. chapitre Communications Inter-processus), le correspondant a
fermé son extrémité du tube.
• Pour une socket (cf. cours CSC4509), le protocole réseau utilise des paquets de données de taille
inférieure à celle qui est réclamée.

' $
1.1.3 Écriture sur descripteur

 ssize_t write(int fd, const void *buf, size_t count) : retour = nombre
d’octets écrits
 Dans le cas d’un fichier, le retour (sans erreur) de l’écriture signifie que :
I Les octets ont été écrits dans les caches du noyau si pas de O_SYNC au
moment du open
I Les octets ont été écrits sur disque si O_SYNC au moment du open.
#7
 Dans le cas d’un fichier, si le nombre d’octets écrits est différent de count, cela
signifie une erreur (disque plein, par exemple)
 ssize_t writev(int fd, const struct iovec *iov, int iovcnt)
 Au retour de writev, les zones référencées par les iovcnt éléments du tableau
iov ont été écrites sur le descripteur fd
 int posix_fallocate(int fd, off_t offset, off_t len)
 Garantit que l’espace disque requis pour le fichier est effectivement alloué
& %
L’écriture sur disque est atomique : si deux processus P1 et P2 écrivent simultanément dans le même
fichier au même endroit, lorsque les deux processus auront fini leur écriture, on trouvera à cet endroit :
• Soit l’écriture faite par P1 ,

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 59


1 Primitives Unix d’entrées-sorties 1.1 Primitives de base

• Soit l’écriture faite par P2 ,


• Mais jamais un mélange des écritures faites par P1 et P2 .
Noter que quand le fichier est ouvert avec l’option O_APPEND, si P1 et P2 écrivent simultanément (à la fin
du fichier, puisqu’on a l’option O_APPEND), lorsque les deux processus auront fini leur écriture, on trouvera
en fin de fichier :
• Soit l’écriture faite par P1 suivie de celle faite par P2 ,
• Soit l’écriture faite par P2 suivie de celle faite par P1 .
Aucune écriture n’est donc perdue ! Attention, cette écriture simultanée en fin de fichier n’est pas équivalente
à deux processus exécutant simultanément les opérations :
lseek(fd,0,SEEK_END); /* Fixe la position courante à la fin du fichier */
write(fd,data,taille);

En effet, dans ce dernier cas, l’une des écritures risque d’être écrasée par l’autre.
Le fichier copier.c de la page suivante illustre l’utilisation de open, read, write et close.
/************/
/* copier.c */
/************/
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <stdio.h>

#define USAGE "USAGE = copier nomSource nomDest\n"


#define ERRECRIT "Erreur d’ecriture (disque plein ?)\n"

int source, dest;


int buf;
int nbCarLu, nbCarEcrit;

int main(int argc, char *argv[]) {


if (argc != 3) {
write(STDERR_FILENO, USAGE, strlen(USAGE));
return EXIT_FAILURE;
}
source = open(argv[1], O_RDONLY);
if (source < 0) {
perror(argv[1]);
return EXIT_FAILURE;
}
dest = open(argv[2],
O_WRONLY|O_CREAT|O_TRUNC,
S_IRWXU|S_IRWXG|S_IRWXO);
if (dest < 0) {
perror(argv[2]);
return EXIT_FAILURE;
}
while ((nbCarLu = read(source, (void*)&buf, sizeof(buf))) > 0) {
nbCarEcrit = write(dest, (void*)&buf, nbCarLu);
if (nbCarEcrit <= 0) {
if (nbCarEcrit == 0) {
write(STDERR_FILENO, ERRECRIT, strlen(ERRECRIT));
}
else {
perror("write");
}
return EXIT_FAILURE;
}
}
if (nbCarLu < 0) {
perror("read");
return EXIT_FAILURE;
}
if (close(source) < 0) {
perror(argv[1]);
return EXIT_FAILURE;
}
if (close(dest) < 0) {
perror(argv[2]);
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 60


1 Primitives Unix d’entrées-sorties 1.3 Contrôle des entrée-sorties

Cette opération de recopie du contenu d’un fichier vers un autre descripteur est une opération fréquem-
ment réalisée dans les serveurs Web. En effet, ces serveurs doivent notamment envoyer le contenu de fichiers
vers des navigateurs qui leur en ont fait la demande. C’est pourquoi le système Linux offre la primitive
sendfile (ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count)). Elle permet
de lire count octets de in_fd pour les écrire sur out_fd (qui doit correspondre à une socket). sendfile est
plus performant que la combinaison read/write.
La fonction fallocate est la version spécifique Linux de la fonction (portable) posix_fallocate.

' $
1.2 Duplication de descripteur

 Mécanisme principalement utilisé pour réaliser des redirections des trois fichiers
d’entrée-sorties standard.
 int dup(int fdACloner) : retour = nouveauFd
#8 associe le plus petit descripteur disponible du processus appelant à la même entrée
dans la table des fichiers ouverts que le descripteur fdACloner
 int dup2(int fdACloner, int fdClone)
force le descripteur fdClone à devenir un synonyme du descripteur fdACloner. Si
le descripteur fdClone n’est pas disponible, le système réalise au préalable une
opération de fermeture close(fdClone)

& %

' $
1.3 Contrôle des entrée-sorties

#9 1.3.1 Primitive fcntl . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10


1.3.2 Verrouillage de fichier . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
1.4.0 Conseil au noyau pour les lectures . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13

& %

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 61


1 Primitives Unix d’entrées-sorties 1.3 Contrôle des entrée-sorties
' $
1.3.1 Primitive fcntl

 int fcntl(int fd, int cmd, ...) : retour fonction du type d’opération
permet la réalisation d’un certain nombre d’opérations à différents niveaux dans les
tables du système
 Les commandes possibles sont :
# 10
 F_GETFL : Valeur des attributs utilisés au moment de l’ouverture du fichier
 F_SETFL : Positionnement de certains de ces attributs (NB : ces attributs sont
communs à tous les processus manipulant le fichier concerné)
On peut ainsi positionner O_ASYNC ou O_NONBLOCK après création du descripteur
de fichier.
 Opérations liées au verrouillage

& %

' $
1.3.2 Verrouillage de fichier

 Les verrous sont attachés à un i-nœud. Donc, l’effet d’un verrou sur un fichier est
visible au travers de tous les descripteurs (et donc tous les fichiers ouverts)
correspondant à ce nœud
 Un verrou est la propriété d’un processus : ce processus est le seul habilité à le
# 11
modifier ou l’enlever
 Les verrous ont une portée de [entier1 : entier2] ou [entier : ∞]
 Les verrous ont un type :
 partagé (shared)
 exclusif (exclusive)

& %

Le fichier vExcl.c illustre la pose d’un verrou exclusif :


/***********/
/* vExcl.c */
/***********/
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
int main(){
int fd;
struct flock verrou;

fd = open("/tmp/ficTest",O_RDWR|O_CREAT, S_IRWXU|S_IRWXG|S_IRWXO);
if (fd < 0) {

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 62


1 Primitives Unix d’entrées-sorties 1.3 Contrôle des entrée-sorties

perror("open");
exit(EXIT_FAILURE);
}

/* Verrou exclusif sur le 15è caractère */


verrou.l_type = F_WRLCK;
verrou.l_whence = SEEK_SET;
verrou.l_start = 15;
verrou.l_len = 1;
/* A cause du parametre F_SETLKW, on se bloque sur le fcntl si jamais */
/* le verrou ne peut pas etre pris */
printf("tentative de pose de verrou (exclusif) par processus %d...\n",
getpid());
if (fcntl(fd, F_SETLKW, &verrou) < 0){
perror("Pose verrou");
exit(EXIT_FAILURE);
}
printf("... Verrou (exclusif) pose par processus %d\n", getpid());

/* Ici on pourrait faire le traitement qui necessitait d’etre protege */


/* par le verrou */
sleep(10);

/* Liberation du verrou */
printf("Relachement verrou par processus %d...\n", getpid());
verrou.l_type = F_UNLCK;
verrou.l_whence = SEEK_SET;
verrou.l_start = 15;
verrou.l_len = 1;
if (fcntl(fd, F_SETLK, &verrou) < 0){
perror("Relachement verrou");
exit(EXIT_FAILURE);
}
printf("...OK\n");

return EXIT_SUCCESS;
}

Le fichier vPart.c illustre la pose d’un verrou partagé :


/***********/
/* vPart.c */
/***********/
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
int main(){
int fd;
struct flock verrou;

fd = open("/tmp/ficTest",O_RDWR|O_CREAT, S_IRWXU|S_IRWXG|S_IRWXO);
if (fd < 0) {
perror("open");
exit(EXIT_FAILURE);
}

/* Verrou partage sur le 15e caractere */


verrou.l_type = F_RDLCK;
verrou.l_whence = SEEK_SET;
verrou.l_start = 15;
verrou.l_len = 1;
/* A cause du parametre F_SETLKW, on se bloque sur le fcntl si jamais */
/* le verrour ne peut pas etre pris */
printf("tentative de pose de verrou (partage) par processus %d...\n",
getpid());
if (fcntl(fd, F_SETLKW, &verrou) < 0){
perror("Pose verrou");
exit(EXIT_FAILURE);
}
printf("...Verrou (partage) pose par %d\n", getpid());

/* Ici on pourrait faire le traitement qui necessitait d’etre protege */


/* par le verrou */
sleep(10);

/* Liberation du verrou */
printf("Relachement verrou par processus %d...\n", getpid());
verrou.l_type = F_UNLCK;
verrou.l_whence = SEEK_SET;
verrou.l_start = 15;
verrou.l_len = 1;
if (fcntl(fd, F_SETLK, &verrou) < 0){
perror("Relachement verrou");

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 63


Les fichiers (et les entrées-sorties) 1 Primitives Unix d’entrées-sorties

exit(EXIT_FAILURE);
}
printf("...OK\n");

return EXIT_SUCCESS;
}

• Si on exécute vExcl en premier, un autre vExcl ou un vPart doivent attendre avant de pouvoir poser
leur verrou.

• Si on exécute vPart en premier, un autre vPart peut poser le verrou (partagé). En revanche, un vExcl
doit attendre avant de pouvoir poser son verrou.

• Noter qu’on peut avoir une famine de vExcl :

– On démarre un 1er vPart.


– On démarre vExcl : il se met en attente.
– On démarre un 2e vPart. Le 1er vPart se termine. Mais comme le 2è vPart est en train de s’exécuter,
vExcl attend encore.
– On démarre un 3e vPart. Le 2e vPart se termine. Mais comme le 3e vPart est en train de s’exécuter,
vExcl attend encore.
– On constate que tant que des vPart démarrent alors que le vPart précédent n’a pas fini de
s’exécuter, vExcl doit attendre : il peut donc y avoir une famine de vExcl.

Pour empêcher cette famine, il faut ajouter une exclusion mutuelle (cf. chapitre « Synchronisation de
processus »).

' $
1.3.3 Conseil au noyau pour les lectures

 int posix_fadvise(int fd, off_t offset, off_t len, int advice) :


# 12
retour = 0 si OK, numéro d’erreur sinon
permet d’annoncer au noyau la manière dont on va accéder à un fichier, ce qui
permet au noyau de faire des optimisations appropriées

& %

Depuis janvier 2011, on sait que cette fonction est utilisée dans les prochaines versions de Fi-
refox pour gagner 40% à 50% de temps au moment du démarrage en chargeant plus efficace-
ment les deux librairies de l’interface graphique, xul.dll et mozjs.dll (plus d’informations en
https://bugzilla.mozilla.org/show_bug.cgi?id=627591).

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 64


Les fichiers (et les entrées-sorties) 1 Primitives Unix d’entrées-sorties
' $
1.4 Manipulation de l’offset

 off_t lseek(int fd, off_t unOffset, int origine) : retour = nouvel


emplacement
permet de manipuler l’offset du fichier
 origine permet de préciser la manière de prendre en compte le paramètre offset :
# 13  SEEK_SET : of f set = unOf f set
 SEEK_CUR : of f set = of f set + unOf f set
 SEEK_END : of f set = f inF ichier + unOf f set

 pread (respectivement pwrite) fait une lecture (respectivement une écriture)


comme read (respectivement write), mais permet en plus de préciser un offset.
NB : L’offset du fichier n’est pas modifié

& %

lseek peut être utilisé pour faire un fichier d’index permettant de retrouver plus rapidement des infor-
mations dans un fichier textuel (cf. exercice Entrées-sorties/5). Toutefois, avant d’engager un développement
spécifique et coûteux basé sur lseek, toujours vérifier que la fonctionnalité recherchée n’existe pas déjà dans
une librairie (Open Source) comme Unix NDBM, GDBM ou Oracle Berkeley DB (voir commentaire de la
section 2.6).
' $
1.5 Gestion de la lenteur ou du blocage des entrées-sorties*
 La réception d’un signal par un processus arrête toute entrée-sortie en cours dans ce
processus (errno est positionné à EINTR).
 Dans le cas où le descripteur est marqué avec l’option O_NONBLOCK : lorsque le
programme invoque une primitive d’entrée-sortie dont le système détecte qu’elle se
bloquerait, la primitive renvoie immédiatement une erreur et positionne errno à
EAGAIN ou EWOULDBLOCK (selon que fichier ou socket).
# 14  Le programme peut s’appuyer sur les primitives d’entrées-sorties asynchrones
aio_read et aio_write. Chacune déclenche l’exécution d’une entrée-sortie de
manière asynchrone :
 Elle ne bloque pas l’exécution du programme ;
 Quand son exécution est terminée (qu’elle se soit bien passée ou qu’il y ait eu
une erreur), un signal est envoyé au programme qui, dans le gestionnaire de
signal, peut récupérer avec aio_return et aio_error le résultat de l’exécution
de l’entrée-sortie asynchrone.
Les entrées-sorties asynchrones sont déconseillées sous Linux.
& %

NB : L’« * » dans son titre signale que ce transparent est un transparent d’approfondissement du cours
CSC4508.

• Vu que la réception d’un signal par un processus arrête toute entrée-sortie en cours, on invoque souvent
la primitive setitimer avant d’invoquer une opération d’entrée-sortie. Ainsi le système envoie un
signal (en général SIGALRM) et arrête donc l’entrée-sortie si cette dernière est restée bloquée plus d’un

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 65


Les fichiers (et les entrées-sorties)

certain temps. Noter que, si on utilise des signaux dans le programme, il faut prévoir la mise en
place d’un gestionnaire de signal (avec sigaction) de sorte que le signal soit géré par le programme
(éventuellement en ne faisant rien). Ainsi le programme ne s’arrête pas parce que le signal n’est pas
géré par le programme.

• En théorie, l’utilisation de O_NONBLOCK pourrait permettre de gérer plusieurs descripteur d’entrées-


sorties en parallèle. Ainsi, supposons que tabFd[] est un tableau de descripteurs (associés à des fichiers)
ouverts en lecture. On pourrait avoir la boucle :

while (1) {
rc = read( tabFd[i], buf, sizeof(buf));
if (rc >= 0) {
// Faire le traitement associé a la lecture correcte
} else if (errno != EAGAIN) {
// L’erreur ne vient pas du fait que le descripteur est un fichier
// ouvert en non-bloquant et que le read se bloquerait.
// Il faut donc s’arreter.
error_at_line(EXIT_FAILURE, errno, __FILE__, __LINE__, "Pb descripteur");
}
i = (i+1) % NB_ELEMENT_DANS_TABFD;
}

Dans la pratique, cette manière de procéder est peu performante. En effet, on doit systématiquement
appeler read sur tous les descripteurs de fichier, alors que peut-être le read ne donnera un résultat
probant que sur l’un des descripteurs.

C’est pourquoi les différents systèmes d’exploitation proposent une méthode pour déterminer en un seul
appel système le (ou les) descripteur(s) sur lequel(/lesquels) il y a eu un événement. Ainsi, sous Linux,
on utilise en général la primitive select. Lors du cours « Éléments d’architecture client-serveur », nous
utilisons la librairie libevent (http://monkey.org/∼provos/libevent/) qui permet de garantir la
portabilité de la gestion des événements.

• Les entrées-sorties asynchrones (aio_read, aio_write. . .) ne sont pas implémentées dans le noyau
Linux. Toutefois, elles sont émulées dans la librairie gcc : cette dernière se charge de démarrer un
thread chargé de faire la lecture ou l’écriture de manière asynchrone. Cette manière de procéder est
très peu performante. L’utilisation des entrées-sorties asynchrones est donc déconseillée sous Linux.

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 66


Les fichiers (et les entrées-sorties) 2 Bibliothèque C d’entrées-sorties
' $
2 Bibliothèque C d’entrées-sorties

2.2 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
2.2 Ouverture/fermeture de fichier . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
# 15 2.3 Lecture de fichier. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .19
2.4 Écriture de fichier . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
2.6 Contrôle du tampon . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
2.6 Divers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23

& %

' $
2.1 Introduction

 L’ensemble des fonctions vues jusqu’à présent sont des appels système spécifiques
Unix : leur utilisation risque de compromettre la portabilité d’une application
# 16  Le langage C définit des mécanismes standard d’entrées-sorties. Leur utilisation
garantit donc la portabilité d’une application
 Néanmoins l’implémentation de ces mécanismes standards est une surcouche au
dessus des primitives systèmes : ces mécanismes standard ont une pénalité en terme
de performances

& %

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 67


2 Bibliothèque C d’entrées-sorties 2.3 Lecture de fichier
' $
2.2 Ouverture/fermeture de fichier

 Ouverture :
 FILE *fopen (const char *path, const char *mode)
provoque l’ouverture d’un flux vers le fichier path
 FILE *fdopen (int fd, const char *mode)
permet d’associer un flux au descripteur de fichier fd obtenu, par exemple, à
# 17 l’aide de open
 FILE *freopen (const char *path, const char *mode, FILE *stream)
Fonctionnalité similaire à dup2
 FILE *tmpfile (void)
crée un fichier temporaire
 Fermeture :
 int fclose (FILE *stream)

& %

Un flux est une structure de données qui contient notamment un descripteur de fichier, mais aussi des
pointeurs sur des tampons utilisés en lecture et en écriture.

fopen crée des fichiers avec des droits combinés avec le umask. Pour d’autres droits, faire un open du
fichier avec les droits qui vous intéressent, puis utiliser fdopen pour obtenir un flux.

' $
2.3 Lecture de fichier
 int fgetc (FILE *stream)

lit un caractère

 int getc (FILE *stream)

équivalent de fgetc, mais implémenté sous forme de macro (→ a priori, plus efficace)

 int getchar (void)

# 18 lit un caractère sur stdin

 char *fgets (char *s, int size, FILE *stream)

lit au plus size − 1 caractères jusqu’à retour-chariot ou EOF

 char *gets (char *s)

lit une chaîne de caractères sur stdin

 size_t fread (void *ptr, size_t size, size_t nmemb, FILE *stream) : retour =
nombre d’éléments lus (et non le nombre d’octets lus)

lit un tableau d’objets


& %

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 68


2 Bibliothèque C d’entrées-sorties 2.4 Écriture de fichier
' $
Lecture de fichier (suite)
 int fscanf (FILE *stream, const char *format, ...)
fait une lecture formatée
 int scanf (const char *format, ...)
fait une lecture formatée sur stdin
 Pour toutes les opérations de lecture, la lecture en fin de fichier déclenche le
# 19 positionnement d’un indicateur testable par feof()

& %

Toujours tester la valeur renvoyée par fscanf et consœurs. Cela évitera des surprises.

La fonction sscanf permet de faire une lecture formatée d’une zone mémoire.

' $
2.4 Écriture de fichier
 int fputc (int c, FILE *stream)
écrit un caractère
 int putc (int c, FILE *stream)
équivalent de fputc, mais implémenté sous forme de macro (→ a priori, plus efficace)
 int putchar (int c)

# 20 écrit un caractère sur stdout


 int fputs (const char *s, FILE *stream)
écrit une chaîne de caractères
 int puts (const char *s)
écrit une chaîne de caractères sur stdout
 size_t fwrite (const void *ptr, size_t size, size_t nmemb, FILE *stream) :
retour = nombre d’éléments écrits (et non le nombre d’octets écrits)
écrit un tableau d’objets
& %

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 69


2 Bibliothèque C d’entrées-sorties 2.4 Écriture de fichier
' $
Écriture de fichier (suite)
 int fprintf (FILE *stream, const char *format, ...)
fait une écriture formatée
 int printf (const char *format, ...)
fait une écriture formatée sur stdout

# 21

& %
Comme fscanf, fprintf et consœurs renvoient un nombre. Toutefois, au contraire de fscanf, cette
valeur de retour n’est en général pas utile (car le compilateur fait des vérifications permettant de contrôler
que tous les paramètres ont bien été utilisés). Noter que splint recommande de caster le retour de fprintf
et consœurs en (void).
La fonction sprintf permet de faire une écriture formatée dans une zone mémoire.
Le fichier copier2.c de la page suivante illustre les primitives de la bibliothèque C d’entrées-sorties.
/*************/
/* copier2.c */
/*************/
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>

FILE *source, *dest;


char buf[1024];
int nbElemLu, nbElemEcrit;

int main(int argc, char *argv[]) {


if (argc != 3) {
fputs("USAGE = copier2 nomSource nomDest\n", stderr);
return EXIT_FAILURE;
}
source = fopen(argv[1], "r");
if (source == NULL) {
fprintf(stderr,
"Erreur ’%s’ lors ouverture fichier %s\n",
strerror(errno),
argv[1]);
return EXIT_FAILURE;
}
dest = fopen(argv[2], "w");
if (dest == NULL) {
fprintf(stderr,
"Erreur ’%s’ lors ouverture fichier %s\n",
strerror(errno),
argv[2]);
return EXIT_FAILURE;
}
while ((nbElemLu = fread((void*)buf, sizeof(buf[0]), sizeof(buf), source)) > 0) {
nbElemEcrit = fwrite((void*)buf, sizeof(buf[0]), nbElemLu, dest);
if (nbElemEcrit < nbElemLu) {
fputs("Erreur d’ecriture\n", stderr);
return EXIT_FAILURE;
}
}
if (ferror(source) != 0) {
fputs("Erreur fread", stderr);

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 70


Les fichiers (et les entrées-sorties) 2 Bibliothèque C d’entrées-sorties

return EXIT_FAILURE;
}
if (fclose(source) < 0) {
perror(argv[1]);
return EXIT_FAILURE;
}
if (fclose(dest) < 0) {
perror(argv[2]);
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}

' $
2.5 Contrôle du tampon

 int setvbuf (FILE *stream, char *buf, int buf, size_t size)
contrôle le tampon utilisé par la librairie standard pour cacher certaines opérations
vis-à-vis du système de fichier
# 22
 int fflush (FILE *flux)
vide le tampon d’un flux
NB : ceci ne force pas l’écriture physique des données qui n’est assurée que par
O_SYNC ou fsync

& %

L’instruction setvbuf(stream, NULL, _IONBF, 0) (ou bien setbuf(file,NULL)) permet de garantir


qu’aucun buffer ne sera utilisé par la librairie standard (aucune opération d’écriture ne sera donc bufferisée).
' $
2.6 Divers

 int fseek (FILE *stream, long offset, int whence)


contrôle l’offset
 int fileno (FILE *stream)
renvoie le descripteur de fichier associé au flux
# 23  void clearerr (FILE *stream)
efface indicateur d’erreur et de fin de fichier
 int feof (FILE *stream)
teste l’indicateur de fin de fichier (effaçable uniquement par clearerr)
 int ferror (FILE *stream)
teste l’indicateur d’erreur (effaçable uniquement par clearerr)

& %

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 71


Les fichiers (et les entrées-sorties)

Il existe aussi des fonctions de « bases de données », plus précisément des accès aux fichiers plus évolués
que l’accès séquentiel ou direct : fichiers séquentiels indexés (avec des clés d’accès), arbres binaires. . .

• Package Unix NDBM : fonction dbm_open. . . (utilisé pour services réseau NIS, par exemple),

• Package GDBM : extensions GNU (fonction gdbm_open. . . ),

• Package Oracle Berkeley DB : fonction dbopen. . . (fichiers accessible à partir du langage C, Java,
Perl. . .).

Voir man ou bien [Blaess, 2002] pour plus d’informations.


' $
3 Projection des fichiers en mémoire

 void * mmap(void *start, size_t length, int prot, int flags, int
fd, off_t offset)
permet d’établir une projection en mémoire d’un fichier (ou d’un périphérique) de
# 24 manière à accéder au contenu du fichier avec des instructions de manipulation
mémoire
 int munmap(void *start, size_t length)
supprime une projection
 ATTENTION : mmap ne permet pas d’ajouter des octets à un fichier

& %

Le fichier affic8.c de la page suivante illustre mmap.


/************/
/* affic8.c */
/************/

/* Programme affichant 8 caracteres (a partir du i-ieme) du fichier dont */


/* le nom est fourni en parametre */

#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/mman.h>

int main(int argc, char *argv[]) {


int fd;
int i,j;
char *tab;
int rc;

if (argc < 3){


fputs("USAGE = permut nomFic valeurDeI\n", stderr);
return EXIT_FAILURE;
}

fd = open(argv[1], O_RDONLY);
if (fd < 0) {
perror("open fic");
return EXIT_FAILURE;
}
i = atoi(argv[2]);

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 72


Les fichiers (et les entrées-sorties)

/* mmap */
tab = (char *)mmap(0,
i+7,
PROT_READ,
MAP_PRIVATE,
fd,
0);
if (tab == MAP_FAILED){
perror("mmap");
return EXIT_FAILURE;
}

/* Affichage */
for (j=0 ; j<8 ; j++){
putc(tab[i+j], stdout);
}
puts("");

/* On ferme tout */
rc = munmap(tab,i+7);
if (rc < 0){
perror("munmap");
return EXIT_FAILURE;
}
if (close(fd) < 0){
perror("close fic");
return EXIT_FAILURE;
}

return EXIT_SUCCESS;
}

Vu que mmap ne permet pas d’ajouter des octets à un fichier, il faut veiller à ce que le fichier ait la taille
maximum souhaitée (par exemple, avec posix_fallocate) avant d’utiliser mmap sur ce fichier.
' $
4 Manipulation des i-noeuds du système de fichiers Unix
 Le système de gestion de fichiers Unix est basé sur la notion d’i-nœuds

 int stat(const char *file_name, struct stat *buf)

fstat(int fd, struct stat *buf)

renvoient des informations relatives à un fichier donné. Structure stat :


Type nom nature
dev_t st_dev Numéro du SGF

# 25 ino_t st_io Numéro i-nœud


mode_t st_mode Protection
nlink_t st_nlink Nombre de liens matériels
uid_t st_uid Numéro du propriétaire
gid_t st_gid Numéro du groupe
dev_t st_rdev Type périphérique
off_t st_size Taille en octets
time_t st_atime Date dernier accès
time_t st_mtime Date dernière modification
... ... ...
& %

Le fichier tailleFic.c de la page suivante illustre une utilisation de stat.


/***************/
/* tailleFic.c */
/***************/

/* Programme affichant la taille en octet du fichier dont le nom est */


/* fourni en parametre */

#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 73


Les fichiers (et les entrées-sorties)

#include <stdio.h>

int main(int argc, char *argv[]) {


int fd;
struct stat sts;

if (argc < 2){


fputs("USAGE = tailleFic nomFic\n", stderr);
return EXIT_FAILURE;
}

fd = open(argv[1], O_RDONLY);
if (fd < 0) {
perror("open fic");
return EXIT_FAILURE;
}
if (fstat(fd,&sts) < 0) {
perror("fstat");
return EXIT_FAILURE;
}
printf("Fichier %s a %ld octets\n", argv[1], sts.st_size);
if (close(fd) < 0){
perror("close fic");
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}

' $
Manipulation des i-noeuds du système de fichiers Unix (suite)
 int access(const char *pathname, int mode)
teste les droits d’accès d’un processus vis-à-vis du fichier pathname
 int unlink(const char *pathname)
permet de détruire un nom dans le système de fichiers
 int rename(const char *OLD, const char *NEW)
# 26
permet de renommer un fichier

& %

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 74


Les fichiers (et les entrées-sorties)
' $
5 Entrées-sorties sur répertoires

 Consultation :
 DIR *opendir (const char *path) : retour = pointeur sur flux répertoire
ouvre le répertoire path en « lecture »
 struct dirent *readdir (DIR *dir) : retour = une entrée de répertoire ou NULL
lit la première entrée (ou la suivante si un appel à readdir a déjà été fait) du répertoire
dir
# 27
 void rewinddir (DIR *dir)
 int closedir (DIR *dir)

 Mise-à-jour :
 int mkdir(const char *path, mode_t mode)
 int rmdir(const char *path)
 Rappel : int unlink(const char *path)
permet de supprimer l’entrée path (et éventuellement de détruire l’i-nœud associé dans
le système de fichier, si cette entrée était la seule pointant sur l’i-œud)
& %
Il existe aussi des fonctions permettant la manipulation de liens symboliques. Elles ne sont pas décrites
ici, car pour l’instant la norme POSIX n’a pas repris ces mécanismes. A titre indicatif, voici les fonctions
disponible dans Linux :
• int symlink(const char *oldpath, const char *newpath)
crée un lien symbolique
• int lstat(const char *path, struct stat *buf)
fournit des informations sur un lien symbolique
• int readlink(const char *path, char *buf, size_t bufsiz)
lit le contenu d’un lien symbolique
' $
6 Limitations de NFS
 NFS (Network File System) est un protocole permettant un accès transparent pour
les utilisateurs à des fichiers présents sur des machines distantes
 Ses limitations sont les suivantes :
 Le serveur NFS est sans état et les verrous ne sont pas supportés pour les
fichiers distants
 Problème de cohérence : NFS gère un ensemble de caches pour les fichiers et
# 28 leurs attributs. Ces caches ont une durée de validité de 3 secondes pour les
fichiers et 30 secondes pour les répertoires et il existe des phases d’incohérence
entre les différents clients
 Problème de sécurité : l’accès aux fichiers distants est contrôlé au travers de
l’identification (numérique) de l’utilisateur demandeur. Le bon fonctionnement
d’ensemble du mécanisme suppose donc qu’une identification numérique donnée
corresponde à la même personne physique sur le système de serveur de fichiers et
sur les systèmes clients qui peuvent le solliciter via NFS. Solution classique :
utiliser un NIS (Network Information Service)
& %

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 75


Les fichiers (et les entrées-sorties)

L’exercice 6 illustre certaines de ces limitations de NFS.


Bibliographie du chapitre
[Blaess, 2002] Blaess, C. (2002). Programmation système en C sous Linux : signaux, processus, threads, IPC
et sockets. Eyrolles, Paris, Paris, France.

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 76


Communications inter-processus

Denis Conan, Michel Simatic et François Trahay

module CSC4508/M2

Avril 2016

77
Communications inter-processus 1 Gestion des processus
' $
Plan du document

1 Gestion des processus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4


#2 2 Communications à l’aide de tubes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
3 Communication à l’aide des IPC POSIX . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
4 Comparaison des mécanismes de synchronisation. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .26

& %

' $
1 Gestion des processus

1.1 Environnement des processus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4


#3 1.2 Informations concernant un processus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.3 Création de processus. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .6
1.4 Terminaison de processus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
1.5 Exécution d’un nouveau programme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8

& %

TELECOM SudParis — Denis Conan, Michel Simatic et François Trahay — Avril 2016 — module CSC4508/M2 78
Communications inter-processus 1 Gestion des processus
' $
1.1 Environnement des processus

 Environnement = liste de chaînes de caractères (couples <nom>=<valeur>)


 Variables exportées du shell
 Accessible à partir de envp ou de la variable externe environ
#4 int main(int argc, char **argv, char **envp)
I Cette forme du main est aujourd’hui dépréciée
 envp et environ possèdent la même valeur
 getenv récupère le contenu d’une variable d’environnement
 char *getenv(char *name)
 Valeur de retour = pointeur sur la chaîne <valeur> ou NULL

& %

Voici un exemple de recherche de variables d’environnement (fichier exemple_getenv.c) :

/********************/
/* exemple_getenv.c */
/********************/

#include <stdio.h>
#include <stdlib.h>
extern char **environ;
int main(int argc, char **argv, char **envp) {
char *p_shell;
char **ancien_environ = environ;
/* parcours de l’environnement */
printf("---> envp :\n");
while(*envp != NULL) {
printf("%s\n", *envp);
envp++;
}
printf("---> environ :\n");
while(*environ != NULL) {
printf("%s\n", *environ);
environ++;
}
/* affichage de la variable SHELL si elle existe */
printf("---> recherche de la variable SHELL :\n");
if ((p_shell = getenv("SHELL")) != NULL) {
printf("SHELL : %s\n", p_shell);
} else {
printf("variable SHELL non définie\n");
}
environ = ancien_environ;
printf("---> recherche de la variable SHELL (environ réinitialisée):\n");
if ((p_shell = getenv("SHELL")) != NULL) {
printf("SHELL : %s\n", p_shell);
} else {
printf("variable SHELL non définie\n");
}
return EXIT_SUCCESS;
}

Veuillez regarder aussi les fonctions putenv(3), setenv(3), unsetenv(3).

TELECOM SudParis — Denis Conan, Michel Simatic et François Trahay — Avril 2016 — module CSC4508/M2 79
Communications inter-processus 1 Gestion des processus
' $
1.2 Informations concernant un processus
 Types d’informations :
 pid_t PID : numéro du processus, unique et non modifiable
 pid_t PPID : numéro du processus parent, unique et non modifiable
 gid_t PGID : numéro du processus chef d’un groupea , unique et modifiable
 uid_t UID : numéro de l’utilisateur réel, unique et non modifiable
 gid_t GID : numéro du groupe de l’utilisateur réel, unique et non modifiable
#5  gid_t EUID : numéro de l’utilisateur effectif, unique et modifiable
 gid_t EGID : numéro du groupe de l’utilisateur effectif, unique et modifiable
 Primitives correspondantes :
 pid_t getpid(), pid_t getppid(), int getpgid(), int setpgid(pid_t
pid, pid_t pgid)
 uid_t getuid(), uid_t geteuid(), gid_t getgid(), gid_t getegid(),
int setuid(uid_t id),int setgid(gid_t id)
a. Attention ! un groupe de processus repère un ensemble de processus dans une même session,
ce n’est pas lié au groupe d’utilisateur.
& %
Dans un système de type UNIX, il n’est pas possible de construire un processus ; en revanche, les processus
peuvent être dupliqués à l’identique (à charge ensuite au programmeur de changer l’image du processus s’il
veut exécuter un programme différent). Ainsi, tout processus (à l’exception du processus initial — noté 0)
possède un parent. Ils sont donc logiquement organisés sous la forme d’une arborescence. Depuis la version
2.4 du noyau GNU/Linux, le nombre de processus n’est plus limité à la valeur NT_TASKS (fixée à 512) mais
dépend de la taille mémoire de la machine. Ainsi, pour une machine à 8 Go, le nombre permis est 62993 :
lisible en regardant dans /proc/sys/kernel/threads-max.
La sémantique associée à chacune de ces primitives système est la suivante : getpid et getppid re-
tournent respectivement le numéro du processus courant et le numéro du parent du processus courant ;
getuid et getgid retournent respectivement l’UID réel et le GID réel associés au processus courant. Les
valeurs du propriétaire et du groupe effectifs déterminent les droits du processus lors des accès aux objets du
système (fichiers et processus). Ces valeurs sont modifiées lorsqu’un processus correspond à l’exécution d’un
programme binaire pour lequel le bit setuid (lettre s en lieu et place du droit x) est positionné. Le proprié-
taire réel reste l’identifiant de l’utilisateur ayant lancé la commande et le propriétaire effectif devient alors
l’identifiant du propriétaire du fichier exécuté. C’est ainsi qu’il est possible d’exécuter des commandes comme
passwd demandant les privilèges du super-utilisateur. Seul les processus de propriétaire le super-utilisateur
(0) peuvent changer d’identifiant avec les primitives setuid et setgid.
Le rôle des groupes de processus permet de gérer ensemble, par exemple pour l’envoi de signaux, les
processus créés par une même application importante. Par exemple, pour supprimer tous les processus de
cette application, il suffira d’envoyer un signal au groupe. Prenons le cas de la commande cat | lp -P hp2
qui connecte l’entrée standard vers l’imprimante hp2. Pour sortir de l’entrée clavier, il suffit de taper un
CTL-D qui envoie un signal d’arrêt aux deux processus mais pas au shell qui a lancé ces deux processus.
Le signal est donc envoyé au groupe de processus qui englobe ces deux processus. Voici un exemple de
manipulation des informations relatives à un processus (fichier exemple_getpid.c) :
/********************/
/* exemple_getpid.c */
/********************/

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>

int main() {
printf("pid = %d\n", getpid());
printf("ppid = %d\n", getppid());
printf("pgrp = %d\n", getpgrp());
printf("création d’un groupe = %d\n", setpgrp());
printf("pid = %d\n", getpid());
printf("ppid = %d\n", getppid());
printf("pgrp = %d\n\n", getpgrp());

TELECOM SudParis — Denis Conan, Michel Simatic et François Trahay — Avril 2016 — module CSC4508/M2 80
Communications inter-processus 1 Gestion des processus

printf("uid = %d\n", getuid());


printf("euid = %d\n", geteuid());
printf("gid = %d\n", getgid());
printf("egid = %d\n", getegid());
printf("changement d’identité effective = %d, %d\n", setuid(0), setgid(0));
printf("uid = %d\n", getuid());
printf("euid = %d\n", geteuid());
printf("gid = %d\n", getgid());
printf("egid = %d\n", getegid());

return EXIT_SUCCESS;
}

' $
1.3 Création de processus

 Héritage de presque tous les attributs du parent :


 Segment text : copié ou partagé
 Segment data : copié
 UID / GID réels et effectifs : conservés

#6  PID / PPID : modifiés


 PGRP : conservé
 Signaux interceptés : conservés
 Descripteurs de fichiers / tubes : copiés
 Temps U.C. : remis à 0
 Primitives correspondantes :
pid_t fork(), pid_t vfork()

& %
La primitive fork permet de dupliquer un processus à l’identique. Aussi, afin de pouvoir discriminer
entre le parent (le processus initial) et l’enfant (la copie), la valeur de retour de cette primitive est différente
selon que l’on est dans l’un ou l’autre processus : dans le parent, la valeur retournée est le numéro du PID
de l’enfant ; dans l’enfant, la valeur retournée est 0 ; en cas d’erreur, comme pour la plupart des primitives
systèmes, la valeur retournée par l’appel à fork dans le parent est -1. vfork est une version allégée de fork
ne devant être utilisée que si elle est suivie d’un exec immédiatement après. Voici un exemple de duplication
de processus (fichier exemple_fork.c) :
/******************/
/* exemple_fork.c */
/******************/

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>

int main() {
pid_t pid;
printf("démonstration de fork\n");
pid = fork();
switch (pid) {
case -1 :
fprintf(stderr, "echec de fork\n");
return EXIT_FAILURE;
case 0 : /* enfant */
sleep(2);
printf("je suis l’enfant\n");
printf("pid = %d\n", getpid());
printf("ppid = %d\n", getppid());
printf("pgrp = %d\n", getpgrp());
printf("uid = %d\n", getuid());
printf("euid = %d\n", geteuid());
printf("gid = %d\n", getgid());
break;

TELECOM SudParis — Denis Conan, Michel Simatic et François Trahay — Avril 2016 — module CSC4508/M2 81
Communications inter-processus 1 Gestion des processus

default : /* parent */
printf("je suis le parent\n");
printf("pid = %d\n", getpid());
printf("ppid = %d\n", getppid());
printf("pgrp = %d\n", getpgrp());
printf("uid = %d\n", getuid());
printf("euid = %d\n", geteuid());
printf("gid = %d\n", getgid());
printf("pid de l’enfant= %d\n", pid);
break;
}
return EXIT_SUCCESS;
}

' $
1.4 Terminaison de processus
 Terminaison :
 Code retour du processus
 Libération des segments de mémoire
 Entrée dans la table des processus (cf. /proc) :
I Gardée : stockage code retour, temps CPU. . .
I Libérée par le processus parent (dans le wait)
 Attente de la terminaison de l’enfant :
#7
 Informations obtenues lors de la fin du processus enfant
I Mode trace → octet poids fort = signal reçu, octet poids faible = 0xb1
I exit → octet poids fort = code retour enfant, octet poids faible = 0x00
I Signal → octet poids fort = 0xb1, octet poids faible = signal reçu
 On utilise plutôt les macros WIFEXITED, WEXITSTATUS, WIFSIGNALED et WTERMSIG
 Primitives correspondantes :
void exit(int code_retour), int atexit(void(*fonction_a_executer)())
pid_t wait(int *code_retour), pid_t waitpid(pid_t pid_enfant, int
*code_retour, int options)
& %

La primitive exit permet de quitter le processus courant en retournant un code de retour ; atexit permet
d’empiler les fonctions devant être exécutées à la terminaison du processus, le paramètre consistant en un
pointeur sur la fonction devant être exécutée. Le système GNU/Linux possède une primitive (on_exit). La
primitive atexit est préférable à on_exit, car elle est standard.
Les primitives wait et waitpid permettent d’attendre la fin d’un processus enfant. Dans le premier cas,
le premier enfant se terminant est pris en compte et la valeur pointée par le paramètre contient le code de
retour de l’enfant ; dans le second cas, le premier paramètre spécifie l’enfant devant être attendu : <-1 pour
n’importe quel enfant appartenant à un groupe de processus d’identité pid_enfant ; -1 pour n’importe quel
enfant ; 0 pour n’importe quel enfant appartenant au même groupe que le parent ; >0 pour l’enfant ayant
le PID indiqué. Le dernier paramètre permet de spécifier le comportement de la primitive ; en particulier,
WNOHANG permet de retourner même si aucun enfant ne s’est terminé. Voici un exemple de terminaison de
processus (fichier exemple_wait.c) :
/******************/
/* exemple_wait.c */
/******************/

#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>

void f(void) {
printf("On va bientot sortir\n");
}

void g(void) {
printf("On sort\n");
}

int main() {
int pid, code_retour;

TELECOM SudParis — Denis Conan, Michel Simatic et François Trahay — Avril 2016 — module CSC4508/M2 82
Communications inter-processus 1 Gestion des processus

pid = fork();
if (pid) { /* parent */
wait(&code_retour);
code_retour = WEXITSTATUS(code_retour);
printf("Parent : code de retour obtenu : %d\n", code_retour);
return(code_retour);
} else { /* enfant */
atexit(g);
atexit(f);
printf("enfant : terminaison avec code de retour %d\n", 5);
return 5;
}
return EXIT_SUCCESS;
}

' $
1.5 Exécution d’un nouveau programme

 Modification de l’image (le segment de code) du processus courant


 Il n’est pas possible de revenir d’un exec
 Les instructions qui suivent un exec ne sont exécutées que si exec échoue
 Primitives correspondantes :
#8
exec{l|v}[e|p] → man 3 exec
I l : liste d’arguments : fournir la liste (char *arg0, char *arg1,..., 0)
I v : tableau d’arguments : fournir un pointeur sur le tableau (char **argv
avec dernier élément = ’0’)
I e : passer un pointeur d’environnement (char **envp)
I p : la variable d’environnement PATH est utilisée pour localiser le fichier (char
*nom_relatif), sinon chemin absolu (char *nom_absolu)

& %

La primitive exec, quelle qu’en soit sa forme, permet de modifier l’image (le segment de code) du processus
courant. Il est important de noter qu’il n’est pas possible de revenir d’un exec et que les instructions qui
suivent son appel ne sont accessibles que s’il s’est produit une erreur.
La liste des primitives est la suivante :
• int execl(char *nom_absolu, char *arg0, char *arg1,..., 0)
• int execv(char *nom_absolu, char *argv[])
• int execle(char *nom_absolu, char *arg0, char *arg1,..., 0, char **envp)
• int execve(char *nom_absolu, char **argv, char **envp)
• int execlp(char *nom_relatif, char *arg0, char *arg1,..., 0)
• int execvp(char *nom_relatif, char **argv)
Les primitives execlp et execvp agiront comme le shell dans la recherche du fichier exécutable si le nom
fourni ne contient pas de « slash » (/). Le chemin de recherche est spécifié dans la variable d’environnement
PATH. Si cette variable n’est pas définie, le chemin par défaut sera “/bin:/usr/bin:”. Voici un exemple de
recouvrement de code (fichier exemple_exec.c) :
/******************/
/* exemple_exec.c */
/******************/

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>

TELECOM SudParis — Denis Conan, Michel Simatic et François Trahay — Avril 2016 — module CSC4508/M2 83
Communications inter-processus 2 Communications à l’aide de tubes

#include <unistd.h>

int main() {
int code_retour;
system("ps");
printf("pid = %d\n", getpid());
printf("ppid = %d\n", getppid());
printf("pgrp = %d\n", getpgrp());
printf("uid = %d\n", getuid());
printf("euid = %d\n", geteuid());
printf("gid = %d\n", getgid());
code_retour = execlp("ps", "ps", (char *)NULL);
printf("code_retour vaut %d\n",code_retour);
perror("pb sur exec");
printf("fin du programme appelant\n");
return EXIT_FAILURE;
}

' $
2 Communications à l’aide de tubes

#9 2.1 Principe des tubes. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .10


2.2 Tubes ordinaires (ou locaux) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
2.3 Tubes nommés . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12

& %

TELECOM SudParis — Denis Conan, Michel Simatic et François Trahay — Avril 2016 — module CSC4508/M2 84
Communications inter-processus 2 Communications à l’aide de tubes
' $
2.1 Principe des tubes

 Premier moyen d’échange de données entre processus sous UNIX


 Fichiers spéciaux gérés selon une méthodologie FIFO
 Taille limitée, généralement 10 blocs

# 10  Deux types de tubes :


 Tubes ordinaires, non visibles dans l’arborescence, entre processus d’une même
filiation
 Tubes nommés, visibles dans l’arborescence, entre processus non forcément liés
par filiation
 Création ou ouverture de tube retournent des descripteurs de fichiers ouverts gérés
comme les autres fichiers

& %

' $
2.2 Tubes ordinaires (ou locaux)
 Accès par création ou héritage
 Perte par un processus = perte d’accès définitive par ce processus
 Position courante entièrement déterminée par les lectures/écritures
 Primitive lseek interdite
 Taille maximum déterminée par PIPE_BUF définie dans <limits.h>
 Opération de lecture par défaut bloquante
# 11
 Bloquant jusqu’à lecture de la taille demandée ou disparition de tous les écrivains
 Pour non bloquante : utiliser fcntl avec le drapeau 0_NONBLOCK
 Opération d’écriture atomique (tout est écrit) si opération bloquante
 Signal SIGPIPE si aucun lecteur
 Primitive correspondante :
int pipe(int descfich[2])
I 0 pour la lecture et 1 pour l’écriture
→ Se mettre d’accord pour l’utilisation (celui qui écrit, ceux qui lisent)
& %

• Le tube est un moyen de communication inter-processus de type FIFO. Le tableau de deux entiers
passé en paramètre permet au retour de l’appel de connaître les deux flux associés au tube. Lors
de l’appel de la primitive pipe, le tube est créé au sein du processus réalisant l’appel. Pour réaliser
une communication inter-processus, il est nécessaire — suite à la création du tube — de dupliquer
le processus. L’écriture et la lecture des informations dans le tube s’effectuent à l’aide des primitives
write et read comme avec les fichiers classiques ; il n’est pas possible de se déplacer dans le tube (avec
la primitive lseek par exemple).
• Pour éviter les problèmes d’interblocage dûs à l’attente de données qui n’arriveront pas sur des des-
cripteurs que les écrivains ne ferment pas, il est fortement conseillé de ne conserver que les descripteurs
utiles et de fermer systématiquement tous les autres.

TELECOM SudParis — Denis Conan, Michel Simatic et François Trahay — Avril 2016 — module CSC4508/M2 85
Communications inter-processus 2 Communications à l’aide de tubes

• La taille maximum déterminée par PIPE_BUF détermine la taille des atomic write, i.e. la taille maximale
au delà de laquelle l’écriture risque de ne pas être atomique. En effet, si 2 processus écrivent sur le
même tube et que la taille des données écrites est proche de PIPE_BUF, il y a un fort risque que le
contenu de ces écritures soit mélangées dans le tube (un morceau de l’écriture du processus 1, puis un
morceau de l’écriture du processus 2, puis un morceau de la suite de l’écriture du processus1. . .).

• La fonction popen engendre un processus en créant un tube, exécutant un fork et en invoquant le


shell. Le tube permet, selon le paramètre fourni à popen d’être en relation avec l’entrée ou la sortie du
processus engendré.
• Voici un exemple de communication de données avec un tube ordinaire (fichier exemple_pipe.c) :

/******************/
/* exemple_pipe.c */
/******************/

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>

void parent(int tube, char *message) {


sprintf(message, "Message du parent (%d)\n", getpid());
write(tube, message, strlen(message) + 1);
close(tube);
}

void enfant(int tube) {


char boite;
printf("Message reçu par %d : \"", getpid());
while (read(tube, &boite, 1) == 1)
putchar(boite);
printf("\"\n\n");
}

int main() {
int pid, descfich[2], code_retour;
char tampon[80];
pipe(descfich);
pid = fork();
switch (pid) {
case -1 :
fprintf(stderr, "echec de fork\n");
exit(EXIT_FAILURE);
case 0 : /* enfant */
close(descfich[1]); /* ... n’ecrit pas dans le tube mais lit */
enfant(descfich[0]);
break;
default : /* parent */
close(descfich[0]); /* ... ne lit pas dans le tube mais ecrit */
parent(descfich[1], tampon);
wait(&code_retour);
break;
}
return EXIT_SUCCESS;
}

• Cet exemple permet à un parent d’envoyer des octets à un enfant. L’enfant sait que le parent a terminé
d’écrire ses données quand le parent ferme le tube. Dans la pratique, ce moyen de savoir que le parent
a terminé d’envoyer un bloc de données à l’enfant n’est pas pratique puisque le tube ne peut servir
qu’une seule fois ! Trois méthodes plus adaptées sont à disposition :

1. Le parent envoie à l’enfant des données qui ont une taille fixe (un entier, une structure de don-
nées. . .). Le parent écrit ses données et l’enfant fait un read de taille fixe octets. read renvoie
donc taille fixe (le parent a écrit une donnée) ou 0 (le parent a fermé le tube).
2. Le parent peut aussi envoyer des données d’une taille variable (par exemple, une chaîne de carac-
tères). Dans ce cas, le parent commence par envoyer un entier contenant la taille de la donnée qu’il
s’apprête à envoyer, puis il envoie cette donnée. L’enfant commence donc par faire un premier
read de sizeof(int) pour déterminer la taille du read qu’il doit faire juste après, puis il fait
le-dit read.

TELECOM SudParis — Denis Conan, Michel Simatic et François Trahay — Avril 2016 — module CSC4508/M2 86
Communications inter-processus 2 Communications à l’aide de tubes

NB : si les tailles sont faibles (quelques dizaines, voire centaines d’octets), il est plus perfor-
mant d’appliquer la méthode 1 en écrivant/lisant systématiquement le nombre maximum d’oc-
tets. Certes, on envoie des octets pour rien, mais on économise des 2 appels système par échange
d’information entre le parent et l’enfant : on est plus performant.
3. Le parent et l’enfant peuvent utiliser les fonctions d’écriture/lecture de la libC. Par exemple, le
parent fait des fputs pendant que l’enfant fait des fgets.
L’implémentation de ces 3 méthodes est laissée à titre d’exercice.

' $
2.3 Tubes nommés
 Création par la primitive mknod qui permet de créer les fichiers spéciaux ou par
mkfifo
 mknod était préalablement réservée au super-utilisateur
 Ouverture possible par les processus connaissant le nom du tube
 Par défaut, ouverture bloquante : lecteurs et écrivains s’attendent →
synchronisation
# 12
 Suppression du tube lorsque explicitement demandé et pas d’ouverture en cours
 Si suppression alors qu’il existe des lecteurs et écrivains, fonctionnement comme
un fichier ordinaire
 Primitives correspondantes :
int mknod(const char *nom_fich, mode_t mode, dev_t n_periph)
I Mode = S_IFIFO (ou 0010000) plus les droits sur les trois derniers octets
int mkfifo(const char *nom_fich, mode_t mode)
I Mode = droits
& %
Remarque : les tubes nommés ont longtemps été le moyen privilégié pour communiquer entre plusiseurs
processus. Cette méthode tend à disparaître au profit de l’utilisation de files de message. La présente Section
n’est maintenue que pour des raisons historiques.
Le mode indique les permissions d’accès. Ces permissions sont modifiées par la valeur d’umask du pro-
cessus : les permissions d’accès effectivement adoptées sont (mode & NOT(umask)).
L’open d’un tube nommé en lecture (respectivement écriture) est bloquant jusqu’à ce qu’un écrivain
(respectivement un lecteur) ouvre le tube en écriture (respectivement lecture) à l’autre extrémité.
Dans l’exemple suivant, le serveur (fichier exemple_mkfifo_serveur.c) attend des requêtes (de la part
de clients décrits ci-après) sur le tube nommé ./client-serveur.
Noter que exemple_mkfifo_serveur.c ouvre le le tube qu’il crée en lecture/écriture au lieu d’une lecture
simple de sorte que :
• Il ne se bloque pas au moment du open en attendant qu’un processus ouvre le tube en écriture
• Si nous ouvrons le tube en lecture seule, à chaque fois que le client qui s’est connecté se termine,
la lecture du tube nommé va échouer car le noyau détecte une fermeture de fichier. En demandant
l’ouverture d’un tube en lecture et en ecriture, nous évitons ce genre de situation, car il reste toujours
au moins un descripteur ouvert sur la sortie.
/****************************/
/* exemple_mkfifo_serveur.c */
/****************************/

#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>

TELECOM SudParis — Denis Conan, Michel Simatic et François Trahay — Avril 2016 — module CSC4508/M2 87
Communications inter-processus 2 Communications à l’aide de tubes

#include <stdlib.h>
#include <string.h>
#include <errno.h>

#define TAILLEMSG 128


#define NOMTUBESERVEUR "./client-serveur"

void traiterRequete(char *requete){


char reponse[TAILLEMSG];
char typRequete[128];
char nomTubeClient[128];
int fdW;
int nbWrite;

/* Analyse de la requete */
/* NB : sscanf presente l’inconvenient que si le client envoie */
/* une chaine de caractere contenant un espace (par exemple : */
/* "PING PING") la chaine lue s’arrete au premier espace ! */
/* fgets serait probablement meilleur. */
sscanf(requete,"%s\n%s", typRequete, nomTubeClient);
/* Preparation de la reponse */
sprintf(reponse, "%s", "PONG");
/* Affichage */
printf("Serveur a recu \"%s\" et repond \"%s\" sur le tube nomme \"%s\"\n",
typRequete,
reponse,
nomTubeClient );
/* Réponse sur le tube nommé du client */
fdW = open(nomTubeClient, O_WRONLY);
if (fdW == -1) {
perror("open(nomTubeClient)");
exit(EXIT_FAILURE);
}
nbWrite = write(fdW, reponse, sizeof(reponse));
if (nbWrite < sizeof(reponse)) {
perror("pb ecriture sur tube nomme");
}
/* Dans cette application, le client ne renvoie pas de requête ultérieure*/
/* nécessitant une réponse ==> On peut fermer ce tube */
close(fdW);
}

int main() {
char requete[TAILLEMSG];
int fdR;
int nbRead;

/* Création du tube nommé côté serveur (qui permettra les communications */


/* client-serveur) */
if (mkfifo(NOMTUBESERVEUR, S_IRUSR|S_IWUSR) < 0) {
if (errno != EEXIST) {
perror("mkfifo(tube nommé serveur");
exit(EXIT_FAILURE);
}
else {
printf("%s existe deja : on suppose que c’est un pipe nomme\n",
NOMTUBESERVEUR);
printf("et qu’on peut continuer le programme sans probleme\n");
puts("");
}
}
/* Ouverture de ce tube nommé */
fdR = open(NOMTUBESERVEUR, O_RDWR);
if (fdR == -1) {
perror("open(tube nommé)");
exit(EXIT_FAILURE);
}
/* Attente de requêtes */
do{
nbRead = read(fdR, requete, sizeof(requete));
if (nbRead != sizeof(requete)) {
perror("read sur tube nomme NOK");
return EXIT_FAILURE;
}
/* On traite la requête */
traiterRequete(requete);
}while(1);
}

Le client exemple_mkfifo_client.c envoie au serveur des requêtes contenant le message PING et le nom
du tube (./serveur-client.<numéroProcessusClient>) sur lequel le client attend la réponse PONG.
/***************************/
/* exemple_mkfifo_client.c */

TELECOM SudParis — Denis Conan, Michel Simatic et François Trahay — Avril 2016 — module CSC4508/M2 88
Communications inter-processus

/***************************/

#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

#define TAILLEMSG 128


#define NOMTUBESERVEUR "./client-serveur"

int main() {
char nomTubeClient[128];
char requete[TAILLEMSG];
char reponse[TAILLEMSG];
int fdW, fdR;
int nbRead, nbWrite;

/* Préparation de nomTubeClient (tube qui permettra les communications */


/* serveur-client) */
sprintf(nomTubeClient, "/tmp/serveur-client.%d", getpid());
/* Création du tube nommé qui permettra au serveur de repondre au client */
if (mkfifo(nomTubeClient, S_IRUSR|S_IWUSR) < 0) {
if (errno != EEXIST) {
perror("mkfifo(tube nommé client");
exit(EXIT_FAILURE);
}
else {
printf("%s existe deja : on suppose que c’est un pipe nomme\n",
nomTubeClient );
printf("et qu’on peut continuer le programme sans probleme\n");
puts("");
}
}
/* Ouverture de ce tube */
fdR = open(nomTubeClient, O_RDWR);
if (fdR == -1) {
perror("open(tube nommé)");
exit(EXIT_FAILURE);
}
/* Envoi de la requête vers le serveur (la requete contient le nom du */
/* tube sur lequel la reponse est attendue) */
sprintf(requete,"%s\n%s", "PING", nomTubeClient);
fdW = open(NOMTUBESERVEUR, O_WRONLY);
if (fdW == -1) {
perror("open(NOMTUBESERVEUR)");
exit(EXIT_FAILURE);
}
nbWrite = write(fdW, requete, sizeof(requete));
if (nbWrite < sizeof(requete)) {
perror("pb ecriture sur pipe nomme");
}
/* On n’aura pas d’autres messages à envoyer au serveur. On peut donc */
/* fermer le tube cote emission */
close(fdW);
/* On lit la réponse et on l’affiche */
nbRead = read(fdR, reponse, sizeof(reponse));
if (nbRead != sizeof(reponse)) {
printf("Communication avec le serveur probablement rompue\n");
exit(EXIT_FAILURE);
}
printf("Reponse du serveur = \"%s\"\n", reponse);
/* On ferme le tube côté réception */
close(fdR);
/* On détruit le tube nommé du client (puisque ce tube nommé ne servira */
/* plus jamais. */
if (unlink(nomTubeClient) < 0) {
perror("unlink");
exit(EXIT_FAILURE);
}
return EXIT_SUCCESS;
}

Dans cet exemple, on a donc un tube nommé pour les communications client-serveur et un tube nommé
(par client) pour les communications serveur-client.

TELECOM SudParis — Denis Conan, Michel Simatic et François Trahay — Avril 2016 — module CSC4508/M2 89
Communications inter-processus 3 Communication à l’aide des IPC POSIX
' $
3 Communication à l’aide des IPC POSIX

3.1 Inter Process Communication POSIX . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14


# 13 3.3 IPC System V versus POSIX IPC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
3.3 Files de messages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
3.4 Mémoire partagée . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
3.5 Sémaphores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20

& %

' $
3.1 Inter Process Communication POSIX

 Trois types d’objets système :


 Files de messages : échange de de flots de données formatées
# 14
 Mémoire partagée
 Sémaphores : outils de synchronisation
 Utilisation d’une clé pour identifier les objets système
 Chaîne de caractère de la forme /nom_de_la_cle

& %

Le terme « IPC » recouvre trois mécanismes de communication introduits à partir du System V : les files
de messages, la mémoire partagée et les sémaphores. Ces mécanismes utilisent un système de fichier virtuel
permettant la persistance. Chaque type d’IPC est donc accessible depuis le système de fichiers :

• Les objets de type mémoire partagée sont stockés dans /dev/shm/

• Les sémaphores nommés sont stockés dans /dev/shm/ sous la forme sem.X (où X est la clé du sémaphore)

• Les files de message sont accessibles via un montage dans le système de fichier (par exemple : mount
-t mqueue none /dev/mqueue)

TELECOM SudParis — Denis Conan, Michel Simatic et François Trahay — Avril 2016 — module CSC4508/M2 90
Communications inter-processus 3 Communication à l’aide des IPC POSIX

Les IPC nécessitent une clé permettant d’identifier l’objet IPC. Une même clé doit donc être fournie par
tous les processus désirant accéder à l’objet. Une clé est une chaîne de caractère commençant par “/” et ne
comportant pas le caractère “/” ailleurs.
Il existe des formes plus évoluées de communication entre les processus d’une même machine. Citons D-Bus
(http://dbus.freedesktop.org), ICE (http://www.zeroc.com), ZeroMQ (http://www.zeromq.org). . .Vu
les fonctionnalités offertes en général par ces librairies (gestion de cycle de vie, gestion de publication-
souscription. . .), ces librairies s’apparentent plus à des intergiciels (middleware) de communication qu’à des
mécanismes d’IPC. Ils seront donc plutôt étudiés en CSC5002 (Intergiciels pour applications réparties).
' $
3.2 IPC System V versus POSIX IPC

 System V = une des premières versions majeures d’Unix. A servi de base pour
l’élaboration du standard POSIX
 POSIX = Portable Operating System Interface (défini par IEEE)
# 15
 System V et POSIX définissent les IPC de manières différentes
 Pendant longtemps, implantation incomplète des IPC POSIX sur certains systèmes
 Les IPC System V ont perduré pendant longtemps
 Aujourd’hui IPC POSIX implantées correctement dans Linux et la plupart des Unix

& %
Les IPC POSIX sont implantées correctement dans la plupart des Unix actuels. Toutefois, Mac OS X
n’offre pas une implantation complète du standard.
' $
3.3 Files de messages

 Création / destruction :
mqd_t mq_open(const char *name, int oflag, mode_t mode, struct
mq_attr *attr)
int mq_unlink(mqd_t mq)
 Ouverture / fermeture d’une file déjà créée :
# 16
mqd_t mq_open(const char *name, int oflag)
int mq_close(mqd_t mq)
 Émission / réception (bloquante si file pleine et oflag ne contient pas O_NONBLOCK) :
int mq_send(mqd_t mqdes, const char *msg_ptr, size_t msg_len,
unsigned msg_prio)
ssize_t mq_receive(mqd_t mqdes, char *msg_ptr, size_t msg_len,
unsigned *msg_prio)

& %

TELECOM SudParis — Denis Conan, Michel Simatic et François Trahay — Avril 2016 — module CSC4508/M2 91
Communications inter-processus 3 Communication à l’aide des IPC POSIX

Lorsqu’un processus envoie un message dans une file, celui-ci est placé par ordre de priorité dans la file, les
messages de même priorité étant classés par ordre d’arrivée. Lorsqu’un processus désire recevoir un message,
il lit le premier message disponible (le plus prioritaire). Ceci se traduit par cinq opérations.
mq_open permet d’obtenir un identifiant de file de messages à partir d’une clé et des droits d’accès et les
conditions de création associées. Le quatrième paramètre de mq_open permet de spécifier les attributs (taille
maximale des messages, nombre maximal de messages dans la file, etc.) de la file de message.
mq_close permet de fermer une file de message. Chaque processus utilisant la file de message doit appeler
cette fonction.
mq_unlink permet de détruire définitivement une file de message.
mq_send permet d’envoyer un message dans la file identifiée par le premier paramètre et dont l’adresse
et la taille sont respectivement les deuxième et troisième paramètres. Le dernier paramètre défini la priorité
du message.
mq_receive demande la réception d’un message à partir de l’identifiant de file de messages. Ce message
sera placé à l’adresse indiquée par le deuxième paramètre et ne pas dépasser la taille indiquée par le troisième.
Si le quatrième paramètre n’est pas NULL, mq_receive y affecte la priorité associée au message lors de l’appel
à mq_send.
À moins que le drapeau O_NONBLOCK ne soit passé lors de l’appel à mq_open, la réception des messages est
toujours bloquante ; l’envoi n’est bloquant que quand la file de message est pleine. Pour des envois/réceptions
non bloquants, on peut utiliser les fonctions mq_timedsend et mq_timedreceive :
ssize_t mq_timedsend(mqd_t mqdes, const char *msg_ptr, size_t msg_len, unsigned
msg_prio, const struct timespec *abs_timeout)
ssize_t mq_timedreceive(mqd_t mqdes, char *msg_ptr, size_t msg_len, unsigned
*msg_prio, const struct timespec *abs_timeout);
Ces fonctions prennent un paramètre supplémentaire de type struct timespec correspondant à la date
jusqu’à laquelle la fonction doit se bloquer si aucun message n’est reçu (pour mq_timedreceive) ou si la file
est pleine (pour mq_timedsend).
Il est également possible de traiter les messages reçu depuis une file de message de manière événementielle
en utilisant :
int mq_notify(mqd_t mqdes, const struct sigevent *sevp)
Lorsqu’un message est reçu dans la file mqdes, la fonction sevp->sigev_notify_function est
appelée (si sevp->sigev_notify vaut SIGEV_THREAD) ou le signal sevp->sigev_signo est émis (si
sevp->sigev_notify vaut SIGEV_SIGNAL). Un exemple d’utilisation de mq_notify est disponible dans
msg_queue/example_msg_server_notify.c.
Voici un exemple de communication à l’aide de files de messages Posix (fichiers
msg_queue/example_msg_server.c et msg_queue/example_msg_client.c) :
/*************************/
/* example_msg_server.c */
/*************************/

/* Server process. Run it with


* $ example_msg_server /message_queue_name &
* Result : prints the clients’ requests
* Stops when a client sends a message starting with @
*/

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <mqueue.h>

#include "example_msg.h"

int main(int argc, char** argv) {


mqd_t requests, responses;
char key_req[80];
char key_rep[80];

struct msg req, rep;

if(argc != 2) {
fprintf(stderr, "Usage: %s /message_queue_name\n", argv[0]);
exit(EXIT_FAILURE);
}

sprintf(key_req, "%s_req", argv[1]);


sprintf(key_rep, "%s_rep", argv[1]);

TELECOM SudParis — Denis Conan, Michel Simatic et François Trahay — Avril 2016 — module CSC4508/M2 92
Communications inter-processus 3 Communication à l’aide des IPC POSIX

struct mq_attr attr;


attr.mq_flags = 0;
attr.mq_maxmsg = 10; /* size of the message queue */
attr.mq_curmsgs = 0;
attr.mq_msgsize = sizeof(struct msg); /* size of one message */

/* message queue for incoming requests */


requests = mq_open(key_req, O_CREAT|O_RDWR, S_IRUSR|S_IWUSR, &attr);
if (requests < 0) {
perror("Server: mq_open");
exit(EXIT_FAILURE);
}

/* message queue for outgoing responses */


responses = mq_open(key_rep, O_CREAT|O_RDWR, S_IRWXU, &attr);
if (responses < 0) {
perror("Server: mq_open");
exit(EXIT_FAILURE);
}

for (;;) {
/* wait for any requests */
int len = mq_receive(requests, (char*)&req, sizeof(req), NULL);
if(len < 0) {
perror("Server: mq_receive");
exit(EXIT_FAILURE);
}

printf("Server: received message: %s \n", req.text);

if (req.text[0] == ’@’) {
/* ask to terminate the server */
break;
}

/* process the request */


strcpy(rep.text, req.text);

/* send the response to the client */


len = mq_send(responses, (char*)&rep, GET_SIZE(&rep), 0);
if (len < 0) {
perror("Server: mq_send");
exit(EXIT_FAILURE);
}
printf("Server: message sent: %s \n", rep.text);
}

strcpy(rep.text, "Terminating the server");

if( mq_send(responses, (char*)&rep, GET_SIZE(&rep), 0) < 0) {


perror("Serveur : mq_send");
exit(EXIT_FAILURE);
}

/* wait to make sure that client received the server’s answer */


sleep(2);

/* close the message queues */


if( (mq_close(requests) < 0) || (mq_close(responses) < 0)) {
perror("Serveur : mq_close");
exit(EXIT_FAILURE);
}

/* delete the message queues */


if((mq_unlink(key_req) < 0) || (mq_unlink(key_rep) < 0)) {
perror("Serveur : mq_unlink");
exit(EXIT_FAILURE);
}

return EXIT_SUCCESS;
}

/************************/
/* example_msg_client.c */
/************************/

/* Client process. Run it with


* $ example_msg_client /message_queue_name text_to_send
* Result: print the message received from the server
*/

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

TELECOM SudParis — Denis Conan, Michel Simatic et François Trahay — Avril 2016 — module CSC4508/M2 93
Communications inter-processus 3 Communication à l’aide des IPC POSIX

#include <string.h>
#include <mqueue.h>
#include "example_msg.h"

int main(int argc, char **argv) {


mqd_t requests, responses;
char key_req[80];
char key_rep[80];

struct msg rep, req;


int len;

if (argc != 3) {
printf("Client: Usage : %s /message_queue_name text_to_send\n",argv[0]);
exit(EXIT_FAILURE);
}

sprintf(key_req, "%s_req", argv[1]);


sprintf(key_rep, "%s_rep", argv[1]);

/* message queue for sending requests */


requests = mq_open(key_req, O_WRONLY);
if (requests < 0) {
perror("Client: mq_open");
exit(EXIT_FAILURE);
}

/* message queue for receiving responses */


responses = mq_open(key_rep, O_RDONLY);
if (requests < 0) {
perror("Client: mq_open");
exit(EXIT_FAILURE);
}

/* prepare the request to send */


strcpy(req.text, argv[2]);
/* send the request */
len = mq_send(requests, (char*)&req, GET_SIZE(&req), 1);
if (len < 0) {
perror("Client: mq_send");
exit(EXIT_FAILURE);
}
printf("Client: message sent: %s \n", req.text);

/* Wait for a responses */


len = mq_receive(responses, (char*)&rep, sizeof(rep), NULL);
if(len < 0) {
perror("Client: mq_receive");
exit(EXIT_FAILURE);
}
printf("Client: message received: %s \n", rep.text);

/* close the message queues */


if((mq_close(requests) < 0) || (mq_close(responses) < 0)){
perror("Serveur : mq_close");
exit(EXIT_FAILURE);
}

return EXIT_SUCCESS;
}

Un autre exemple est également disponibles dans msg_queue/example_msg_server_pingpong.c et


msg_queue/example_msg_client_pingpong.c.

TELECOM SudParis — Denis Conan, Michel Simatic et François Trahay — Avril 2016 — module CSC4508/M2 94
Communications inter-processus 3 Communication à l’aide des IPC POSIX
' $
3.4 Mémoire partagée
 Création :
int shm_open(const char *name, int oflag, mode_t mode)
 Création (oflag contient O_CREAT) d’un objet de taille nulle
 Retour = descripteur de fichier

 Changer la taille de l’objet mémoire partagée :


int ftruncate(int fd, off_t length)

 Attachement :
# 17 void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset)
 addr = 0 → choix de l’adresse par le système d’exploitation
 Une même région peut être attachée plusieurs fois à des adresses différentes
 prot : droit d’accès (lecture/écriture/exécution)
 flags : MAP_SHARED pour partager la zone mémoire avec un autre processus
 fd : descripteur de fichier retourné par shm_open
 offset : emplacement de départ de la zone dans la mémoire partagée
 Retour : adresse d’attachement effective ou MAP_FAILED

 Détachement :
int munmap(void *addr, size_t length)
& %
' $
 Fermeture :
int close(int fd)

 Destruction :
int shm_unlink(const char *name)

# 18

& %

Un segment de mémoire partagée est un morceau de mémoire qui peut être commun à plusieurs pro-
cessus : l’espace mémoire virtuel est ainsi partagé. Toute modification effectuée par l’un des processus peut
automatiquement être connue des autres processus partageant ce morceau de mémoire. Trois étapes jalonnent
l’utilisation de la mémoire partagée :

• shm_open permet d’obtenir un identifiant associé au segment de mémoire partagée dont la clé est name.
Le paramètre oflag précise les conditions de création. Le dernier paramètre (mode) précise les droits
d’accès. Une fois le segment de mémoire partagé créé, il se comporte comme un fichier classique et peut
être manipulé à partir du descripteur de fichier fd.

• Lors de sa création, le segment de mémoire partagé est de taille nulle. Le programme peut, par la suite,
changer sa taille à l’aide de la fonction ftruncate.

TELECOM SudParis — Denis Conan, Michel Simatic et François Trahay — Avril 2016 — module CSC4508/M2 95
Communications inter-processus 3 Communication à l’aide des IPC POSIX

• mmap permet d’attacher (c’est-à-dire d’insérer) le segment de mémoire partagé dans l’espace d’adressage
virtuel du processus en précisant le descripteur de fichier du segment de mémoire partagé, l’adresse
préférée où attacher le segment (si la valeur NULL est fournie, le système place le segment dans le premier
espace libre suffisamment grand), les droits d’accès (une combinaison de PROT_EXEC, PROT_WRITE,
PROT_READ et PROT_NONE) et un drapeau permettant — entre autres — de spécifier que le segment
de mémoire est destiné à être partagé entre plusieurs processus (MAP_SHARED). En retour, la primitive
retourne l’adresse, dans l’espace d’adressage virtuel du processus, où le segment a été attaché.

Lors d’un fork, l’enfant hérite de la mémoire partagée. Lors d’un exec ou d’un exit, les segments
mémoire du processus sont automatiquement détachés.
La terminaison d’un programme utilisant un segment de mémoire partagée se décompose en trois étapes :
• munmap détache le segment de mémoire partagé
• close ferme le segment de mémoire partagé
• shm_unlink supprime le segment de mémoire partagé. Une fois le segment supprimé, plus aucun pro-
cessus ne peut l’utiliser.
Voici un exemple de communication à l’aide de segments de mémoire partagée IPC (fichiers
exemple_shm.c :
/*************************/
/* exemple_shm.c */
/*************************/

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <sys/stat.h> /* For mode constants */
#include <fcntl.h> /* For O_* constants */
#include <errno.h>
#include <time.h>
#include <semaphore.h>
#include <stdint.h>

/* key that identifies the shared memory */


#define SHM_KEY "/plop"

#define NB_ELEMENTS 10

typedef struct {
int tab[NB_ELEMENTS];
} shared_memory;

shared_memory *buffer;

#define SHM_SIZE sizeof(shared_memory)

void print_buffer() {
int i;
printf("The shared memory segment contains:\n");
for(i=0; i<NB_ELEMENTS; i++) {
printf("%d\t", buffer->tab[i]);
}
printf("\n");
}

int main(int argc, char *argv[]) {


int fd;
int rank;
int i;

if (argc < 2){


fputs( "USAGE: exemple_shm <rank>\n", stderr);
exit(EXIT_FAILURE);
}

rank = atoi(argv[1]);

/* open the shared memory segment */


fd = shm_open(SHM_KEY, O_RDWR, 0666);
if(fd<0) {
perror("shm_open");
exit(EXIT_FAILURE);
}

TELECOM SudParis — Denis Conan, Michel Simatic et François Trahay — Avril 2016 — module CSC4508/M2 96
3 Communication à l’aide des IPC POSIX 3.5 Sémaphores

/* set the size of the shared memory segment */


if(ftruncate(fd, SHM_SIZE) < 0) {
perror("ftruncate");
exit(EXIT_FAILURE);
}

/* map the shared memory segment in the current address space */


if((buffer = mmap(NULL, SHM_SIZE, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0)) <0) {
perror("mmap");
exit(EXIT_FAILURE);
}

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


print_buffer();
int v = buffer->tab[rank];
sleep(1);
v++;
buffer->tab[rank] = v;
}

/* unmap the memory region */


if(munmap(buffer, SHM_SIZE) < 0) {
perror("munmap");
exit(EXIT_FAILURE);
}

/* close the shared memory segment */


if(close(fd) < 0){
perror("close");
exit(EXIT_FAILURE);
}

return EXIT_SUCCESS;
}

' $
3.5 Sémaphores

3.5.1 Introduction aux sémaphores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20


# 19 3.5.2 Introduction aux sémaphores : analogie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
3.5.4 Introduction aux sémaphores : algorithmes P ET V . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
3.5.4 Sémaphores POSIX : initialisation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
3.5.5 Sémaphores POSIX : utilisation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25

& %

TELECOM SudParis — Denis Conan, Michel Simatic et François Trahay — Avril 2016 — module CSC4508/M2 97
3 Communication à l’aide des IPC POSIX 3.5 Sémaphores
' $
3.5.1 Introduction aux sémaphores

 Sémaphore = objet composé :


 D’une variable (sa valeur)
 D’une file d’attente (les processus bloqués)
 Primitives associées :
 Initialisation (avec une valeur positive ou nulle)
# 20  Manipulation :
I Prise (P ou Wait) = demande d’autorisation
I Validation (V ou Signal) = fin d’utilisation
 Principe : sémaphore associé à une ressource
 Prise = demande d’autorisation (Puis-je ?)
si valeur > 0 accord, sinon blocage
 Validation = restitution d’autorisation (Vas-y)
si valeur < 0 déblocage d’un processus
& %
Interprétation de la valeur du sémaphore :
• si valeur >= 0 : nombre d’autorisations
• si valeur <= 0 : nombre de processus bloqués
valeur = valeurInitiale − nb(P ) + nb(V )
La valeur initiale n’est jamais négative.
Le sémaphore est l’outil de base pour résoudre les problèmes de synchronisation entre processus (cf.
chapitre « Synchronisation entre processus »).
' $
3.5.2 Introduction aux sémaphores : analogie

 Parking de N places contrôlé par un feu

# 21

& %

• État initial : parking vide ==> valeur sémaphore = N


• Capteur d’entrée = rôle de P

TELECOM SudParis — Denis Conan, Michel Simatic et François Trahay — Avril 2016 — module CSC4508/M2 98
3 Communication à l’aide des IPC POSIX 3.5 Sémaphores

−1 sur valeur

si valeur >= 0 feu vert, sinon feu rouge

• Capteur de sortie = rôle de V

+1 sur valeur

si valeur <= 0 feu vert pour une voiture

sinon rien (une place disponible de plus)

• NB : si une voiture ne joue pas le jeu (elle ne passe pas devant les capteurs), elle met en péril le bon
fonctionnement du système :

– trop de voitures dans le parking

– OU BIEN feu rouge alors que le parking contient des places libres

' $
3.5.3 Introduction aux sémaphores : algorithmes P ET V
 Initialisation(sémaphore,n)
valeur[sémaphore] = n
 P(sémaphore)
valeur[sémaphore] = valeur[sémaphore] - 1
si (valeur[sémaphore] < 0) alors
étatProcessus = Bloqué
# 22 mettre processus en file d’attente
finSi
invoquer l’ordonnanceur
 V(sémaphore)
valeur[sémaphore] = valeur[sémaphore] + 1
si (valeur[sémaphore] <= 0) alors
extraire processus de file d’attente
étatProcessus = Prêt
finSi
invoquer l’ordonnanceur
& %

TELECOM SudParis — Denis Conan, Michel Simatic et François Trahay — Avril 2016 — module CSC4508/M2 99
3 Communication à l’aide des IPC POSIX 3.5 Sémaphores
' $
3.5.4 Sémaphores POSIX : initialisation

2 types de sémaphores :
 Sémaphores nommés : identifiés par une clé de la forme /un_nom. Les sémaphores
nommés sont persistants.
 Création / ouverture :
sem_t *sem_open(const char *name, int oflag, mode_t mode,
# 23 unsigned int value)
sem_t *sem_open(const char *name, int oflag)
 Fermeture / destruction :
int sem_close(sem_t *sem)
int sem_unlink(const char *name)
 Sémaphores anonymes : placés dans une zone mémoire partagée par plusieurs
threads ou processus (par exemple dans un segment de mémoire partagée).
 Initialisation :
int sem_init(sem_t *sem, int pshared, unsigned int value)
& %

' $
 Destruction :
int sem_destroy(sem_t *sem)

# 24

& %

TELECOM SudParis — Denis Conan, Michel Simatic et François Trahay — Avril 2016 — module CSC4508/M2 100
3 Communication à l’aide des IPC POSIX 3.5 Sémaphores
' $
3.5.5 Sémaphores POSIX : utilisation

Une fois ouvert/initialisés les deux types de sémaphores peuvent être manipulés avec :
 Prise :
int sem_wait(sem_t *sem)
# 25 int sem_trywait(sem_t *sem)
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout)
 Validation :
int sem_post(sem_t *sem)
 Consultation :
int sem_getvalue(sem_t *sem, int *sval)

& %

La création d’un sémaphore nommé se fait en fournissant son identifiant (name), un drapeau (O_CREATE
pour créer le sémaphore), les droits d’accès au sémaphore, ainsi que la valeur initiale du sémaphore.
La création d’un sémaphore anonyme consiste à initialiser un objet de type sem_t accessible par tous
les threads succeptibles d’utiliser le sémaphore. Si le sémaphore est utilisé par plusieurs processus, on place
généralement le sémaphore dans un segment de mémoire partagé par les processus. Dans ce cas, le paramètre
pshared doit être à 1 lors de l’appel à sem_init. Dans tous les cas, le paramètre value désigne la valeur
initiale du sémaphore.
Une fois la phase d’initialisation terminée, le sémaphore est utilisable avec sem_post (équivalent à l’opé-
ration V()) et sem_wait (équivalent à l’opération P()).
La fonction sem_wait est bloquante : si la valeur du sémaphore devient inférieure à 0, le thread se bloque
en attendant que la valeur du sémaphore redevienne positive. Pour éviter de bloquer indéfiniement, on peut
utiliser sem_timedwait qui fixe une date à partir de laquelle le thread doit être débloqué. Si sem_timedwait
se termine du fait de l’expiration du timer, la fonction retourne -1 et errno vaut ETIMEDOUT. Il est également
possible d’utiliser la fonction non-bloquante sem_trywait qui tente de prendre le sémaphore. En cas d’échec
(si la valeur du sémaphore est inférieure à 1), la fonction ne se bloque pas, mais retourne -1 et errno vaut
EAGAIN.
Deux fonctions sont traditionnellement réalisées lorsque l’on utilise les sémaphores. La première P(s)
(correspondant à la fonction sem_post) se traduit par « Puis-je ? », tandis que la seconde V(s) (correspondant
à la fonction sem_wait) signifie « Vas-y ! ». Lorsque l’on désire prendre une ressource, on effectue un P(s) ;
lorsque la ressource n’est plus nécessaire, on la relâche en effectuant un V(s).
L’exemple suivant utilise un sémaphore pour réaliser un parking.
Les deux programmes se partagent la constante CLE_PARKING définie dans exempleSemInit.h.
/****************/
/* exempleSem.h */
/****************/

#define CLE_PARKING "/exemple_cle"

Le programme exempleSemInit.c crée ce sémaphore (dont la clé est basée sur un nom de fichier) et
l’initialise :
/********************/
/* exempleSemInit.c */
/********************/

#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h> /* For O_* constants */
#include <sys/stat.h> /* For mode constants */
#include <semaphore.h>

TELECOM SudParis — Denis Conan, Michel Simatic et François Trahay — Avril 2016 — module CSC4508/M2 101
Communications inter-processus

#include "exempleSem.h"
#define NB_PLACES 2

int main(int argc, char *argv[]){


sem_t *semParking;

if (argc != 1) {
fputs("USAGE = exempleSemInit\n", stderr);
exit(EXIT_FAILURE);
}

/* Création et initialisation du sémaphore */


semParking = sem_open(CLE_PARKING, O_CREAT, S_IRWXU, NB_PLACES);
if (semParking == SEM_FAILED) {
perror("sem_open");
exit(EXIT_FAILURE);
}
puts("Initialisation OK, vous pouvez executer exempleSem");

int sval = -1;


if(sem_getvalue(semParking, &sval) <0) {
perror("sem_getvalue");
exit(EXIT_FAILURE);
}
printf("sval = %d\n", sval);

return EXIT_SUCCESS;
}

Le programme exempleSem.c utilise ce sémaphore pour protéger l’accès au parking :


/****************/
/* exempleSem.c */
/****************/

#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h> /* For O_* constants */
#include <sys/stat.h> /* For mode constants */
#include <semaphore.h>
#include <unistd.h>

#include "exempleSem.h"

void P(sem_t *sem_id){


sem_wait(sem_id);
}

void V(sem_t *sem_id){


sem_post(sem_id);
}

int main(int argc, char *argv[]){


sem_t *semParking;

if (argc != 1) {
fputs("USAGE = exempleSem\n", stderr);
exit(EXIT_FAILURE);
}

/* ouverture du sÃľmaphore deja crÃľÃľ */


semParking = sem_open(CLE_PARKING, 0);
if (semParking == SEM_FAILED) {
perror("sem_open");
exit(EXIT_FAILURE);
}

printf("Demande d’entree dans le parking...\n");

P(semParking);
printf("\a...OK\n");

sleep(5);

printf("Avertir de sortie du parking...\n");


V(semParking);
printf("\a...OK\n");

sleep(5);

return EXIT_SUCCESS;
}

TELECOM SudParis — Denis Conan, Michel Simatic et François Trahay — Avril 2016 — module CSC4508/M2 102
Communications inter-processus
' $
4 Comparaison des mécanismes de synchronisation

Temps écoulé pour envoyer un message via différents media de communication :


1. Tube pour transférer 4096 octets : 2, 37 µs
2. Tube pour transférer 416 octets en 2 écritures (longueur, puis donnée) : 1, 27 µs,
soit un gain de 47%
# 26
3. Tube pour transférer pointeur (suppose malloc et free) : 2, 20 µs, soit un gain de 7%
4. Tableau de zones mémoire géré par paradigme de synchronisation
Producteur/Consommateur avec des sémaphores POSIX : 0, 94 µs, soit un gain de
60%
5. Idem, mais avec des conditions POSIX : 3, 29 µs, soit un surcoût de 39%
6. File de messages (IPC et POSIX) : 0, 99 µs, soit un gain de 58%

& %

Cette comparaison des mécanismes de synchronisation a été réalisée lors des TP notés de 2009 (première
et deuxième session). Pour le code des expériences réalisées, se reporter au corrigé de ces TP notés.
Le principe de ces expériences est le suivant : un thread distributeur envoie 1.000.000 messages via un
medium de communication à des threads traiteur. Les performances sont mesurées à l’aide de la commande
clock_gettime sur une machine munie d’un processeur quadri-cœur Intel Core i7-4600U cadencé à 2.1 GHz,
8 Go de RAM et, du noyau Linux 4.4.0. Noter que cette comparaison est réalisée à l’aide de threads. De ce
fait, les threads sont en mesure de partager l’espace mémoire. Par exemple, une zone mémoire allouée via
malloc par le thread distributeur est libérable via free par un thread traiteur.
Voici le détail des expériences réalisées :

1. Le thread distributeur envoie systématiquement 4096 octets aux threads traiteur via un tube.

2. Dans cette méthode, on fait transiter moins d’octets dans le tube.

• Le thread distributeur écrit d’abord dans le tube la longueur de la chaîne qu’il s’apprête à écrire,
puis la chaîne elle-même (et pas tous les octets du tableau de caractères dans lequel est stockée
cette chaîne) ;
• Chaque thread traiteur lit d’abord la longueur de la chaîne qui a été écrite par le thread distributeur,
puis la chaîne elle-même.

Le gain obtenu (47%) est dû au fait qu’on divise par 4096/416 ' 10 le nombre d’octets transférés
(inutilement) entre la mémoire du thread distributeur et le tube d’une part, et entre le tube et la
mémoire des threads traiteurs d’autre part.

3. Dans cette méthode, on écrit encore moins d’octets dans le tuyau et on économise d’une part les
recopies d’octets entre la mémoire du thread distributeur et le tube, et d’autre part celles entre le tube
et la mémoire de chaque thread traiteur.

• À chaque requête, le thread distributeur alloue, à l’aide de malloc, une zone mémoire qu’il remplit ;
puis il stocke le pointeur (vers cette zone mémoire) dans le tube ;
• Chaque thread traiteur lit ce pointeur dans le tube, puis traite les données dans cette zones
mémoire, et enfin libère cette zone mémoire à l’aide de free.

Si on a gagné 7% de temps par rapport à la première méthode de travail, on a perdu par rapport au code
de la deuxième méthode (73% d’augmentation). Les malloc/free sont ici beaucoup plus pénalisants
que l’écriture/lecture des octets du message sur le tube.

TELECOM SudParis — Denis Conan, Michel Simatic et François Trahay — Avril 2016 — module CSC4508/M2 103
Communications inter-processus

4. Dans cette méthode, on remplace le tube par un tableau de 2 ∗ N cases (N étant le nombre de threads
traiteur) géré par les threads selon un paradigme de synchronisation Producteur/Consommateur, en
utilisant des sémaphores POSIX (sem_t).
• On définit un tableau de 2 ∗ N zones de TAILLE_MAX_REQUETE octets ;
• À chaque requête, le thread distributeur :
(a) essaye d’accéder à l’une des cases de ce tableau selon le paradigme de synchronisation Pro-
ducteur/Consommateur,
(b) communique l’adresse de cette case à la fonction chargée de remplir cette zone mémoire,
(c) informe les threads traiteur grâce au paradigme Producteur/Consommateur qu’une informa-
tion est prête ;
• Chaque thread traiteur attend qu’une information soit prête dans ce tableau ;
• Quand c’est le cas, le thread traiteur :
(a) recopie le contenu de la case prête dans une variable locale,
(b) indique au thread distributeur (grâce au paradigme Producteur/Consommateur) que cette
place est à nouveau disponible,
(c) transmet l’adresse de cette variable locale à sa fonction de traitement ;
• Toutes les opérations de synchronisation sur ces tableaux sont réalisées à l’aide de sémaphores
sem_t.
Le gain obtenu est dû au fait qu’on économise au maximum les transferts mémoire entre la mémoire
des threads et celle du tube. De plus, le mécanisme de synchronisation à base de sémaphore semble
plus efficace que celui basé sur les tubes.
5. Dans cette méthode, on utilise des conditions POSIX au lieu de sémaphores POSIX. Il apparaît que
les conditions sont moins performantes (ce qui semble logique, vu qu’on fait beaucoup plus d’appels à
des primitives de synchronisation tout au long du code).

6. Cette méthode s’appuie sur une file de message IPC (et non POSIX).
• le thread distributeur écrit ses requêtes en tant que messages dans une file de messages IPC
(primitive msgsnd et non mq_send) qu’il a créée au moment de l’initialisation du programme ;
• chaque thread traiteur lit un message sur cette file, puis appelle sa fonction de traitement avec la
requête contenue dans ce message.
• La file de message est supprimée, une fois que le programme a fini son exécution.
Les performances de ce code sont légèrement moins bonnes que les performances de la solution 4.
Toutefois, la solution à base de file de message a l’avantage de pouvoir fonctionner entre plusieurs
processus et nécessite un code plus “propre”. Une expérience réalisée avec les files de messages POSIX
montre que ces dernières ont un niveau de performance équivalent au files de messages IPC.

7. Quelles sont les performances d’un tube nommé par rapport à un tube standard ? La question est
ouverte (au sens où le test reste à faire).

TELECOM SudParis — Denis Conan, Michel Simatic et François Trahay — Avril 2016 — module CSC4508/M2 104
Synchronisation entre processus

Dominique Bouillet et Michel Simatic

module CSC4508/M2

Avril 2016

105
Synchronisation entre processus 1 Introduction
' $
Plan du document

1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
#2 2 Sémaphore = Outil de base . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
3 Résolution de problèmes de synchronisation typiques . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
4 Interblocage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
5 Mise en oeuvre dans un système d’exploitation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35

& %

' $
1 Introduction

 Les problèmes de synchronisation sont légions dans la vie courante :


 Quand on crédite son compte en banque, ce crédit ne doit pas être perdu parce
qu’en parallèle la banque débite un chèque
 Un parking de capacité N places ne doit pas laisser entrer N + 1 véhicules
#3  Roméo et Juliette ne peuvent se prendre par la main que s’ils se retrouvent à
leur rendez-vous
 Robert et Raymonde font la vaisselle. Raymonde lave. Robert essuie. L’égouttoir
les synchronise.
 Certaines lignes de train possèdent des sections de voie unique. Sur ces sections,
on ne peut avoir que des trains circulant dans un même sens à un instant donné
 On rencontre des problèmes similaires lorsqu’on utilise un système d’exploitation

& %

TELECOM SudParis — Dominique Bouillet et Michel Simatic — Avril 2016 — module CSC4508/M2 106
Synchronisation entre processus 2 Sémaphore = Outil de base
' $
1.1 Correspondance problèmes vie courante/informatique
Banque Problème d’exclusion mutuelle : une ressource ne doit être accessible que par une
entité à un instant donné. Cas, par exemple, d’une zone mémoire contenant le solde d’un
compte.

Parking Problème de cohorte : un groupe de taille bornée est autorisé à offrir/utiliser un


service. Cas, par exemple, d’un serveur de connexions Internet qui ne doit pas autoriser
plus de N connexions en parallèle.

#4 Roméo et Juliette Problème de passage de témoin : on divise le travail entre des processus.
Cas, par exemple, de 2 processus qui doivent s’échanger des informations à un moment
donné de leur exécution avant de continuer.

Robert et Raymonde Problème de producteurs/consommateurs : un consommateur ne


peut consommer que si tous les producteurs ont fait leur travail. Cas, par exemple, d’un
processus chargé d’envoyer des tampons qui ont été remplis par d’autres processus.

Train Problème de lecteurs/rédacteurs où l’on doit gérer, de manière cohérente, une


compétition entre différentes catégories d’entités. Cas, par exemple, d’une tâche de fond
périodique de « nettoyage » qui ne peut se déclencher que quand les tâches principales
sont inactives.
& %

' $
2 Sémaphore = Outil de base

#5 2.1 Généralités . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
2.2 Analogie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
3.0 Algorithmes P ET V . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9

& %

Le sémaphore est l’outil de base pour résoudre les problèmes de synchronisation.

TELECOM SudParis — Dominique Bouillet et Michel Simatic — Avril 2016 — module CSC4508/M2 107
Synchronisation entre processus 2 Sémaphore = Outil de base
' $
2.1 Généralités

 Sémaphore = objet composé :


 D’une variable (sa valeur)
 D’une file d’attente (les processus bloqués)
 Primitives associées :
 Initialisation (avec une valeur positive ou nulle)
#6  Manipulation :
I Prise (P ou Wait) = demande d’autorisation
I Validation (V ou Signal) = fin d’utilisation
 Principe : sémaphore associé à une ressource
 Prise = demande d’autorisation (Puis-je ?)
si valeur > 0 accord, sinon blocage
 Validation = restitution d’autorisation (Vas-y)
si valeur < 0 déblocage d’un processus
& %
Interprétation de la valeur du sémaphore :
• si valeur >= 0 : nombre d’autorisations
• si valeur <= 0 : nombre de processus bloqués
valeur = valeurInitiale − nb(P ) + nb(V )
La valeur initiale n’est jamais négative.
' $
2.2 Analogie

 Parking de N places contrôlé par un feu

#7

& %

• État initial : parking vide ==> valeur sémaphore = N


• Capteur d’entrée = rôle de P
−1 sur valeur

TELECOM SudParis — Dominique Bouillet et Michel Simatic — Avril 2016 — module CSC4508/M2 108
Synchronisation entre processus

si valeur >= 0 feu vert, sinon feu rouge

• Capteur de sortie = rôle de V

+1 sur valeur

si valeur <= 0 feu vert pour une voiture

sinon rien (une place disponible de plus)

• NB : si une voiture ne joue pas le jeu (elle ne passe pas devant les capteurs), elle met en péril le bon
fonctionnement du système :

– trop de voitures dans le parking

– OU BIEN feu rouge alors que le parking contient des places libres

' $
2.3 Algorithmes P ET V
 Initialisation(sémaphore,n)
valeur[sémaphore] = n
 P(sémaphore)
valeur[sémaphore] = valeur[sémaphore] - 1
si (valeur[sémaphore] < 0) alors
étatProcessus = Bloqué
#8 mettre processus en file d’attente
finSi
invoquer l’ordonnanceur
 V(sémaphore)
valeur[sémaphore] = valeur[sémaphore] + 1
si (valeur[sémaphore] <= 0) alors
extraire processus de file d’attente
étatProcessus = Prêt
finSi
invoquer l’ordonnanceur
& %

TELECOM SudParis — Dominique Bouillet et Michel Simatic — Avril 2016 — module CSC4508/M2 109
Synchronisation entre processus 3 Résolution de problèmes de synchronisation typiques
' $
3 Résolution de problèmes de synchronisation typiques

3.1 Exclusion mutuelle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10


#9 3.3 Cohorte. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .13
3.3 Passage de témoin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
3.4 Producteurs/Consommateurs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
3.5 Lecteurs/rédacteurs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28

& %

Le sémaphore permet d’élaborer des mécanismes de plus haut niveau qui sont étudiés dans cette partie.
On disposera ainsi de paradigmes (ou patrons), c’est-à-dire d’exemples-type permettant de modéliser
des classes de problèmes qui sont fréquemment rencontrés et qui sont présents à tous les niveaux dans les
systèmes et applications concurrentes.
Leur solution fournit des schémas de conception et de programmation de composants concurrents. Ils
servent de briques de base à toute étude, analyse ou construction de systèmes ou d’applications coopératives.
NB : il existe de nombreux autres problèmes/paradigmes aux noms plus poétiques les uns que
les autres (le problème du salon de coiffure, le problème du Père Noël, le problème de la montagne
russe. . .) [Downey, 2005].
' $
3.1 Exclusion mutuelle

 Permet la gestion d’accès concurrent à des ressources partagées


 Principe :
 Sémaphore mutex initialisé à 1
 Primitive P en début de section critique
 Primitive V en fin de section critique
# 10
 Exemple :
 Sémaphore mutex initialisé à 1
 Prog1 Prog2
P(mutex) P(mutex)
x=lire (cpte) x=lire (cpte)
x = x + 10 x = x - 100
écrire (cpte=x) écrire (cpte=x)
V(mutex) V(mutex)
& %

Quand Prog1 s’exécute sans que Prog2 soit activé, alors P(mutex) fait passer mutex à 0 et Prog1 entre
en section critique.

TELECOM SudParis — Dominique Bouillet et Michel Simatic — Avril 2016 — module CSC4508/M2 110
3 Résolution de problèmes de synchronisation typiques 3.3 Passage de témoin

Si Prog2 est activé quand Prog1 est en section critique, alors P(mutex) fait passer mutex à −1. Donc
Prog2 se retrouve bloqué. Quand Prog1 exécute V(mutex), alors mutex passe à 0 et Prog2 est débloqué.

' $
3.2 Cohorte

 Permet la coopération d’un groupe de taille maximum donnée


 Principe :
 Sémaphore parking initialisé à N
 Primitive P en début de besoin
 Primitive V en fin de besoin
# 11
 Exemple :
 Sémaphore parking initialisé à N
 Prog vehicule
...
P(parking)
|...
V(parking)
...
& %

Le (N + 1)e véhicule qui cherche à entrer dans le parking se retrouve bloqué par son P(parking). Il n’est
débloqué que quand un autre véhicule sort en faisant V(parking).
Noter que valeurSem[parking] = valeurInitialeSem[parking] − nb(P ) + nb(V )

' $
3.3 Passage de témoin

 Permet la coopération par division du travail entre processus

# 12  3 types :
 Envoi de signal
 Rendez-vous entre 2 processus
 Appel procédural entre processus

& %

TELECOM SudParis — Dominique Bouillet et Michel Simatic — Avril 2016 — module CSC4508/M2 111
3 Résolution de problèmes de synchronisation typiques 3.3 Passage de témoin
' $
3.3.1 Envoi de signal

 Permet à un processus d’envoyer un « top » à un autre pour signaler la


présence/disponibilité d’une information
 Principe :
 Sémaphore top initialisé à 0
 Primitive P pour attente « top »
# 13  Primitive V pour signal « top »
 Exemple :
 Sémaphore top initialisé à 0
 Prog1 Prog2
... ...
calcul(info) P(top)
V(top) utilisation(info)
... ...
& %

Ce mécanisme peut être utilisé par Prog1 pour donner à Prog2 le droit d’accès à une ressource quand
Prog1 estime que cette ressource est prête.
' $
3.3.2 Rendez-vous entre deux processus

 Permet à deux processus d’établir un point de synchronisation


 Principe :
 Sémaphore romeo initialisé à 0
# 14
 Sémaphore juliette initialisé à 0
 Prog1 Prog2
... ...
V(juliette) V(romeo)
P(romeo) P(juliette)
... ...

& %

Ce patron permet à Prog1 et Prog2 de s’échanger des informations à un moment précis de leur exécution
avant de continuer.
Généralisation : principe d’un rendez-vous entre N processus (NB : cet algorithme ne fonctionne qu’une
seule fois)

• Sémaphore rdv initialisé à 0

• Sémaphore mutex initialisé à 1

• Entier nbAttente initialisé à 0

TELECOM SudParis — Dominique Bouillet et Michel Simatic — Avril 2016 — module CSC4508/M2 112
Synchronisation entre processus 3 Résolution de problèmes de synchronisation typiques

...
P(mutex)
si nbAttente < N - 1 alors
nbAttente = nbAttente + 1
V(mutex)
P(rdv)
sinon
V(mutex)
répéter (N-1) fois
V(rdv)
fin répéter
nbAttente = 0 // Pas obligatoire, vu qu’algorithme ne fonctionne qu’une fois
finsi
...

' $
3.3.3 Appel procédural entre processus

 Permet à un processus de faire un « appel de procédure », alors que le code de cette


procédure est localisé dans un autre processus
 Principe :
 Sémaphore appel initialisé à 0
 Sémaphore retour initialisé à 0
# 15
 Serveur Client
répéter ...
P(appel) preparationParamAppel()
analyseParamAppel() V(appel)
... P(retour)
préparationParamRetour() analyseParamRetour()
V(retour) ...
finRépéter

& %

Serveur est le serveur d’« appel de procédure ». Il se met en attente d’un appel en faisant P(appel).

Client démarre. Il prépare ses paramètres d’appel en les mettant, par exemple, dans une zone de mémoire
partagée. Avec V(appel), il prévient Serveur de la disponibilité de ces informations.

Serveur est alors réactivé. Il analyse les paramètres d’appel, effectue son traitement et stocke les pa-
ramètres de retour dans une autre zone de mémoire partagée. Avec V(retour), il prévient Client de la
disponibilité de ces informations. Il se met ensuite en attente d’un autre appel.

Client est réactivé, analyse les paramètres retour et poursuit son traitement.

TELECOM SudParis — Dominique Bouillet et Michel Simatic — Avril 2016 — module CSC4508/M2 113
3 Résolution de problèmes de synchronisation typiques 3.4 Producteurs/Consommateurs
' $
3.4 Producteurs/Consommateurs

3.4.2 Objectif . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
3.4.2 Principe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
# 16 3.4.4 Déposer et extraire . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
3.4.4 Exemple . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
3.4.6 K producteurs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
3.4.6 Exemple de problème avec 2 producteurs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
3.5.1 Solution complète . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28

& %

' $
3.4.1 Objectif

 Permettre le contrôle de flux entre un (ou des) producteur(s) et un (ou des)


consommateur(s) dans le cas où ils communiquent via un tampon mémoire de
N cases
 1. Exécution Produc : il produit info0
info0
# 17
2. Exécution Produc : il produit info1
info0 info1
3. Exécution Conso : il consomme info0
info1
4. Exécution Produc : il produit info2
info1 info2

& %

Ce patron est une généralisation des fonctionnalités offertes par les tubes (transit d’information entre K
processus producteurs et P processus consommateurs).

TELECOM SudParis — Dominique Bouillet et Michel Simatic — Avril 2016 — module CSC4508/M2 114
3 Résolution de problèmes de synchronisation typiques 3.4 Producteurs/Consommateurs
' $
3.4.2 Principe

 Sémaphore infoPrete initialisé à 0


 Sémaphore placeDispo initialisé à N
 Produc Conso
répéter répéter
# 18
... P(infoPrete)
calcul(info) extraire(info)
P(placeDispo) V(placeDispo)
déposer(info) utiliser(info)
V(infoPrete) finRépéter
...
finRépéter

& %

Ici, le producteur Produc peut écrire N informations avant d’être bloqué (degré de liberté N ).

Chaque itération de Conso libère une case.

' $
3.4.3 Déposer et extraire

 Le tampon ne peut s’étendre à l’infini → Tampon géré de façon circulaire,


c’est-à-dire que quand dernière case utilisée, retour à la première
 Il faut :
 un indice de dépot iDépot
 un indice d’extraction iExtrait
# 19
 ajouter à l’initialisation iDépot = 0 et iExtrait = 0
 déposer(info)
tampon[iDépot] = info
iDépot = (iDépot + 1) modulo N
 extraire(info)
info = tampon[iExtrait]
iExtrait = (iExtrait + 1) modulo N

& %

TELECOM SudParis — Dominique Bouillet et Michel Simatic — Avril 2016 — module CSC4508/M2 115
3 Résolution de problèmes de synchronisation typiques 3.4 Producteurs/Consommateurs
' $
3.4.4 Exemple
1. On suppose qu’à un instant donné, le tampon est dans l’état suivant :
 Tampon : info8
 valeurSem[placeDispo] = 4 et valeurSem[inf oP rete] = 1
 iDépot = 4 et iExtrait = 3
2. Exécution Produc : il produit info9

# 20  Tampon : info8 info9


 valeurSem[placeDispo] = 3 et valeurSem[inf oP rete] = 2
 iDépot = 0 et iExtrait = 3
3. Exécution Produc : il produit infoA
 Tampon : infoA info8 info9
 valeurSem[placeDispo] = 2 et valeurSem[inf oP rete] = 3
 iDépot = 1 et iExtrait = 3
4. Exécution Conso : il consomme info8
& %
' $
 Tampon : infoA info9
 valeurSem[placeDispo] = 3 et valeurSem[inf oP rete] = 2
 iDépot = 1 et iExtrait = 4

# 21

& %

NB :

• déposer(info) et extraire(info) sont simples grâce aux sémaphores placeDispo et infoPrete.

• La valeur des sémaphores ne suffit pas à déterminer où il faut écrire ou lire en premier. Si on considère
les deux tampons mémoire suivants :

– Tampon 1 : info info info


valeurSem[placeDispo] = 2 et valeurSem[inf oP rete] = 3
iDépot = 3 et iExtrait = 0

– Tampon 2 : info info info

TELECOM SudParis — Dominique Bouillet et Michel Simatic — Avril 2016 — module CSC4508/M2 116
3 Résolution de problèmes de synchronisation typiques 3.4 Producteurs/Consommateurs

valeurSem[placeDispo] = 2 et valeurSem[inf oP rete] = 3

iDépot = 0 et iExtrait = 2

Si les valeurs de sémaphores sont identiques, iDépot et iExtrait sont différents !

' $
3.4.5 K producteurs

 Ce modèle semble rester valable pour plusieurs producteurs (ou plusieurs


# 22 consommateurs) en termes de synchronisation. . .
 . . .mais en fait risque d’information perdue à cause des indices iDépot (pour
K producteurs) ou iExtrait (pour P consommateurs) qui sont des variables globales
(cf. exemple)

& %

' $
3.4.6 Exemple de problème avec 2 producteurs

 soit un tampon avec N = 5 et deux dépots effectués sans extraction

# 23  L’état courant est alors :


 Tampon : info0 info1
 valeurSem[placeDispo] = 3 et valeurSem[inf oP rete] = 2
 iDépot = 2 et iExtrait = 0

& %

TELECOM SudParis — Dominique Bouillet et Michel Simatic — Avril 2016 — module CSC4508/M2 117
3 Résolution de problèmes de synchronisation typiques 3.4 Producteurs/Consommateurs
' $
Exemple de problème avec 2 producteurs (suite)
1. Début d’exécution de Produc1 :
P(placeDispo);tampon[iDépot] = infoP1;...
 Tampon : info0 info1 infoP1
 valeurSem[placeDispo] = 2 et valeurSem[inf oP rete] = 2
 iDépot = 2 et iExtrait = 0

# 24 2. Exécution complète de Produc2 : P(placeDispo);tampon[iDépot] =


infoP2;iDépot=(iDépot+1) modulo N;V(infoPrete)
 Tampon : info0 info1 infoP2
 valeurSem[placeDispo] = 1 et valeurSem[inf oP rete] = 3
 iDépot = 3 et iExtrait = 0
3. Fin d’exécution de Produc1 :
...;iDépot=(iDépot+1) modulo N;V(infoPrete)
 Tampon : info0 info1 infoP2 i !* ?#
& %
' $
 valeurSem[placeDispo] = 1 et valeurSem[inf oP rete] = 4
 iDépot = 4 et iExtrait = 0

# 25

& %

Conclusion de l’exemple :

• Valeur des sémaphores et indices correcte :

– 1 case libre : la dernière


– 4 infos disponibles à partir de 0

• Mais

– infoP1 écrasée (par infoP2)


– Vu que le prochain producteur écrira dans la case 4 (la dernière), l’information qui sera lue par
un consommateur en case 3 sera erronée : l’information en case 3 est non valide.

TELECOM SudParis — Dominique Bouillet et Michel Simatic — Avril 2016 — module CSC4508/M2 118
3 Résolution de problèmes de synchronisation typiques 3.5 Lecteurs/rédacteurs

' $
3.4.7 Solution complète

 Il faut une exclusion mutuelle sur déposer() et extraire() autour de iDépot et


iExtrait
 Pour K producteurs :
 Sémaphore mutex initialisé à 1
 déposer(info)
# 26
P(mutex)
|tampon[iDépot] = info
|iDépot = (iDépot + 1) modulo N
V(mutex)
 Pour P consommateurs, idem pour extraire
 Pour K producteurs et P consommateurs, utiliser deux sémaphores mutexP et
mutexC

& %

' $
3.5 Lecteurs/rédacteurs

3.5.1 Objectif . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
# 27 3.5.3 Solution de base . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .30
3.5.3 Analyse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
3.5.4 Solution avec priorités égales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31

& %

TELECOM SudParis — Dominique Bouillet et Michel Simatic — Avril 2016 — module CSC4508/M2 119
3 Résolution de problèmes de synchronisation typiques 3.5 Lecteurs/rédacteurs
' $
3.5.1 Objectif

 Permettre une compétition cohérente entre deux types de processus (les « lecteurs »
# 28 et les « rédacteurs ») :
 Plusieurs lecteurs peuvent accéder simultanément à la ressource
 Les rédacteurs sont exclusifs entre eux pour leur exploitation de la ressource
 Un rédacteur est exclusif avec les lecteurs

& %

Ce patron permet, par exemple, d’interdire à un processus d’écrire dans un fichier, si des processus sont
en train de lire (simultanément) ce fichier. De plus, deux processus ne peuvent écrire simultanément dans le
fichier. En revanche, plusieurs processus peuvent lire simultanément le fichier.
Le paradigme Lecteurs/Rédacteur permet de réaliser une exclusion mutuelle entre un groupe d’entités (les
lecteurs) et une entité (le rédacteur). Il est généralisable à l’exclusion mutuelle entre deux groupes d’entités
(cf. TP noté 2006, question 2 : exclusion mutuelle entre les nains et les ours).
' $
3.5.2 Solution de base
 Sémaphore mutexG initialisé à 1
 Sémaphore mutexL initialisé à 1
 Entier NL initialisé à 0
 Lecteur Rédacteur
P(mutexL) P(mutexG)
|NL = NL + 1 | ecrituresEtLectures()
|si NL == 1 alors V(mutexG)
# 29
| P(mutexG)
|finSi
V(mutexL)
lectures()
P(mutexL)
|NL = NL - 1
|si NL == 0 alors
| V(mutexG)
|finSi
V(mutexL)
& %

• Le « G » de mutexG signifie « Général ».

• Le « L » de mutexL signifie « Lecteur ».

TELECOM SudParis — Dominique Bouillet et Michel Simatic — Avril 2016 — module CSC4508/M2 120
3 Résolution de problèmes de synchronisation typiques 3.5 Lecteurs/rédacteurs
' $
3.5.3 Analyse

 La solution de base fonctionne, mais on constate qu’il y a une possibilité de famine


pour les rédacteurs.
# 30  Pour éviter cette famine, on ajoute les contraintes suivantes :
 Si la ressource est utilisée par un lecteur :
I Tout écrivain est mis en attente.
I Tout lecteur est accepté s’il n’y a pas d’écrivain en attente.
I Tout lecteur est mis en attente s’il y a un écrivain en attente.

& %

Exemple de famine pour les rédacteurs :

• Un lecteur L1 se met à lire

• Un rédacteur R arrive et se met donc en attente de la fin de lecture de L1 .

• Un lecteur L2 arrive. Vu l’algorithme, il est autorisé à lire sans attendre

• L1 termine sa lecture. Comme L2 n’a pas fini sa lecture, R continue à attendre.

• Un lecteur L3 arrive. . .

• R peut potentiellement ne jamais réussir à écrire.

' $
3.5.4 Solution avec priorités égales
 Sémaphore mutexG initialisé à 1
 Sémaphore mutexL initialisé à 1
 Sémaphore fifo initialisé à 1
 Entier NL initialisé à 0
 Lecteur Rédacteur
P(fifo) P(fifo)
P(mutexL) P(mutexG)
NL = NL + 1 V(fifo)
# 31 si NL == 1 alors ecrituresEtLectures()
P(mutexG) V(mutexG)
finSi
V(mutexL)
V(fifo)
lectures()
P(mutexL)
NL = NL - 1
si NL == 0 alors
V(mutexG)
finSi
V(mutexL)
& %

TELECOM SudParis — Dominique Bouillet et Michel Simatic — Avril 2016 — module CSC4508/M2 121
Synchronisation entre processus 4 Interblocage

fifo signifie « First In First Out ».

Attention, cette solution ne marche que parce qu’on suppose que la file d’attente de processus en attente
sur le sémaphore fifo est elle-même gérée en FIFO ! Si ce n’est pas le cas, on peut encore avoir famine.

' $
4 Interblocage

# 32 4.2 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
4.2 Généralités . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34

& %

' $
4.1 Introduction
 Interblocage (Deadlock) = toute situation telle que deux processus au moins sont
chacun en attente d’une ressource non partageable déjà allouée à l’autre
 Exemple : exclusion mutuelle sur deux ressources différentes
 Sémaphore mutex1 initialisé à 1
 Sémaphore mutex2 initialisé à 1
 Prog1 Prog2
# 33 ... ...
P(mutex1) P(mutex2)
|accès à ressource 1 |accès à ressource 2
|P(mutex2) |P(mutex1)
||accès à ressource 2 ||accès à ressource 1
|V(mutex2) |V(mutex1)
V(mutex1) V(mutex2)
... ...
 Les deux programmes se bloqueront mutuellement si Prog1 fait P(mutex1) alors
que simultanément Prog2 fait P(mutex2)
& %

TELECOM SudParis — Dominique Bouillet et Michel Simatic — Avril 2016 — module CSC4508/M2 122
Synchronisation entre processus
' $
4.2 Généralités

 Synonymes d’interblocage : verrou mortel ou étreinte fatale


 Conditions nécessaires et suffisantes (Coffman, 1971)
4 CNS simultanées :
1. Ressources en exclusion mutuelle (ressources concernées non partageables)
# 34 2. Processus en attente (processus conserve les ressources allouées)
3. Non préemption des ressources (ressources concernées non préemptibles)
4. Chaîne circulaire de processus bloqués (généralisation des demandes réciproques)
 3 stratégies de lutte contre les blocages :
 Prévention (statique)
 Détection avec guérison
 Évitement (prévention dynamique)

& %

L’interblocage est approfondi dans l’U.V. « Algorithmique et communications des applications »


(CSC4509).
' $
5 Mise en oeuvre dans un système d’exploitation

 Sémaphores (cf. chapitres « Communications inter-processus » et « Threads »)


 Sémaphores IPC
 Sémaphores Posix
 Moniteurs (ou conditions ou variables-condition)
# 35
 Le moniteur ajoute le type condition et une file d’attente associée à chaque
variable de type condition ainsi que des primitives wait x, signal x (x étant
une condition).
 Si besoin, un processus se bloque par wait et libère le moniteur
 Un processus réveille un des processus en attente par signal
 Il peut également réveiller tous les processus en attente par broadcast

& %

• Les moniteurs (plus exactement les conditions) C sont étudiées dans le chapitre « Threads ».

• En Java, on a :

– le mot-clé synchronized sur les méthodes


– les méthodes wait(), notify() et notifyAll()
– une condition anonyme avec une seule file d’attente par moniteur

TELECOM SudParis — Dominique Bouillet et Michel Simatic — Avril 2016 — module CSC4508/M2 123
Synchronisation entre processus

• Des exemples de code en environnement Windows sont présentés dans l’article « OMG, Multi-Threading
is easier than Networking » (http://www.gamasutra.com/view/feature/4006/sponsored_feature_
omg_.php)

Bibliographie du chapitre
[Downey, 2005] Downey, A. B. (2005). The Little Book of Semaphores. GreenTeaPress, http ://greentea-
press.com/semaphores/. Version 2.1.5.

TELECOM SudParis — Dominique Bouillet et Michel Simatic — Avril 2016 — module CSC4508/M2 124
Threads ou processus légers

Éric Renault et Frédérique Silber-Chaussumier

module CSC4508/M2

Avril 2016

125
Threads ou processus légers 1 Présentation
' $
Plan du document

1 Présentation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
2 Création/destruction de threads . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
#2 3 Partage des données . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
4 Synchronisation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
5 Utilisation et limitations des threads . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
6 Autres fonctions de la bibliothèque POSIX threads . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36

& %

' $
1 Présentation

#3 1.1 Bibliographie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.3 Threads . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.3 Détacher le flot d’exécution des ressources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7

& %

TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 126
Threads ou processus légers 1 Présentation
' $
1.1 Bibliographie
 « Computer Systems : A Programmer’s Perspective », R. E. Bryant et D. R.
O’Hallaron, Prentice Hall, 2003.
 « UNIX SYSTEMS Programming : communication, Concurrency and Threads », K.
A. Robbins et S. Robbins, Prentice Hall, 2003.
 « Programmation système en C sous Linux », C. Blaess, Eyrolles, 2000.
 « Understanding the Linux kernel, 2nd edition », Daniel P. Bovet et M. Cesati,
#4 O’Reilly, 2003.
 Threads spécifiquement
 « Pthreads Programming : A POSIX Standard for Better Multiprocessing », B.
Nichols, D. Buttlar et J. P. Farrell, O’Reilly and Associates, 1996.
 « Threads Primer : A Guide to Multithreaded Programming », B. Lewis, D. Berg
et B. Lewis, Prentice Hall, 1995.
 « Programming With POSIX Threads », D. Butenhof, Addison Wesley, 1997.
 « Techniques du multithread : du parallélisme dans les processus », B. Zignin,
Hermès, 1996.
& %

Le cours de Bryant et Hallaron est très bien fait. Il part des couches basses pour expliquer les différentes
notions : les images mémoire des processus pour expliquer la notion de thread, les instructions liées à une
lecture et à une écriture pour expliquer les problèmes de synchronisation avec pour exemple le compteur.
Le livre Robbins et Robbins est très progressif. Il explique notamment le sens de thread-safe avec strtok()
comme exemple (p. 39) et y revient dans un chapitre sur les threads POSIX avec comme exemples strerr()
et errno (p. 432). Les threads sont expliqués sur trois chapitres avec beaucoup d’exemples, notamment des
exemples détaillés de gestion des signaux avec des threads.
Le livre de C. Blaess fournit beaucoup d’explications sur l’implantation des threads sous Linux même s’il
est déjà un peu ancien par exemple ce qui concerne la directive _REENTRANT est déjà obsolète.
' $
1.2 Threads

 Traduction : fil d’exécution, processus légers


 Principe : détacher le flot d’exécution des ressources
 Introduits dans divers langages et systèmes :

#5  Programmation concurrente
 Utilisation des ressources simultanément : recouvrement du calcul et des
entrées/sorties
 Exploitation des architectures multiprocesseurs
I Architectures SMP ou CMP
I Plus généralement des architectures à mémoire partagée
 Exploitation de la technologie hyper-threading

& %

SMP : Symmetric MultiProcessor


CMP : Chip MultiProcessor

TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 127
1 Présentation 1.3 Détacher le flot d’exécution des ressources

' $
1.3 Détacher le flot d’exécution des ressources

#6 1.3.1 Vision traditionnelle d’un processus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7


1.3.3 Autre vision d’un processus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
1.3.3 Processus multi-thread . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9

& %

' $
1.3.1 Vision traditionnelle d’un processus

 Processus mono-thread
 Contexte : contexte d’exécution + contexte du noyau
 Espace d’adressage : code, données et pile

#7

& %

Sur une machine 32 bits, le processus dispose d’un espace d’adressage s’étendant jusqu’à 3 Go. La taille
de l’espace d’adressage d’un processus est plus importante sur une machine 64 bits, mais les explications
suivantes restent valables. Sur le schéma de cette page, les différents segments sont schématisés en trois zones.
La zone “Code” contient le code et les données en écriture seule, la zone “Données” contient les données en
lecture/écriture et le “Tas” contient les données allouées dynamiquement. La zone “Pile” représente la pile
d’exécution du processus.
Le contexte du processus est représenté divisé en deux. Le contexte d’exécution représente les informations
sur l’état d’exécution du programme : registres, instruction courante et pointeur de pile. Le contexte du noyau
représente les informations sur l’état des ressources nécessaires au processus : structures permettant de gérer

TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 128
1 Présentation 1.3 Détacher le flot d’exécution des ressources

la mémoire, les tables de descripteurs pour accéder aux fichiers, aux ports de communication... et le pointeur
vers la fin du segment de données, indiqué pointeur brk (voir le chapitre sur la mémoire).
Les schémas sont tirés du cours de O’Hallaron et al. à l’université Carnegie Mellon.
' $
1.3.2 Autre vision d’un processus

 Détacher le flot d’exécution des ressources


 Processus mono-thread
 Fil d’exécution ou thread : pile + contexte d’exécution
 Code, données et le contexte du noyau

#8

& %

Cette autre vision du processus détache le flot d’exécution des ressources. Les informations nécessaires au
cours de déroulement du programme c’est-à-dire la pile et le contexte d’exécution sont isolées des informations
concernant les ressources stockant le code, les données et le contexte du noyau. Ceci fait donc apparaître
les informations nécessaires au fil d’exécution, en anglais « thread» et les informations qui pourront être
partagées par plusieurs fils d’exécution.
' $
1.3.3 Processus multi-thread

 Processus multi-thread
 Plusieurs fils d’exécution
 Code, données et contexte du noyau partagés : notamment partage des fichiers
et des ports de communication

#9

& %

Dans le cas d’un processus multi-thread, les différents fils d’exécution caractérisés par leur pile, l’état des
registres et le compteur ordinal se partagent le code, les données, le tas et le contexte du noyau notamment

TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 129
Threads ou processus légers 2 Création/destruction de threads

les tables de descripteurs. Les fichiers et les ports de communication par exemple sont partagés.
La pile d’un processus habituel n’est contrainte que par la limite de la zone nommée tas, dans la-
quelle les variables dynamiques sont allouées. En principe, la pile d’un tel processus pourrait croître
jusqu’à remplir l’essentiel de l’espace d’adressage du programme, soit environ 3 Go. Dans le cas d’un
programme multithread, les différentes piles doivent être positionnées à des emplacements figés dès
la création des threads, ce qui impose des limites de taille puisqu’elles ne doivent pas se rejoindre.
Dans la bibliothèque POSIX, l’adresse et la taille maximum de la pile sont données par : _PO-
SIX_THREAD_ATTR_STACKADDR et _POSIX_THREAD_ATTR_STACKSIZE. La taille minimum
de la pile est définie par PTHREAD_STACK_MIN correspondant à 16 Ko sous Linux. (Voir le “Blaess”
p.286-287)
' $
2 Création/destruction de threads

# 10 2.1 Pthread “Hello world” . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11


2.2 Ensemble de threads pairs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
2.3 Threads POSIX : création/destruction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14

& %

' $
2.1 Pthread “Hello world”

 Interface POSIX Pthread


 Fournir une fonction point d’entrée

# 11

 Visualisation des différents threads avec la commande ps


& %

TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 130
Threads ou processus légers 2 Création/destruction de threads

Voilà un premier programme multi-thread. Ce programme utilise l’interface POSIX Pthread. Il doit être
compilé avec l’option -pthread de gcc (voir le man de gcc) :
gcc helloworld.c -pthread -o helloworld

#include <pthread.h>
#include <time.h>
#include <error.h>
#include <stdlib.h>
#include <stdio.h>

void *thread_function( void *arg )


{
struct timespec sleeptime;

printf("Hello world\n");

/* ralentissement */
sleeptime.tv_sec = 10;
sleeptime.tv_nsec = 0;

nanosleep(&sleeptime, NULL);

pthread_exit(NULL);
}

int main(int argc, char * argv [])


{
pthread_t thread;
int rc;
rc = pthread_create( &thread, NULL, thread_function, NULL);
if(rc)
error(EXIT_FAILURE, rc, "pthread_create");

rc = pthread_join(thread, NULL);
if(rc)
error(EXIT_FAILURE, rc, "pthread_join");

return EXIT_SUCCESS;
}

Un thread est créé pour afficher “Hello world”.


Le thread principal déclare un thread de type pthread_t et appelle la fonction pthread_create().
Cette fonction prend comme argument l’adresse du thread, les attributs de ce thread (souvent à NULL), un
pointeur vers la fonction point d’entrée ici thread_function() et enfin un pointeur vers les arguments de
cette fonction (NULL dans notre cas).
Le thread créé exécute la fonction thread_function() et se termine avec pthread_exit().
pthread_exit() prend comme argument une éventuelle valeur de retour. Le thread principal attend la
terminaison du thread créé, libère les ressources liées à ce thread et récupère la valeur de retour avec
pthread_join().
Le thread principal se termine avec exit(). Il faut noter que l’appel à exit() envoie un signal de
terminaison à tous les threads du processus. Il faut donc s’assurer, comme c’est le cas ici grâce à l’appel à
pthread_join(), que tous les autres threads sont déjà terminés.
La commande ps -L (ou ps -m) selon les versions affiche les différents threads. Vérifiez qu’un seul iden-
tifiant de processus apparaît pour l’ensemble des threads.

Remarque : Pour les anciennes versions de la libc (inférieures à la libc6) il est nécessaire de compiler les
programmes multithread avec la directive _REENTRANT pour définir des fonctions réentrantes supplémentaires
et fournir une définition correcte de errno. Cette directive n’est plus nécessaire à partir de la libc6 (voir
notamment features.h).

Interactivement pendant le cours : exécution sur une machine de la salle de TP du programme


helloworld avec temporisation pour visualiser les différents threads.

TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 131
2 Création/destruction de threads 2.3 Threads POSIX : création/destruction
' $
2.2 Ensemble de threads pairs

 Création d’un ensemble de threads pairs


 Pas d’arborescence parent/enfant comme pour les processus

# 12

& %

Les threads ne forment pas une arborescence comme les processus. Lorsqu’un processus crée un thread,
on parle alors de thread principal et de thread pair. L’ensemble de threads contient alors deux threads. Un
thread, principal ou non, peut créer un autre thread qui rejoint alors l’ensemble de threads.

' $
2.3 Threads POSIX : création/destruction

# 13 2.3.1 Identification . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
2.3.2 Utilisation. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .15
2.3.3 Attributs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18

& %

TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 132
2 Création/destruction de threads 2.3 Threads POSIX : création/destruction
' $
2.3.1 Identification

 Un équivalent du pid_t : pthread_t

# 14  Identifiant du thread courant :


 pthread_t pthread_self (void)
 Comparaison de deux identifiants :
 int pthread_equal (pthread_t thread1, pthread_t thread2)

& %

Les threads sont identifiés par un objet de type pthread_t.


La fonction pthread_self permet de connaître l’identifiant du thread courant (retourné à l’appel de la
fonction).
Si le type pid_t est défini comme étant un entier, il n’en est pas de même pour le type thread_t. Sa
définition est dépendante du système utilisé. Aussi, afin d’éviter tout problème, la comparaison de deux
identifiants de thread est effectué par l’intermédiaire de la fonction pthread_equal.
' $
2.3.2 Utilisation

 Création
 int pthread_create (pthread_t *thread, pthread_attr_t *attr, void *(*
start_routine) (void *), void *arg)

 Terminaison
# 15
 void pthread_exit (void *retval)
 int pthread_join (pthread_t thread, void **thread_return)
 int pthread_detach (pthread_t thread)
 Attention : pour la plupart des fonctions de la bibliothèque Threads POSIX, un code
d’erreur non nul est renvoyé en cas de problème mais errno n’est pas
nécessairement positionné.

& %

pthread_create permet de créer un nouveau fil d’exécution pour le processus courant. Pour cela, le
premier paramètre contient l’adresse où sera placé l’identifiant du thread au sortir de la fonction. Le second
paramètre pointe sur les attributs associés au thread dès sa création (cf les transparents « Attributs »). Les
deux derniers paramètres représentent respectivement l’adresse de la fonction devant être exécutée par le
nouveau thread et le paramètre fourni à cette fonction. La valeur retournée est 0 en cas de succès et le code

TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 133
2 Création/destruction de threads 2.3 Threads POSIX : création/destruction

de l’erreur sinon.
Le thread s’exécute jusqu’à la fin de la fonction, sauf s’il effectue un appel à pthread_exit. Dans ce cas,
le fil d’exécution est explicitement interrompu et la valeur passée en paramètre correspond à la valeur de fin
d’exécution du thread. L’objet spécifié est de type void * pour des raisons de généricité.
Les ressources liées à un thread ne sont pas libérées lorsque ce thread se termine sauf s’il s’agit d’un
thread détaché. Pour libérer les ressources liées à un thread non détaché, il faut qu’un autre thread appelle
pthread_join.
pthread_join permet d’attendre la fin de l’exécution d’un thread. Cette fonction bloque jusqu’à ce que
le fil d’exécution associé à l’identifiant du premier paramètre se termine. La valeur de fin d’exécution du
thread est alors copiée à l’emplacement pointé par le second paramètre.
pthread_detach permet de « détacher » un fil d’exécution, c’est-à-dire qu’il n’est pas possible pour
un autre thread de se synchroniser avec lui avec la fonction pthread_join et que ses ressources seront
automatiquement libérées dès qu’il se terminera.
Les threads peuvent aussi se terminer par un mécanisme d’annulation. Tout thread peut deman-
der l’annulation d’un autre thread. Chaque thread contrôle le fait de pouvoir être annulé ou non (voir
pthread_cancel()).

' $
2.3.3 Attributs

 Création et destruction d’une structure d’attributs


 int pthread_attr_init (pthread_attr_t *attr)
 int pthread_attr_destroy (pthread_attr_t *attr)
# 16  État joignable / détaché
 int pthread_attr_getdetachstate(const pthread_attr_t *attr, int
*detachstate)
 int pthread_attr_setdetachstate(const pthread_attr_t *attr, int
detachstate)
 → detachstate = PTHREAD_CREATE_[JOINABLE|DETACHED]

& %

Chaque fil d’exécution est doté d’un certain nombre d’attributs, regroupé dans un type opaque
pthread_attr_t. Les attributs sont fixés lors de la création du thread. Lorsque les attributs par défaut
sont suffisants, on passe généralement un pointeur NULL. Un grand nombre de fonctions sont fournies afin
de manipuler les attributs associés aux fils d’exécution. Les deux premières — pthread_attr_init et
pthread_attr_destroy — permettent respectivement d’initialiser et de détruire le lot d’attributs sur lequel
pointe le premier paramètre. Les autres fonctions permettent respectivement de lire (pthread_attr_getX)
et de modifier (pthread_attr_setX) l’état des attributs. Un couple de fonctions est fourni par attribut, le
X représentant le nom de l’attribut. Le premier paramètre est un pointeur sur le lot d’attributs ; tandis que
le second peut être la nouvelle valeur de l’attribut ou l’adresse où sera placée la valeur courante de l’attribut
selon que l’on effectue une mise à jour ou une lecture.
L’attribut detachstate permet de spécifier s’il sera possible de se synchroniser sur le fil d’exécution une
fois qu’il sera terminé. Deux valeurs sont possibles : PTHREAD_CREATE_JOINABLE pour autoriser la synchro-
nisation et PTHREAD_CREATE_DETACHED pour la décliner.

TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 134
Threads ou processus légers
' $
Attributs (2/2)
 Politique d’ordonnancement des threads
int pthread_attr_getschedpolicy(const pthread_attr_t *attr, int *policy)

int pthread_attr_setschedpolicy(const pthread_attr_t *attr, int policy)

→ policy = SCHED_[OTHER|RR|FIFO]
 Paramètres d’ordonnancement
# 17  Priorité d’ordonnancement
int pthread_attr_getschedparam(const pthread_attr_t *attr, const struct
sched_param *param)

int pthread_attr_setschedparam(const pthread_attr_t *attr, struct


sched_param *param)
 Héritage de l’ordonnancement
int pthread_attr_getinheritsched(const pthread_attr_t *attr, int
*inherit)
& %

' $
int pthread_attr_setinheritsched(const pthread_attr_t *attr, int inherit)

→ inherit = PTHREAD_[EXPLICIT|INHERIT]_SCHED
 Interprétation des valeurs d’ordonnancement
int pthread_attr_getscope(const pthread_attr_t *attr, int *scope)

int pthread_attr_setscope(const pthread_attr_t *attr, int scope)

→ scope = PTHREAD_SCOPE_[SYSTEM|PROCESS]
# 18  Adresse de la pile du thread
int pthread_attr_getstackaddr(const pthread_attr_t *attr, void *addr)

int pthread_attr_setstackaddr(const pthread_attr_t *attr, void *addr)

 Taille de la pile du thread


int pthread_attr_getstacksize(const pthread_attr_t *attr, int size)

int pthread_attr_setstacksize(const pthread_attr_t *attr, int *size)

& %

Les attributs schedpolicy, schedparam, scope et inheritsched concernent l’ordonancement des fils
d’exécution. Ils ne sont disponibles que si la constante _POSIX_THREAD_PRIORITY_SCHEDULING est définie
dans unistd.h.
En particulier, l’attribut schedpolicy correspond à la politique d’ordonnancement employée par le thread.
Trois valeurs sont possibles : SCHED_OTHER pour l’ordonnancement classique ; SCHED_RR pour un séquence-
ment temps-réel avec l’algorithme Round Robin ; SCHED_FIFO pour un ordonnancement temps-réel FIFO.
Pour plus de détails, cf. chapitre « Interaction système multi-tâche et processus ».
Les attributs stackaddr et stacksize permettent de configurer la pile utilisée par le fil d’exécution. Ils
permettent respectivement de préciser l’adresse de départ et la taille maximale de la pile.

TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 135
Threads ou processus légers 3 Partage des données
' $
3 Partage des données

3.2 Notion de variable partagée . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21


# 19 3.2 Partage non-intentionnel des données . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
3.3 Code réentrant et thread-safe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
3.4 Thread-Local Storage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23

& %

' $
3.1 Notion de variable partagée
 Variable partagée : deux définitions possibles
 Conceptuellement : variable utilisée par plusieurs threads
 Techniquement : pendant l’exécution une seule instance de la variable pour tous
les threads
I Variable globale : une seule instance “partagée”
I Variable statique locale : une seule instance “partagée”
I Variable automatique locale : une instance dans chacune des piles des threads
# 20
appelants
 Attention : les threads partagent l’intégralité de l’espace d’adressage du processus
 Toutes les variables peuvent potentiellement être partagées.
 Même les variables automatiques locales peuvent être partagées : à utiliser avec
précaution.
 Partage du contexte du noyau
 Gestion des flux
 Gestion des signaux
& %

Conceptuellement, une variable partagée est une variable utilisée par plusieurs threads. Techniquement,
on appelle variable partagée une variable pour laquelle durant l’exécution du programme, il n’existe qu’une
seule instance. En C, les variables globales et les variables locales statiques ne sont allouées qu’une seule
fois pour l’ensemble des threads, sur le tas. Donc il n’en existe qu’une seule instance pendant l’exécution
du programme. En revanche, les variables automatiques locales à une fonction sont allouées dans la pile de
chaque thread appelant cette fonction. Donc il existe plusieurs instances de ces variables pendant l’exécution
du programme.
Attention : les threads partagent l’intégralité de l’espace d’adressage du processus ce qui signifie que
TOUTES les variables peuvent être partagées. MAIS généralement, on utilise des variables globales, visibles
dans l’ensemble des fonctions utiles, pour définir des variables partagées.
Voir Briant et O’Hallaron.

TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 136
Threads ou processus légers 3 Partage des données
' $
3.2 Partage non-intentionnel des données

 Lorsqu’une même instance de variable est utilisée par plusieurs threads alors que
# 21 conceptuellement ce n’est pas une variable partagée
 Peut provoquer des situations de compétition (race condition)
I Le résultat varie selon les conditions d’exécution
 Erreurs difficiles à détecter !

& %

/*
* race.c: code faux (poly Briant et Hallaron)
*
* sans temporisation, semble s’executer correctement!
* en ajoutant une temporisation, on voit l’erreur.
*/

#include <pthread.h>
#include <error.h>
#include <stdlib.h>
#include <stdio.h>

#define NTHREADS 100

void *func(void *arg)


{
int me;

me = *((int *)arg);
printf("Hello from %d\n", me);

pthread_exit(NULL);
}

int main (int argc, char **argv)


{
int i;
pthread_t threads[NTHREADS];

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


{
int rc;
/* OOPS !!!! FAUX: i est partagé par tous les threads! */
rc = pthread_create(&threads[i], NULL, func, &i);
if(rc)
error(EXIT_FAILURE, rc, "pthread_create");
}

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


{
int rc;

rc = pthread_join(threads[i], NULL);
if(rc)
error(EXIT_FAILURE, rc, "pthread_join");
}

exit(EXIT_SUCCESS);
}

TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 137
Threads ou processus légers 3 Partage des données

L’exécution du programme race.c affiche le numéro du thread créé en passant en paramètre l’adresse
de l’indice de la boucle. Une seule instance de l’indice de boucle est utilisée par tous les threads donc en
fonction de l’instant où l’affichage s’effectue, le résultat est différent. Le code est donc faux. Il s’agit d’une
race en anglais (situation de compétition en français), c’est-à-dire que le résultat de l’exécution dépend du
fait qu’un thread atteint un point x d’exécution avant qu’un autre thread atteigne un point y (voir le Briant
et O’Hallaron).
Les exécutions semblent correctes mais en ajoutant une temporisation d’une seconde avant stockage de
la valeur, l’erreur est bien visible : exécution de raceTempo.c.
De manière générale, l’utilisation de threads, il est nécessaire d’exécuter plusieurs fois le même code
simultanément. En effet, sur une machine multiprocesseur, il est possible que pendant l’exécution du code
par un processeur, un autre processeur commence l’exécution de ce même code. Il faut donc vérifier que
dans ce cas, les exécutions se déroulent correctement. Sur une machine monoprocesseur, le problème se
pose aussi. En effet, dans la plupart des systèmes d’exploitation, lorsqu’un processus est en cours, il peut
être interrompu pour exécuter un autre processus. De plus, les fonctions récursives sont aussi un exemple
d’exécutions simultanées du même code.

Interactivement pendant le cours : Exemple de partage non-intentionnel des données.


' $
3.3 Code réentrant et thread-safe

 Code réentrant : pas de variables partagées lors d’exécutions simultanées par


plusieurs threads
 Ne pas maintenir d’état persistant entre les appels
I Contre-exemple : strtok, rand
 Ne pas retourner de pointeur sur une variable statique
# 22
I Contre-exemple : ctime
 Code thread-safe : résultats corrects lors d’exécutions simultanées par plusieurs
threads
 Protéger les accès aux données partagées dans les fonctions
I Contre-exemple : variable globale externe errno
I Redéfinition de errno : chaque thread a son propre errno
 Pas d’appel à du code non thread-safe

& %

On trouve dans la littérature deux termes différents : réentrant et thread-safe. Pour certains, ils sont
équivalents. Pour d’autres, bien que ces notions soient liées à la façon de gérer les ressources , ce sont deux
notions différentes. Une fonction thread-safe produit un résultat correct lors d’une exécution multithread.
Une fonction réentrante n’utilise pas de variables partagées lors d’une exécution multithread (voir le Briant
et O’Hallaron). Une fonction peut être soit réentrante, soit thread-safe, soit les deux, soit ni l’un ni l’autre.
Une fonction réentrante ne conserve pas d’état entre deux appels successifs ; elle ne retourne pas non
plus de pointeur sur des données statiques. Toutes les données sont fournies par l’appelant. Une fonction
réentrante ne doit pas appeler non plus de fonction non-réentrante.
Le standard POSIX spécifie que toutes les fonctions nécessaires, notamment celles de la bibliothèque C,
doivent être implantées de façon à pouvoir être exécutées par plusieurs threads simultanément. Certaines
fonctions, néanmoins, échappent à cette spécification parmi lesquelles dirname, getenv, gethostbyname,
gethostbyadddr, rand, readdir, setenv, putenv, strerror, strtok... (Voir le Robbins et Robbins p.432)
Pour l’ensemble de ces fonctions, une version réentrante doit être implantée désignée avec le suffixe _r.
Une fonction non-réentrante peut souvent, mais pas toujours, être identifiée par son interface externe et
son utilisation. Par exemple, en C, la primitive strtok n’est pas réentrante parce qu’elle conserve la chaîne
de caractères pour la diviser en éléments. La primitive ctime n’est pas non plus réentrante ; elle retourne un

TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 138
Threads ou processus légers 3 Partage des données

pointeur sur des données statiques qui est écrasé à chaque appel. Il faut alors utiliser les versions ré-entrantes
de ces primitives, par exemple strtok_r.
Une fonction thread-safe protège les données partagées (variables mais aussi écriture dans des fichiers par
exemple) des accès concurrents par des verrous. La notion de thread-safe ne concerne que l’implantation de
la fonction et non son interface externe.
Une fonction thread-safe non-réentrante est souvent moins performante qu’une fonction réentrante puis-
qu’il a été nécessaire d’ajouter des synchronisations pour la rendre thread-safe.
Dans une application multi-thread, toutes les fonctions appelées par plusieurs threads, doivent être thread-
safe. Il faut remarquer aussi que la plupart du temps, les fonctions non-réentrantes ne sont pas thread-safe
mais que les rendre réentrantes les rend aussi thread-safe.
Les bibliothèques réentrantes et thread-safe sont utiles dans tous les environnements de programmation
parallèles et asynchrones et pas seulement pour des threads.
errno est par défaut une variable globale externe. Cette implantation ne peut pas fonctionner avec des
threads. En effet, le résultat de la lecture de errno n’est pas correct puisqu’un autre thread peut l’avoir
modifiée. Dans le cas de threads, errno est propre à chaque thread. La bibliothèque pthread redéfinit la
fonction _errno_location() (voir bits/errno.h) pour fournir une adresse propre à chaque thread. Par
défaut, pour un processus monothread, _errno_location() renvoie l’adresse de la variable globale errno.
/*
* Exemple de code non-réentrant
* strtok() maintient un état persistant entre les appels
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <error.h>
#include <pthread.h>
#define MAXSIZE 256

void *start_function(void *arg)


{
char *phrase;
char *element;
char *delim = " ";

phrase = malloc(MAXSIZE * sizeof(char));


if (phrase == NULL)
{
perror("malloc");
exit(EXIT_FAILURE);
}

strcpy(phrase, "11 12 13");

element = strtok(phrase, delim);


printf("1) element 0 = %s\n", element);

element = strtok(NULL, delim);


printf("1) element 1 = %s\n", element);

pthread_exit(NULL);
}

int main(int argc, char **argv)


{
int rc;
pthread_t thread;
char *element;
char *delim = " ";
char *phrase;

phrase = malloc(MAXSIZE * sizeof(char));


if (phrase == NULL)
{
perror("malloc");
exit(EXIT_FAILURE);
}

strcpy(phrase, "01 02 03");

element = strtok(phrase, delim);


printf("0) element 0 = %s\n", element);
rc = pthread_create(&thread, NULL, start_function, NULL);
if (rc)
error(EXIT_FAILURE, rc, "pthread_create");

pthread_join(thread, NULL);

TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 139
Threads ou processus légers 3 Partage des données

if (rc)
error(EXIT_FAILURE, rc, "pthread_join");

element = strtok(NULL, delim);


printf("0) element 1 = %s: OUOUOUPS!\n", element);

return EXIT_SUCCESS;
}

Dans les programmes fournis, deux threads, le thread principal et le thread pair, analysent chacun une
chaîne de caractères différente pour la décomposer en élément.
Le programme str.c utilise la fonction strtok(). Cette fonction découpe une chaîne de caractères
en plusieurs éléments, retournés successivement par différents appels à strtok(). Entre chaque appel à
strtok(), un pointeur vers l’élément suivant de la chaîne est maintenu par l’intermédiaire d’une variable
statique à l’intérieur de la fonction strtok().
Le résultat du programme dépend donc de l’ordre des appels à strtok(). Dans notre cas, le premier
appel est effectué par le thread principal. Ensuite le thread pair est créé. Si le thread pair effectue alors un
appel à strtok(), à l’appel suivant, le thread principal renverra un résultat correspondant à la chaîne du
thread pair et non à la sienne !
Pour corriger ce problème et faire en sorte que le résultat ne dépende pas de l’ordonnancement des
exécutions des threads, il faut utiliser la fonction strtok_r(). Cette fonction permet au programme appelant
de fournir lui-même la zone de mémoire permettant de conserver un pointeur vers l’élément suivant (en
troisième argument de la fonction).
/*
* Attention le buffer en entree, phrase, est modifie
* par l’appel a strotk donc il ne sert a rien d’appeler
* plusieurs fois strtok sur le meme buffer!
*
* appel a strtok_r
*
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <error.h>
#include <pthread.h>
#define MAXSIZE 256

void *start_function(void *arg)


{
char *phrase;
char *element;
char *delim = " ";
char *ptr;

phrase = malloc(MAXSIZE * sizeof(char));


if (phrase == NULL)
{
perror("malloc phrase");
exit(EXIT_FAILURE);
}

strcpy(phrase, "11 12 13");

element = strtok_r(phrase, delim, &ptr);


printf("1) element 0 = %s\n", element);

element = strtok_r(NULL, delim, &ptr);


printf("1) element 1 = %s\n", element);

pthread_exit(NULL);
}

int main(int argc, char **argv)


{
int rc;
pthread_t thread;
char *element;
char *delim = " ";
char *phrase;
char *ptr;
phrase = malloc(MAXSIZE * sizeof(char));
if (phrase == NULL)
{
perror("malloc");
exit(EXIT_FAILURE);
}

TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 140
Threads ou processus légers 3 Partage des données

strcpy(phrase, "01 02 03");

element = strtok_r(phrase, delim, &ptr);


printf("0) element 0 = %s\n", element);
rc = pthread_create(&thread, NULL, start_function, NULL);
if (rc)
error(EXIT_FAILURE, rc, "pthread_create");

element = strtok_r(NULL, delim, &ptr);


rc = pthread_join(thread, NULL);
if (rc)
error(EXIT_FAILURE, rc, "pthread_join");

printf("0) element 1 = %s: OK!\n", element);

return EXIT_SUCCESS;
}

Interactivement en cours : Exemple avec un code utilisant strtok().


' $
3.4 Thread-Local Storage

 Variables globales propre à chaque thread


 Chaque thread a une copie distincte de la variable
 exemple : errno
 Création
 int pthread_key_create (pthread_key_t *cle, void (*destr_fonction ) (
# 23 void * )

 Destruction
 int pthread_key_delete ( pthread_key_t cle)
 Utilisation
 int pthread_setspecific (pthread_key_t cle, const void *pointeur)
 void * pthread_getspecific( pthread_key_t cle)
 Ceci peut utilement être utilisé avec pthread_once
& %
Lorsqu’une valeur doit être conservée d’un appel de fonction à l’autre, elle peut être déclarée statique ou
globale. Elle est alors commune à l’ensemble des threads. Il n’est pas non plus possible de la stocker dans la
pile. On utilise alors les « données privées ».
Une clé (de type pthread_key_t) doit être associée à chaque donnée privée et peut résider en variable
statique. La bibliothèque associe la clé avec un pointeur générique différent pour chaque thread.
pthread_key_create initialise la clé dont l’adresse est passée en premier argument ; le second argument
est l’adresse de la fonction qui sera appelée lors de la destruction de la clé.
pthread_key_delete détruit la clé pointée par le paramètre.
pthread_setspecific associe la clé passée en premier argument aux données personnelles composant le
second argument. Le second argument est typiquement un tableau dont chaque élément sera utilisé par un
thread différent. Cette opération ne doit être réalisée qu’une seule fois, quel que soit le nombre de threads.
pthread_getspecific retourne l’adresse de la donnée personnelle associée au thread pour la clé passée en
paramètre.
/*
specific.c

*/

#include <error.h>
#include <unistd.h>

TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 141
Threads ou processus légers

#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>

static pthread_key_t cleUnique;

void set_buffer(pthread_key_t cle, int init_value)


{
int *val;
val = (int *) pthread_getspecific(cle);
*val = init_value;
}

int *get_buffer(pthread_key_t cle)


{
int *val;
val = (int *) pthread_getspecific(cle);

return val;
}

void *start_routine(void *arg)


{
int *maval;

/* doit etre appelee par chaque thread, le buffer en arg doit etre
different */
pthread_setspecific(cleUnique, malloc(sizeof(int)));

set_buffer(cleUnique, 1);

maval = get_buffer(cleUnique);
printf("thread1: maval = %d\n", *maval);
pthread_exit(EXIT_SUCCESS);
}

int main (int argc, char *argv[]) {


int rc;
int *maval;
pthread_t thread1;

/* ne doit etre fait qu’une seule fois, pas de fonction de


destruction */
pthread_key_create(&cleUnique, NULL);

/* doit etre appelee par chaque thread, le buffer en arg doit etre
different */
pthread_setspecific(cleUnique, malloc(sizeof(int)));

set_buffer(cleUnique, 2);
maval = get_buffer(cleUnique);

printf("thread maitre: maval = %d\n", *maval);

rc = pthread_create(&thread1, NULL, start_routine, NULL);


if (rc)
error(EXIT_FAILURE, rc, "pthread_create");

rc = pthread_join(thread1, NULL);
if (rc)
error(EXIT_FAILURE, rc, "pthread_join");

maval = get_buffer(cleUnique);
printf("thread maitre: maval = %d\n", *maval);
pthread_key_delete(cleUnique);

pthread_exit(EXIT_SUCCESS);
}

Afin de simplifier l’utilisation de variables TLS, certains compilateurs (GCC, Intel CC, etc.) permettent
l’utilisation du mot-clé __thread lors de la déclaration d’une variable. Par exemple :

__thread int variable_name = 0 ;

Les variables TLS sont stockées dans la section .tdata de l’espace d’adressage. Lorsqu’un nouveau
thread est créé, la section .tdata est recopiée, permettant ainsi d’allouer toutes les variables TLS du
thread. De plus amples explications sur l’implémentation de variables TLS sous Linux sont disponibles dans :

TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 142
Threads ou processus légers 4 Synchronisation

Drepper, U. Elf handling for thread-local storage. Technical report, Red Hat, Inc., 2003.
' $
4 Synchronisation

4.2 Synchronisation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
# 24 4.2 Exclusions mutuelles. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .27
4.3 Sémaphores POSIX (rappel) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
4.4 Attente de conditions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31

& %

' $
4.1 Synchronisation

 Garantir la consistance des données


 Accès simultanés à une donnée partagée en lecture/écriture
I Séquencement des instructions load, update, store
# 25  Exemple : exclusion mutuelle de type compte bancaire
 Implantation de P et V avec des threads : plusieurs outils de la bibliothèque Pthread
 Sémaphores
 Mutex
 Conditions

& %

Les threads doivent synchroniser leurs activités pour interagir. Ceci inclut les synchronisations implicites
par la modification de données partagées et les synchronisations explicites informant les autres des événements
qui se sont produits.
Le programme compteurBOOM.c est un exemple de mauvaise utilisation d’une donnée partagée, la variable
compteur. Deux threads incrémentent simultanément cette variable un nombre de fois identiques et n’arrivent
pas au même résultat.
Il est donc nécessaire d’introduire les fonctions P et V pour synchroniser les différents threads (voir le
chapitre Synchronisation entre processus).

TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 143
Threads ou processus légers 4 Synchronisation

Simultanément en cours : Exécution de compteurBOOM.c sur une machine monoprocesseur et bipro-


cesseur. Commenter les résultats.
' $
4.2 Exclusions mutuelles

 Type : pthread_mutex_t
 Création
# 26  int pthread_mutex_init (pthread_mutex_t *mutex, const
pthread_mutexattr_t *mutexattr)
 pthread_mutex_init retourne toujours 0.
 Destruction
 int pthread_mutex_destroy (pthread_mutex_t *mutex)

& %

Les variables globales et les descripteurs de fichiers (et d’autres choses encore) étant partagés par l’en-
semble des threads d’un processus, un mécanisme dit d’« exclusion mutuelle » est fourni afin d’assurer que cer-
taines opérations seront synchronisées. Pour cela, on utilise un « mutex » dont le type est pthread_mutex_t.
L’initialisation d’un mutex peut s’effectuer soit de manière statique (en assignant la va-
leur PTHREAD_MUTEX_INITIALIZER au mutex), soit de manière dynamique (à l’aide de la fonction
pthread_mutex_init). Dans le cas dynamique, le premier paramètre est un pointeur sur le mutex et le
second est un pointeur sur les attributs que l’on veut associer au mutex (une valeur nulle associe les attri-
buts par défaut).
pthread_mutex_destroy permet de détruire un mutex (non verrouillé) en précisant l’adresse du mutex.
' $
Exclusions mutuelles (2/2)

 Utilisation
 int pthread_mutex_lock (pthread_mutex_t *mutex)
 int pthread_mutex_unlock (pthread_mutex_t *mutex)
# 27
 int pthread_mutex_trylock (pthread_mutex_t *mutex)
 Attributs
 Les attributs associés aux MUTEX ne sont pas portables
 Il convient de ne pas trop les utiliser
 Ou de se référer à la documentation en ligne

& %

TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 144
Threads ou processus légers 4 Synchronisation

pthread_mutex_lock permet de prendre le mutex passé en paramètre. Si le mutex est disponible, il est
pris de suite ; sinon, le thread se bloque jusqu’à obtenir le mutex.
pthread_mutex_unlock libère le mutex. Le thread libérant le mutex doit en être le possesseur.
pthread_mutex_trylock permet de savoir s’il est possible de prendre le mutex. Ceci ne garantit en rien
que le mutex sera toujours libre lors du prochain appel à pthread_mutex_lock.
Il est à noter qu’à ce jour la norme POSIX ne prévoit aucun attribut pour les mutex (des attri-
buts existent dans certaines implantations, en particulier les mutex récursifs sous Linux, voir les fonctions
pthread_mutexattr_init, pthread_mutexattr_destroy...).
/*
* compteurMutex.c
*
* Acces au compteur protege par mutex
* mutex_init ne renvoie pas de code d’erreur
*
*/

#include <unistd.h>
#include <error.h>
#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>

/* INT_MAX / 2 */
#define NBITER 1000000000

int compteur = 0;

pthread_mutex_t mutex;

void *start_routine(void *arg)


{
int i, rc;

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


{
rc = pthread_mutex_lock(&mutex);
if(rc)
error(EXIT_FAILURE, rc, "pthread_mutex_lock");

compteur ++;

rc = pthread_mutex_unlock(&mutex);
if(rc)
error(EXIT_FAILURE, rc, "pthread_mutex_unlock");
}
pthread_exit(NULL);
}

int main (int argc, char *argv[]) {


int rc;
pthread_t thread1, thread2;

pthread_mutex_init(&mutex, NULL);

rc = pthread_create(&thread1, NULL, start_routine, NULL);


if(rc)
error(EXIT_FAILURE, rc, "pthread_create");

rc = pthread_create(&thread2, NULL, start_routine, NULL);


if(rc)
error(EXIT_FAILURE, rc, "pthread_create");

rc = pthread_join(thread1, NULL);
if(rc)
error(EXIT_FAILURE, rc, "pthread_join");

rc = pthread_join(thread2, NULL);
if(rc)
error(EXIT_FAILURE, rc, "pthread_join");

if (compteur != 2 * NBITER)
printf("BOOM! compteur = %d\n", compteur);
else
printf("OK compteur = %d\n", compteur);

rc = pthread_mutex_destroy(&mutex);
if (rc)
error(EXIT_FAILURE, rc, "pthread_mutex_destroy");

exit(EXIT_SUCCESS);

TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 145
Threads ou processus légers 4 Synchronisation

' $
4.3 Sémaphores POSIX (rappel)

 Création et destruction
 int sem_init ( sem_t *sem, int pshared, u_int value)
 int sem_destroy ( sem_t *sem)

# 28  Utilisation
 int sem_wait ( sem_t *sem)
 int sem_post ( sem_t *sem)
 int sem_trywait ( sem_t *sem)
 int sem_getvalue ( sem_t *sem, int *sval)
 Toutes ces fonctions renvoient -1 en cas de problème et positionnent errno.

& %
Remarque : les sémaphores POSIX sont également abordée dans le chapitre « Communications inter-
processus ».
Les sémaphores POSIX font partie de la bibliothèque des threads Linux si la constante
_POSIX_SEMAPHORES est définie dans unistd.h ; les fonctionnalités sont alors définies dans semaphore.h.
Ils sont sensiblement différents des sémaphores IPC System V. Le type associé aux sémaphores POSIX est
sem_t.
sem_init permet d’initialiser le sémaphore dont l’adresse constitue le premier paramètre et la valeur le
dernier. Le deuxième paramètre indique si le sémaphore peut être partagé par plusieurs processus .
sem_destroy détruit le sémaphore passé en argument.
sem_wait attend que le sémaphore passé en argument soit libre pour le prendre.
sem_post libère le sémaphore passé en argument.
sem_trywait renvoie -1 (et place la valeur EAGAIN dans errno) si le sémaphore est déjà pris ; elle renvoie
0 dans le cas contraire.
sem_getvalue place la valeur courante du sémaphore à l’adresse passé en second argument.
/**
** semaphore.c
**
** synchronisation de deux threads: utilisation d’un semaphore
**
** exemple d’un schéma producteur/consommateur
**
** Le thread principal écrit un caractère dans une zone de mémoire commune.
** Un deuxième thread le lit.
**
** sem_init, sem_wait, sem_post et sem_destroy renvoient -1 en cas de
** probleme et positionnent errno.
**
**/

#include <pthread.h>
#include <semaphore.h>
#include <error.h>
#include <stdlib.h>
#include <stdio.h>

char buffer;
sem_t placeDispo;
sem_t infoPrete;

void production()
{

TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 146
Threads ou processus légers 4 Synchronisation

buffer = ’a’;

printf("producteur: <0x%x> item = %c \n", (unsigned int) pthread_self(), buffer);


}

void consommation()
{
printf("consommateur: <0x%x> item = %c \n", (unsigned int) pthread_self(), buffer);
}

void *ecrire(void *null)


{
int rc;

/* P(placeDispo) */
rc = sem_wait(&placeDispo);
if (rc)
{
perror("sem_wait");
exit(EXIT_FAILURE);
}

production();

/* V(infoPrete) */
rc = sem_post(&infoPrete);
if (rc)
{
perror("sem_post");
exit(EXIT_FAILURE);
}

return NULL;
}

void *lire(void *null)


{
int rc;

/* P(infoPrete) */
rc = sem_wait(&infoPrete);
if (rc)
{
perror("sem_wait");
exit(EXIT_FAILURE);
}

consommation( buffer );

/* V(placeDispo) */
rc = sem_post(&placeDispo);
if (rc)
{
perror("sem_post");
exit(EXIT_FAILURE);
}

pthread_exit(NULL);
}

int main()
{
int rc;
pthread_t lecteur;

sem_init(&placeDispo, 0, 1);
sem_init(&infoPrete, 0, 0);

rc = pthread_create( &lecteur, NULL, &lire, NULL);


if(rc)
error(EXIT_FAILURE, rc, "pthread_create");

ecrire(NULL);

rc = pthread_join(lecteur, NULL);
if(rc)
error(EXIT_FAILURE, rc, "pthread_join");

rc = sem_destroy(&placeDispo);
if (rc)
{
perror("sem_destroy");
exit(EXIT_FAILURE);
}
rc = sem_destroy(&infoPrete);

TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 147
Threads ou processus légers 4 Synchronisation

if (rc)
{
perror("sem_destroy");
exit(EXIT_FAILURE);
}

return EXIT_SUCCESS;
}

' $
4.4 Attente de conditions
 Principe
 Un premier thread se met en attente d’un condition
 Lorsqu’un second thread réalise la condition, il émet un signal à destination de la
condition, réveillant un thread en attente
I Si aucun thread n’est en attente, rien ne se passe
I Si plusieurs threads sont en attente, un thread est réveillé
# 29  Type : pthread_cond_t
 Création
 int pthread_cond_init (pthread_cond_t *cond, const pthread_condattr_t
*cond_attr)
 pthread_cond_init retourne toujours 0.
 Destruction
 int pthread_cond_destroy ( pthread_cond_t *cond)
 Notes : il n’existe pas d’attribut pour les conditions
& %

Lorsqu’un fil d’exécution doit patienter jusqu’à ce qu’un événement survienne dans un autre fil d’exé-
cution, on peut utiliser une autre technique de synchronisation : les « conditions » dont le type est
pthread_cond_t. Le principe est le suivant : un premier thread se met en attente d’une condition devant
être réalisée ; lorsqu’un second thread réalise la condition, il émet un signal à destination de la condition qui
réveille le thread en attente. Si aucun thread n’est en attente, rien ne se passe ; si plus d’un thread est en
attente, l’un d’eux est réveillé, mais on ne peut déterminer lequel.

Une condition peut être initialisée soit statiquement en assignant la valeur définie par la macro
PTHREAD_COND_INITIALIZER, soit dynamiquement en réalisant un appel à pthread_cond_init. Cette fonc-
tion nécessite deux paramètres : le premier est un pointeur sur la condition à initialiser ; le second est
un pointeur les attributs associés à la condition (ou la valeur nulle si on désire les attributs par défaut).
pthread_cond_destroy détruit la condition sur laquelle pointe le paramètre. Il est à noter que la biblio-
thèque de threads Linux n’implémente aucun attribut pour les conditions.

TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 148
Threads ou processus légers 4 Synchronisation
' $
Attente de conditions (2/2)

 Utilisation
 int pthread_cond_signal ( pthread_cond_t * cond)
 int pthread_cond_wait (pthread_cond_t *cond, pthread_mutex_t *mutex)
 int pthread_cond_timedwait (pthread_cond_t *cond, pthread_mutex_t
# 30
*mutex, const struct timespec *abstime)
 int pthread_cond_broadcast ( pthread_cond_t * cond)
 Les fonctions pthread_cond_signal, pthread_cond_wait et
pthread_cond_broadcast renvoient toujours 0.
 Notes
 Toujours prendre le mutex associé à la condition avant et le relâcher ensuite.
I Pas d’interblocage entre le thread se plaçant en attente de la condition et
celui la réalisant.
& %
' $
I pthread_cond_wait libére le mutex puis se met en attente de la condition.
Une fois que la condition est réalisée, la fonction bloque à nouveau le mutex
avant de sortir.
 pthread_cond_wait peut se terminer même si la condition n’a pas été réalisée.

# 31

& %

pthread_cond_signal réalise la condition passé en paramètre.


pthread_cond_wait place le fil d’exécution courant en attente de réalisation de la condition passée en
premier paramètre et associée au mutex passé en second paramètre (une condition est toujours associée à un
mutex pour éviter les problèmes de concurrence d’accès sur la variable). Le mutex doit être verrouillé par le
thread appelant. pthread_cond_wait déverrouille alors le mutex et attend que la condition soit signalée. Un
appel à pthread_cond_wait peut se terminer même si la condition n’a pas été réalisée. Il est donc important
de vérifier que c’est le cas avant de continuer.
pthread_cond_timedwait réalise la même opération. Le dernier paramètre est la date (et l’heure) à
partir de laquelle le thread sera réveillé, même si la condition n’a pas été réalisée.
pthread_cond_broadcast réalise la condition et réveille tous les fils d’exécution en attente de sa réali-
sation.
Pour utiliser correctement ces fonctions, il faut toujours prendre le mutex associé à la condition avant et

TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 149
Threads ou processus légers 4 Synchronisation

le relâcher ensuite. Il n’y a pas d’interblocage entre le fil d’exécution se plaçant en attente de la condition
et celui la réalisant. En effet, la fonction pthread_cond_wait commence par libérer le mutex puis se met en
attente de la condition. Une fois que la condition est réalisée, la fonction bloque à nouveau le mutex avant
de sortir.
/**
** conditionUn.c
**
** synchronisation de deux threads: utilisation d’une variable condition
**
** exemple d’un schéma producteur/consommateur
**
** Le thread principal écrit un caractère dans une zone de mémoire commune.
** Un deuxième thread le lit.
**
** mutex_init, cond_init, cond_signal, cond_broadcast et cond_wait ne
** renvoient pas de code d’erreur
**
**/

#include <pthread.h>
#include <error.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

char buffer = ’a’;


pthread_mutex_t mutex;

pthread_cond_t condPlaceDispo;
pthread_cond_t condInfoPrete;
int infoPrete = 0;

void production()
{
printf("producteur: <0x%x> buffer = %c \n", (unsigned int) pthread_self(), buffer);
}

void comsommation()
{
printf("consommateur: <0x%x> buffer = %c \n", (unsigned int) pthread_self(), buffer);
}

void *ecrire(void *null)


{
int rc;

/* P(placeDispo) */

rc = pthread_mutex_lock(&mutex);
if(rc)
error(EXIT_FAILURE, rc, "pthread_mutex_lock");

/* while pallie les cas où le thread se reveille alors que le


predicat n’est pas rempli */
while (infoPrete)
pthread_cond_wait(&condPlaceDispo, &mutex);

rc = pthread_mutex_unlock(&mutex);
if(rc)
error(EXIT_FAILURE, rc, "pthread_mutex_unlock");

production();

/* V(infoPrete) */
rc = pthread_mutex_lock(&mutex);
if(rc)
error(EXIT_FAILURE, rc, "pthread_mutex_lock");

infoPrete = 1;
pthread_cond_signal(&condInfoPrete);
rc = pthread_mutex_unlock(&mutex);
if(rc)
error(EXIT_FAILURE, rc, "pthread_mutex_unlock");

return NULL;
}

void *lire(void * null)


{
int rc;

/* P(infoPrete) */

TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 150
Threads ou processus légers 4 Synchronisation

rc = pthread_mutex_lock(&mutex);
if(rc)
error(EXIT_FAILURE, rc, "pthread_mutex_lock");

/* while pallie les cas où le thread se reveille alors que le


predicat n’est pas rempli */
while (!infoPrete)
pthread_cond_wait(&condInfoPrete, &mutex);
rc = pthread_mutex_unlock(&mutex);
if(rc)
error(EXIT_FAILURE, rc, "pthread_mutex_unlock");

comsommation( buffer );

/* V(placeDispo) */
rc = pthread_mutex_lock( &mutex);
if(rc)
error(EXIT_FAILURE, rc, "pthread_mutex_lock");

infoPrete = 0;
pthread_cond_signal(&condPlaceDispo);

rc = pthread_mutex_unlock(&mutex);
if(rc)
error(EXIT_FAILURE, rc, "pthread_mutex_unlock");

pthread_exit(NULL);
}

int main()
{
int rc;
pthread_t lecteur;

pthread_mutex_init(&mutex, NULL);

pthread_cond_init(&condPlaceDispo, NULL);
pthread_cond_init(&condInfoPrete, NULL);

rc = pthread_create( &lecteur, NULL, &lire, NULL);


if(rc)
error(EXIT_FAILURE, rc, "pthread_create");

ecrire(NULL);

rc = pthread_join(lecteur, NULL);
if(rc)
error(EXIT_FAILURE, rc, "pthread_join");

rc = pthread_mutex_destroy(&mutex);
if(rc)
error(EXIT_FAILURE, rc, "pthread_mutex_destroy");
rc = pthread_cond_destroy(&condPlaceDispo);
if(rc)
error(EXIT_FAILURE, rc, "pthread_cond_destroy");
rc = pthread_cond_destroy(&condInfoPrete);
if(rc)
error(EXIT_FAILURE, rc, "pthread_cond_destroy");

return EXIT_SUCCESS;
}

/**
** conditionMult.c
**
** synchronisation de deux threads: utilisation d’une variable condition
**
** exemple d’un schéma producteur/consommateur
**
** Le thread principal écrit caractère a caractere dans une zone de mémoire commune.
** Un deuxième thread lit caractere a caractere.
**
** mutex_init, cond_init, cond_signal, cond_broadcast et cond_wait ne
** renvoient pas de code d’erreur
**
**/

#include <pthread.h>
#include <error.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 151
Threads ou processus légers 4 Synchronisation

#define NBCHARS 26
char buffer = ’a’ - 1;
pthread_mutex_t mutex;

pthread_cond_t condPlaceDispo;
pthread_cond_t condInfoPrete;
int infoPrete = 0;

void production()
{
buffer++;

printf("producteur: <0x%x> buffer = %c \n", (unsigned int) pthread_self(), buffer);


}

void comsommation()
{
printf("consommateur: <0x%x> buffer = %c \n", (unsigned int) pthread_self(), buffer);
}

void *ecrire(void *null)


{

int i;
for (i = 0; i < NBCHARS; i++)
{
int rc;
/* P(placeDispo) */

rc = pthread_mutex_lock(&mutex);
if(rc)
error(EXIT_FAILURE, rc, "pthread_mutex_lock");

/* while pallie les cas où le thread se reveille alors que le


predicat n’est pas rempli */
while (infoPrete)
rc = pthread_cond_wait(&condPlaceDispo, &mutex);

rc = pthread_mutex_unlock(&mutex);
if(rc)
error(EXIT_FAILURE, rc, "pthread_mutex_unlock");

production();

/* V(infoPrete) */
rc = pthread_mutex_lock(&mutex);
if(rc)
error(EXIT_FAILURE, rc, "pthread_mutex_lock");

infoPrete = 1;
pthread_cond_signal(&condInfoPrete);

rc = pthread_mutex_unlock(&mutex);
if(rc)
error(EXIT_FAILURE, rc, "pthread_mutex_unlock");

return NULL;
}

void *lire(void * null)


{
int i;
for (i = 0; i < NBCHARS; i++)
{
int rc;

rc = pthread_mutex_lock( &mutex);
if(rc)
error(EXIT_FAILURE, rc, "pthread_mutex_lock");

/* P(infoPrete) */

/* while pallie les cas où le thread se reveille alors que le


predicat n’est pas rempli */
while (!infoPrete)
rc = pthread_cond_wait(&condInfoPrete, &mutex);

comsommation( buffer );

infoPrete = 0;

/* V(placeDispo) */
pthread_cond_signal(&condPlaceDispo);

TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 152
Threads ou processus légers 5 Utilisation et limitations des threads

rc = pthread_mutex_unlock(&mutex);
if(rc)
error(EXIT_FAILURE, rc, "pthread_mutex_unlock");

pthread_exit(NULL);
}

int main()
{
int rc;
pthread_t lecteur;

pthread_mutex_init(&mutex, NULL);

pthread_cond_init(&condPlaceDispo, NULL);
pthread_cond_init(&condInfoPrete, NULL);

rc = pthread_create( &lecteur, NULL, &lire, NULL);


if(rc)
error(EXIT_FAILURE, rc, "pthread_create");

ecrire(NULL);

rc = pthread_join(lecteur, NULL);
if(rc)
error(EXIT_FAILURE, rc, "pthread_join");

rc = pthread_mutex_destroy(&mutex);
if(rc)
error(EXIT_FAILURE, rc, "pthread_mutex_destroy");
rc = pthread_cond_destroy(&condPlaceDispo);
if(rc)
error(EXIT_FAILURE, rc, "pthread_cond_destroy");
rc = pthread_cond_destroy(&condInfoPrete);
if(rc)
error(EXIT_FAILURE, rc, "pthread_cond_destroy");

return EXIT_SUCCESS;
}

' $
5 Utilisation et limitations des threads

# 32 5.1 Utilisation des threads . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34


5.2 Limitations des threads . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35

& %

TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 153
Threads ou processus légers 5 Utilisation et limitations des threads
' $
5.1 Utilisation des threads

 Pourquoi utiliser les threads ?


 Améliorer la réactivité des applications
 Partager des ressources
 Économiser du temps et de la place mémoire
 Exploiter des architectures multi-processeurs

# 33  Exemples d’architectures logicielles


 Maître/esclave
 Diviser pour régner
 Producteur/consommateur
 Exemples d’applications
 Traitement de texte
 Navigateur web
 Serveur web
& %
' $
 Pour mon application ?
 Identifier des opérations coûteuses
 Déterminer la taille des données à partager (si possible petite)
 Identifier les (nombreux ?) problèmes de verrous
 Répartir les tâches indépendantes

# 34

& %

Programmer une application interactive avec des threads peut permettre à un programme de continuer à
s’exécuter même si une partie de l’application est bloquée ou est en train d’effectuer une opération coûteuse.
Cela permet donc d’accroître l’interactivité de l’application.
Les architectures maître/esclave, diviser pour régner et producteurs/consommateurs sont des exemples
d’architectures pouvant être facilement implantées avec des threads. Ces architectures mènent toutes à des
programmes modulaires efficacement implantés par des threads.
Dans le cas d’une architecture maître/esclave, une entité maître reçoit la ou les requêtes et crée les
entités esclaves pour les exécuter. Le maître contrôle, par exemple, le nombre d’esclaves existants et ce que
fait chaque esclave. Un esclave s’exécute indépendamment des autres esclaves.
Un gestionnaire d’impressions est un exemple d’architecture maître/esclaves. Un gestionnaire d’impres-
sions gère plusieurs imprimantes, s’assure que toutes les requêtes d’impression reçues sont effectuées en un
temps raisonnable. Quand le gestionnaire reçoit une requête, l’entité maître choisit une imprimante et de-

TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 154
Threads ou processus légers 5 Utilisation et limitations des threads

mande à une entité esclave d’effectuer l’impression sur une imprimante donnée. Chaque esclave peut effectuer
une impression à un instant donné sur une imprimante. Le gestionnaire est chargé de traiter les suppressions
des requêtes d’impressions.
Dans le modèle diviser pour régner, les entités effectuent les tâches en parallèle, indépendamment les
unes des autres. Il n’y a pas d’entité maître.
Un exemple de modèle diviser pour régner serait d’exécuter une commande grep parallèle. La commande
grep établit tout d’abord un ensemble de fichiers à examiner. Elle crée ensuite un ensemble d’entités. Chaque
entité traite un fichier et effectue la recherche du schéma envoyant le résultat sur une sortie commune. Quand
une entité termine sa recherche dans le fichier, elle traite un autre fichier ou s’arrête.
Le modèle producteur/consommateur représente typiquement une chaîne de production (modèle pipe-
liné).
Voilà quelques exemples d’application pouvant tirer profit d’une implantation multi-thread. Un navigateur
web peut avoir un thread affichant les images et le texte pendant qu’un autre thread récupère les données
sur le réseau. Un traitement de texte peut avoir un thread gérant l’interface graphique, un deuxième thread
gérant les entrées de l’utilisateur par l’intermédiaire du clavier et enfin un troisième thread pour faire les
vérifications grammaticales et orthographiques.
Certaines applications ont besoin d’effectuer plusieurs fois les mêmes tâches. Typiquement, un serveur
web accepte des requêtes clientes. Ces requêtes demandent des pages web, des images, des sons, ... Un
serveur peut devoir servir beaucoup de requêtes clientes simultanément. Si ce serveur web est un processus
mono-thread, il ne pourra servir qu’une requête à la fois. Une solution est d’implanter le serveur comme un
processus multi-thread. Un thread est chargé de recevoir les requêtes. Lorsque ce thread reçoit une requête,
il crée un autre thread pour traiter cette requête. Une amélioration de cette solution consiste à utiliser un
ensemble (pool) de threads créés au préalable.
Si vous êtes un programmeur et que vous voulez tirer profit d’une implantation multi-thread, il faut
identifier les parties du programme qui devraient et celles qui ne devraient pas être multi-thread. Voilà un
certain nombre de bonnes questions :

• Y-a-t-il des opérations coûteuses ne dépendant pas du CPU (dessin d’une fenêtre, impression d’un
document, réponse à un clic de souris, calcul d’une colonne de feuille de calcul, gestion de signaux, ...) ?

• Y-aura-t-il beaucoup de données à partager ?

• Y-a-t-il beaucoup de problèmes de verrous ? exclusion mutuelle de données, interblocages (deadlocks)


( 2 threads ont verrouillés des données qu’un troisième essaye d’obtenir) et race conditions (situation
de compétition en français) ?

Situations d’interblocages : avec s= 1 et t = 1 :


Thread 1 Thread 2
P(s) P(t)
P(t) P(s)
V(s) V(t)
V(t) V(s)
ou avec s = 1 et t = 0 :
Thread 1 Thread 2
P(s) P(s)
V(s) V(s)
P(t) P(t)
V(t) V(t)

• Les tâches peuvent-elles être réparties en plusieurs ? par exemple un thread pour la gestion des signaux,
un autre pour l’interface graphique, ...

TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 155
Threads ou processus légers
' $
5.2 Limitations des threads
 Ressources que l’on ne souhaite pas partager
 ID utilisateur, groupe
 droits
 quotas d’utilisation de ressources : nombre maximal de fichiers ouverts par un
processus, ...
 Corruption des ressources partagées en mémoire
 Mort d’un thread → mort de l’application entière
# 35
 Problématiques
 Exécution d’un fork
I Duplication de tous les threads ou nouveau processus mono-thread
 Gestion de signaux : à qui envoyer le signal ?
I Au thread auquel il s’applique ?
I À tous les threads du processus ?
I À certains threads ?
I À un thread spécifique qui se chargera de le gérer correctement ?
 Éviter de communiquer par signaux dans une application multi-thread !
& %

Programmer une application avec des threads est utile pour implanter des applications utilisant plusieurs
entités indépendantes. Néanmoins, dans certains cas, il est préférable d’utiliser plusieurs processus.

Beaucoup de ressources sont gérées par les systèmes d’exploitation au niveau du processus. Par exemple,
les identifiants d’utilisateurs et de groupe et les permissions qui leur sont associées sont gérés au niveau du
processus. Les programmes qui ont besoin d’affecter un utilisateur différent aux différentes entités de leur
programme utiliseront plusieurs processus plutôt qu’un seul processus possédant plusieurs threads. D’autre
part, les attributs du système de fichiers tels que le répertoire de travail courant ou le nombre maximal
de fichiers ouverts sont aussi partagés par tous les threads appartenant à un même processus. Ainsi, une
application ayant besoin de gérer ces attributs de manière indépendante utilisera plusieurs processus. Dans
un programme multi-thread, les sémantiques de fork() et de exec() peuvent être modifiées. Si un thread
appelle fork() à l’intérieur d’un programme, ce nouveau processus doit-il dupliquer tous les threads ? Ce
nouveau processus doit-il être mono-thread ?

Dans Linux, lorsqu’un thread appelle un fork(), le processus entier est dupliqué y compris les zones de
mémoire partagées avec les autres threads. Par contre, il n’y a dans le processus fils qu’un seul fil d’exécution,
celui du thread ayant invoqué fork(). Donc il faut faire attention aux ressources utilisées par les autres threads
sauf s’il y a appel à exec(). Les ressources allouées dynamiquement existeront dans le nouveau processus
mais ne pourront pas être libérées et les ressources verrouillées ne pourront pas être déverrouillées (voir le
Blaess p. 291)

Le traitement d’un signal dépend du type de signal. Un signal synchrone (accès illégal à la mémoire,
division par zéro) doit être envoyé au thread concerné et non aux autres. Pour les signaux asynchrones, la
situation n’est pas claire. Certains signaux asynchrones comme Control-C doivent être envoyés à tous les
threads du processus. Certaines implantations d’UNIX permettent à un thread de spécifier quel signal il
recevra et quel signal il bloquera. Cependant, les signaux ne doivent être gérés qu’une seule et unique fois.
C’est pourquoi, le signal n’est souvent envoyé qu’au premier thread qui ne bloque pas le signal. Solaris 2
implante la quatrième solution, un thread spécifique gère tous les signaux.

TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 156
Threads ou processus légers 6 Autres fonctions de la bibliothèque POSIX threads
' $
6 Autres fonctions de la bibliothèque POSIX threads

# 36 6.1 Annulation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
6.2 Nettoyage des ressources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
6.3 Initialisation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40

& %

' $
6.1 Annulation

 Envoi d’une requête d’annulation


 int pthread_cancel (pthread_t thread)
 Définition du comportement
# 37
 int pthread_setcancelstate (int state, int * oldstate)
→ state = PTHREAD_CANCEL_[ENABLE|DISABLE]
 Définition du type d’annulation (si PTHREAD_CANCEL_ENABLE)
 int pthread_setcanceltype (int type, int * oldtype)
→ type = PTHREAD_CANCEL_[DEFERRED|ASYNCHRONOUS]

& %

Un thread peut vouloir annuler un autre thread. Il envoie alors une demande d’annulation par l’inter-
médiaire de la fonction pthread_cancel. Le thread annulé se termine comme s’il avait lui-même invoqué la
fonction pthread_exit. La valeur 0 est retournée en cas de succès ; dans le cas contraire, un code d’erreur
est renvoyé.
Le thread récepteur peut accepter la requête, la refuser ou la repousser jusqu’à atteindre un « point
d’annulation ». Ceci est particulièrement intéressant si le code exécuté par le thread est sensible (comme la
manipulation des sémaphores par exemple).
pthread_setcancelstate permet de préciser si la prochaine requête d’annulation sera prise en compte
(PTHREAD_CANCEL_ENABLE) ou non (PTHREAD_CANCEL_DISABLE). Le second paramètre permet de récupérer
l’état précédent.
pthread_setcanceltype précise si les requêtes d’annulation sont prises en compte, le comportement du

TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 157
Threads ou processus légers 6 Autres fonctions de la bibliothèque POSIX threads

fil d’exécution : PTHREAD_CANCEL_DEFERRED diffère la terminaison du thread au prochain point d’annulation ;


PTHREAD_CANCEL_ASYNCHRONOUS termine le thread dès réception de la requête. Le second paramètre permet
de récupérer l’état précédent.
L’opportunité de différer l’acceptation des demandes de terminaison se justifie dans la mesure où elles ne
sont pas mémorisées. Ainsi, si seuls l’acception asynchrone et le refus des demandes de terminaison étaient
autorisées, les demandes arrivant pendant les périodes de refus seraient systématiquement perdues.

/*
cancel.c

*/

#include <assert.h>
#include <error.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>

#define GRANDNOMBRE 12000000

void print_compteur(void *arg)


{
int *compteur = (int *)arg;
printf("J’ai recu une annulation et compteur = %d\n", *compteur);
}

void *start_routine(void *arg)


{
int i;
int old_value;
int compteur = 0;

/* pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &old_value); */

/* par defaut DEFERRED */


pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, &old_value);
/* appel print_compteur quand annulation */
pthread_cleanup_push(print_compteur, &compteur);

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


compteur ++;

/* Point d’annulation explicite, utile si DEFERRED */


pthread_testcancel();

printf("Je m’endors avec compteur = %d\n", compteur);


sleep(2);
printf("Je me reveille\n");

/* parametre a 0 parce que non active (annulation avant)


mais appel necessaire pour
compilation */
pthread_cleanup_pop(0);
pthread_exit(EXIT_SUCCESS);
}

int main (int argc, char *argv[]) {


int rc;
pthread_t thread1;
int *rc_join;
rc = pthread_create(&thread1, NULL, start_routine, NULL);
if (rc)
error(EXIT_FAILURE, rc, "pthread_create");

rc = pthread_cancel(thread1);
if (rc)
error(EXIT_FAILURE, rc, "pthread_cancel");

rc = pthread_join(thread1, (void **)&rc_join);


if (rc)
error(EXIT_FAILURE, rc, "pthread_join");

assert(rc_join == PTHREAD_CANCELED || rc_join == EXIT_SUCCESS);

rc = pthread_cancel(thread1);
if (rc)
error(EXIT_FAILURE, rc, "pthread_cancel");

TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 158
Threads ou processus légers 6 Autres fonctions de la bibliothèque POSIX threads

pthread_exit(EXIT_SUCCESS);
}

' $
Annulation (2/2)

 Test d’annulation explicite

# 38  void pthread_testcancel (void)


 Test d’annulation implicite
 pthread_cond_wait, pthread_cond_timedwait
 pthread_join, sem_wait, sigwait

& %
Il existe deux types de point d’annulation. Les points d’annulation explicites sont précisés par le program-
meur avec la fonction pthread_testcancel. Les points d’annulation implicites correspondent en général à
des fonctions pouvant attendre un événement indéfiniment. La liste des points d’annulation dépend des sys-
tèmes (et surtout de leur implantation !). Se reporter à la documentation (GNU en particulier) pour plus
d’information.
' $
6.2 Nettoyage des ressources

 Deux routines
 void pthread_cleanup_push (void (* routine) (void *), void *arg)
# 39  void pthread_cleanup_pop (int execute)
I execute à 0 : fonction supprimée de la pile mais non exécutée
I execute à 1 : fonction supprimée de la pile ET exécutée

 Attention
 Les deux appels doivent appartenir au même bloc d’instructions

& %
Un point d’annulation pouvant intervenir à n’importe quel moment et les ressources associées aux threads
n’étant pas libérées en fin d’exécution (elles ne le sont qu’à la fin du processus), un mécanisme a été mis en
place afin de libérer ses ressources avant qu’il se termine vraiment.

TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 159
Threads ou processus légers 6 Autres fonctions de la bibliothèque POSIX threads

Pour une ressource que l’on vient d’allouer à un thread, pthread_cleanup_push permet de préciser la
fonction devant être exécutée afin de libérer la ressource, et le paramètre qui sera passé à cette fonction. Ces
fonctions sont placées dans une pile spéciale. À la terminaison du programme, les fonctions sont dés-empilées
et exécutées.
Le programmeur peut lui-même dés-empiler ces fonctions par l’intermédiaire d’un appel à
pthread_cleanup_pop. L’unique paramètre précise si la fonction doit être simplement dés-empilée (0) ou
aussi exécutée (1).
pthread_cleanup_push et pthread_cleanup_pop sont généralement implémentés sous la forme de ma-
cros dont la première ouvre un bloc lexical que ferme la seconde. Les deux appels doivent appartenir au
même bloc d’instructions.

Voir l’exemple précédent : cancel.c


' $
6.3 Initialisation

 À l’entrée d’une fonction


 Type : pthread_once_t
 Valeur : PTHREAD_ONCE_INIT
# 40  int pthread_once ( pthread_once_t *once_control, void
(*once_routine)())

 Lors d’un fork


 Duplication du fil d’exécution réalisant un appel à fork
 int pthread_atfork (void (*prepare) ( ), void (*parent) ( ), void
(*child) ( ))

& %
Une fonction peut être utilisée par plusieurs threads. Toutefois, certaines de ses variables peuvent ne devoir
être initialisées qu’une seule fois (c’est le cas pour l’ouverture d’une base de données par exemple). La fonction
pthread_once permet de réaliser cette opération en s’affranchissant des problèmes de synchronisation si
plusieurs threads l’appellent simultanément.
Le type pthread_once_t est opaque. Pour être utile, une variable de ce type doit être déclarée de
manière statique ou globale (afin de ne pas avoir une copie de la variable pour chaque appel de fonction).
L’initialisation s’effectue à l’aide de la valeur prédéfinie PTHREAD_ONCE_INIT. La fonction précisée en second
paramètre de l’appel à pthread_once n’est exécutée que lors du premier passage. Le premier paramètre est
un pointeur sur une variable de type pthread_once_t.
Lors d’un appel à la primitive fork, seul le fil d’exécution réalisant l’appel est dupliqué. Cependant, l’en-
semble des ressources (en particulier les piles d’exécutions et les segments de mémoire alloués dynamiquement
par les autres threads) sont aussi dupliqués. La primitive pthread_atfork permet d’empiler — pour chaque
ressource devant être libérée lors d’un appel à fork — les fonctions devant être appelées respectivement
avant l’appel à fork, par le père après l’appel à fork et par le fils après l’appel à fork.

TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 160
Architecture

François Trahay

module CSC4508/M2

Avril 2016

161
Architecture 1 Introduction
' $
Plan du document

1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
#2 3 Processeur séquentiel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
3 Pipeline . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
4 Parallel Processing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
5 Hiérarchie mémoire . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20

& %

' $

1 Introduction

#3 Pourquoi ce cours ?
 Comprendre ce qui se passe dans la partie “hardware” de la pile d’exécution
 Pour écrire des programmes adaptés aux machines modernes

& %

Dans les faits, le compilateur se débrouille généralement pour générer un binaire qui permette d’exploiter
toutes les capacités du processeur. Mais le compilateur échoue parfois et génère un code non optimisé. Il
faut donc être capable de détecter le problème, et être capable d’écrire un code que le compilateur saura
optimiser.

TELECOM SudParis — François Trahay — Avril 2016 — module CSC4508/M2 162


Architecture
' $
1.1 Loi de Moore
1965 – 2005
 Loi de Moore (1965) : le nombre de transistors des micro processeurs double tous
les deux ans
 La finesse de gravure du processeur diminue
 La fréquence d’horloge augmente
#4
–> Augmentation des performances du processeur
Depuis 2005
 La finesse de gravure continue de diminuer (mais moins vite)
 La fréquence d’horloge n’augmente plus
 Dissipation thermique dépend de la fréquence, du nombre de transistors, de la
taille des transistors
 Si on diminue la taille des transistors, il faut réduire la fréquence
& %

' $
2 Processeur séquentiel
 Une instruction nécessite N étapes
 Fetch : chargement de l’instruction depuis la mémoire
 Decode : identification de l’instruction
 Execute : exécution de l’instruction
 Writeback : stockage du résultat

#5  Chaque étape est traitée par un circuit du processeur


 La plupart des circuits n’est pas utilisée à chaque étape
→ Une instruction est exécutée tous les N cycles
Fetch Execute

Decode Writeback

instructions

cycles d'horloge

& %

Le nombre d’étapes nécessaire à l’exécution d’une instruction dépend du type de processeur (Pentium 4 :
31 étages, Intel Haswell : 14-19 étages, ARM9 : 5 étages, etc.)

TELECOM SudParis — François Trahay — Avril 2016 — module CSC4508/M2 163


Architecture 3 Pipeline
' $

3 Pipeline
 Pipeline d’instructions
 À chaque étape, plusieurs circuits sont utilisés
→ Une instruction est exécutée à chaque cycle
Fetch Execute
#6
Decode Writeback

instructions

cycles d'horloge

Figure 1 : Exécution d’instructions sur un processeur avec pipeline

& %

' $

3.1 Micro architecture d’un pipeline


 Chaque étage du pipeline est implémenté par un ensemble de portes logiques
 Étage Execute : un sous-circuit par type d’opération (unité fonctionnelle)
int
#7 fetch decode float writeback
branch
mem

Figure 2 : Micro-architecture d’un pipeline

& %

TELECOM SudParis — François Trahay — Avril 2016 — module CSC4508/M2 164


Architecture 3 Pipeline
' $

3.2 Processeurs superscalaires


 Utilisation des différentes unités fonc-
tionnelles simultanément
int writeback

→ plusieurs instructions exécutées simulta-


nément !
float writeback

 Nécessité de charger et décoder plusieurs


fetch decode &
#8 instructions simultanément dispatch

test branch

mem writeback

Figure 3 : Micro-architecture d’un pro-


cesseur superscalaire

& %

' $

3.3 Processeurs superscalaires


Fetch Execute

Decode Writeback

#9
instructions

cycles d'horloge

& %

TELECOM SudParis — François Trahay — Avril 2016 — module CSC4508/M2 165


Architecture 3 Pipeline
' $

3.4 Dépendance entre instructions


Limitations du superscalaire :
 il ne doit pas y avoir de dépendance entre les instructions exécutées simultanément.
 Exemple d’instructions non parallélisables
# 10
a = b * c;
d = a + 1;
 Degré de parallélisme des instructions : Instruction Level Parallelism (ILP)
 Les instructions exécutées en parallèles doivent utiliser des unités fonctionnelles
différentes

& %

' $
3.5 Gestion des branchements
 Comment remplir le pipeline quand les instructions contiennent des branchements
conditionnels ?
cmp a, 7 ; a > 7 ?
ble L1
mov c, b ; b = c
br L2
L1: mov d, b ; b = d
# 11
L2: ...
 En cas de mauvais choix : il faut “vider” le pipeline
→ perte de temps
Fetch

Execute
mov c,b

instructions ? ble L1 cmp a,7


mov d,b Decode Writeback

cycles d'horloge
& %

Le coût d’un mauvais choix lors du chargement d’une branche dépend de la profondeur du pipeline : plus
le pipeline est long, plus il faut vider d’étages (et donc attendre avant d’exécuter une instruction). Pour cette
raison (entre autres), la profondeur du pipeline dans un processeur est limitée.

TELECOM SudParis — François Trahay — Avril 2016 — module CSC4508/M2 166


Architecture 3 Pipeline
' $

3.6 Prédiction de branchement


 Le processeur implémente un algorithme de prédiction
 Idée générale :
 Pour chaque branchement, on se rappelle des résultats précédents
# 12 0x12 loop:
... addr branch history
0x50 inc eax 0x23 0011
0x54 cmpl eax, 10000 0x42 1000
0x5A jl loop 0x5A 1111
0x5C end_loop: 0x7E 0000
...

& %
Les algorithmes de prédiction de branchement implémentés dans les processeurs modernes sont aujour-
d’hui très évolués et atteignent une efficacité supérieure à 98 % (sur la suite de benchmarks SPEC89).
Pour connaître le nombre de bonnes/mauvaises prédictions, on peut consulter les compteurs matériels
du processeur. Avec la bibliothèque PAPI 1 , les compteurs PAPI_BR_PRC et PAPI_BR_MSP donnent le nombre
de branchements correctement et incorrectement prédits.
' $

3.7 Instructions vectorielles


 De nombreuses applications fonctionnent en mode Data Parallelism
 Single Instruction, Multiple Data (SIMD) : une même opération appliquée à un
ensemble de données
for(i=0; i<size; i++) {
C[i] = A[i] * B[i];
# 13 }
 Exemple : image, scientific computing
 Utilisation d’instructions vectorielles (MMX, SSE, AVX, ...)
 instructions spécifiques à un type de processeur
for(i=0; i<size; i+= 8) {
*pC = _mm_mul_ps(*pA, *pB);
pA++; pB++; pC++;
}
& %
Les instructions vectorielles ont été démocratisés à la fin des années 1990 avec les jeux d’instructions
MMX (Intel) et 3DNow ! (AMD) permettant de travailler sur 64 bits (par exemple pour faire 2 opérations 32
bits en simultané). Depuis, chaque génération de processeurs x86 apporte son extension au jeu d’instruction :
SSE2, SSSE3 (128 bits), SSE4, AVX, AVX2 (256 bits), AVX512 (512 bits). Les autres types de processeurs
fournissent également des jeux d’instructions vectorielles (par exemple NEON (128 bits) sur ARM).
Les jeux d’instructions vectorielles sont spécifiques à certains processeurs. Le fichier /proc/cpuinfo
1. http ://icl.cs.utk.edu/projects/papi/

TELECOM SudParis — François Trahay — Avril 2016 — module CSC4508/M2 167


Architecture

contient (entre autres) les jeux d’instructions disponibles sur le processeur d’une machine. Par exemple, sur
un Intel Core i7 :

$ cat /proc/cpuinfo
processor : 0
vendor_id : GenuineIntel
cpu family : 6
model : 69
model name : Intel(R) Core(TM) i7-4600U CPU @ 2.10GHz
stepping : 1
microcode : 0x1d
cpu MHz : 1484.683
cache size : 4096 KB
physical id : 0
siblings : 4
core id : 0
cpu cores : 2
apicid : 0
initial apicid : 0
fpu : yes
fpu_exception : yes
cpuid level : 13
wp : yes
flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov
pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx
pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good nopl
xtopology nonstop_tsc aperfmperf eagerfpu pni pclmulqdq dtes64
monitor ds_cpl vmx smx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid
sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx
f16c rdrand lahf_lm abm ida arat epb pln pts dtherm tpr_shadow vnmi
flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms
invpcid xsaveopt
bugs :
bogomips : 5387.82
clflush size : 64
cache_alignment : 64
address sizes : 39 bits physical, 48 bits virtual
power management:
[...]

Le champs flags contient la liste de toutes les capabilities du processeur, notamment les jeux d’instruc-
tions disponibles : mmx, sse, sse2, ssse3, sse4_1, sse4_2, avx2.

L’utilisation de ces jeux d’instructions vectorielles peut se faire en programmant directement en assem-
bleur ou en exploitant les intrinsics que fournissent les compilateurs. Toutefois, devant le nombre de jeux
d’instructions disponible et l’évolutilité des architectures de processeurs, il est recommandé de laisser le
compilateur optimiser lui même le code, par exemple en utilisant l’option -O3.

TELECOM SudParis — François Trahay — Avril 2016 — module CSC4508/M2 168


Architecture 4 Parallel Processing
' $

# 14 4 Parallel Processing

& %

' $
4.1 Hyperthreading / SMT
 Problème du superscalaire/vectoriel :
 Il faut que l’application ait suffisamment de parallélisme à exploiter
 Il y a d’autres applications qui attendent d’avoir le CPU
 Simultaneous Multi-Threading (SMT) :
 Modifier un processeur superscalaire pour l’exécution de plusieurs threads

# 15  Duplication de certains circuits


 Mise en commun de certains circuits
Mem/Branch

Thread A ALU (int)

decode &
Shared fetch dispatch
FPU (float) writeback

Thread B ALU (int)

Mem/Branch

& %

Le SMT est un moyen peu cher d’augmenter les performances d’un processeur : en dupliquant les “petits”
circuits (ALU, registres, etc.) et en mettant en commun les “gros” circuits (FPU, prédiction de branchements,
caches), on peut exécuter plusieurs threads simultanément. Le surcoût en terme de fabrication est léger et
le gain en performances peut être grand.
Comme le dispatcher ordonnance les instructions de plusieurs threads, une erreur du prédicteur de bran-
chement devient moins grave puisque pendant que le pipeline du thread fautif se renouvelle, un autre thread
peut s’exécuter.
Le gain de performance lorsque plusieurs threads s’exécutent n’est pas systématique puisque certains
circuits restent partagés (par exemple, le FPU).

TELECOM SudParis — François Trahay — Avril 2016 — module CSC4508/M2 169


Architecture 4 Parallel Processing
' $
4.2 Processeurs multi-cœurs
 Scalabilité du SMT limitée
 dispatcher partagé
 FPU partagé
→ Duplication de tous les circuits

Mem/Branch
Core A
# 16
decode &
fetch ALU (int) writeback
dispatch

FPU (float)

ALU (int)
Core B
decode &
fetch Mem/Branch writeback
dispatch

FPU (float)

& %

Il est bien sûr possible de combiner multi-cœur avec SMT. La plupart des fondeurs produisent des
processeurs multi-cœur SMT : Intel Core i7 (4 cœurs x 2 threads), SPARC T3 Niagara-3 (16 cœurs x 8
threads), IBM POWER 7 (8 cœurs x 4 threads).

' $

4.3 Architectures SMP


 Symmetric Multi-Processing
 Assemblage de processeurs sur une carte mère
 Les processeurs se partagent le bus système
 Les processeurs se partagent la mémoire

# 17  Problème de scalabilité : contention pour l’accès au bus

CPU CPU

System bus I/O

Mem

& %

TELECOM SudParis — François Trahay — Avril 2016 — module CSC4508/M2 170


Architecture 5 Hiérarchie mémoire
' $

4.4 Architectures NUMA


 Nœuds NUMA connectés par un réseau rapide
 Cohérence mémoire entre les processeurs
 Accès privilégié au banc mémoire local
 Accès possible (avec un surcoût) aux bancs mémoire situés sur les autres nœuds

# 18 → Non-Uniform Memory Architecture

CPU CPU
Mem Mem

I/O

I/O
Mem
Mem
CPU CPU

& %

Les premières machines NUMA (dans les années 1990) étaient simplement des ensembles de machines
reliées par un réseau propriétaire chargé de gérer les transferts mémoire. Depuis 2003, certaines cartes mères
permettent de relier plusieurs processeurs Opteron (AMD) reliés par un lien HyperTransport. Intel a par la
suite développé une technologie similaire (Quick Path Interconnect, QPI) pour relier ses processeurs Nehalem
(sortis en 2007).

' $

# 19 5 Hiérarchie mémoire

& %

TELECOM SudParis — François Trahay — Avril 2016 — module CSC4508/M2 171


Architecture 5 Hiérarchie mémoire
' $

5.1 Enjeux
 Jusqu’en 2005 : augmentation des perfs des CPUs : 55%/an
 Depuis 2005 : augmentation du nombre de cœur par processeur
 Augmentation des perfs de la mémoire : 10%/an
 Ce sont les accès mémoire qui sont coûteux : Memory Wall
# 20  Il faut des mécanismes pour améliorer les performances de la mémoire

& %

Jusqu’aux années 1990, le goulet d’étranglement était le processeur. Du point de vue logiciel, on cherchait
alors à minimiser le nombre d’instructions à exécuter.

Avec l’augmentation des performances des processeurs, la pression est maintenant sur la mémoire. Côté
logiciel, on cherche donc à minimiser le nombre d’accès à la mémoire. Cette pression sur la mémoire est
exacerbée par le développement des processeurs multi-cœurs.

Par exemple, un processeur Intel Core i7 peut engendrer jusqu’à 2 accès mémoire par cycle d’horloge. Un
processeur à 8 cœurs hyper-threadés (donc 16 threads) tournant à 3.0 Ghz 1 peut donc générer 2×16.0×109 =
96 milliards de références mémoire par seconde. Si l’on considère des accès à des données de 64 bits, cela
représente 3072 Go/s (3.072 To/s). À ces accès aux données, il faut ajouter les accès aux instructions (jusqu’à
128 bits par instruction). On arrive donc à 6144 Go/s (donc 6.144 To/s !) de débit maximum.

À titre de comparaison, en 2016, une barette de mémoire RAM (DDR4) a un débit maximum de l’ordre
de 20 Go/s. Il est donc nécessaire de mettre en place des mécanismes pour éviter que le processeur ne passe
son temps à attendre la mémoire.

1. Exemple : un Intel Core i7-5960X sorti en 2014

TELECOM SudParis — François Trahay — Avril 2016 — module CSC4508/M2 172


Architecture 5 Hiérarchie mémoire
' $
5.2 Caches
 Accès à la mémoire (RAM) très coûteux (env. 60 ns – env. 180 cycles)
 Pour accélérer les accès mémoire, utilisation de mémoire cache rapide :
 Cache L1 : très petite capacité (typiquement : 64 Ko), très rapide (env. 4 cycles)
 Cache L2 : petite capacité (typiquement : 256 Ko), rapide (env. 10 cycles)
 Cache L3 : grande capacité (typiquement : entre 4Mo et 20Mo), lent (env. 40
cycles)
# 21
 Accès au disque dur (SWAP) très coûteux : env. 40 ms (150 µs sur un disque SSD)

& %

Pour visualiser la hiérarchie mémoire d’une machine, vous pouvez utiliser l’outil lstopo fourni par la
bibliothèque hwloc.

TELECOM SudParis — François Trahay — Avril 2016 — module CSC4508/M2 173


5 Hiérarchie mémoire 5.3 Memory Management Unit (MMU)
' $

5.3 Memory Management Unit (MMU)


 Traduit les adresses mémoire virtuelles
CPU MMU disk
en adresses physiques v addr.
cache RAM
p addr. Page
TLB table
# 22  Cherche dans le TLB (Translation Loo-
registers
kaside Buffer), puis dans la table des Data / Instruction

pages
 Une fois l’adresse physique trouvée, de-
mande les données au cache/mémoire

& %

' $

5.3.1 Fully-associative caches


 Cache = tableau à N entrées
Adresse
 À chaque référence, recherche de Tag dans le
Tag Offset
tableau
# 23  Si trouvé (cache hit) et Valid=1 : accès à la Valid Tag Data

ligne de cache Data


 Sinon (cache miss) : accès RAM
 Problème : nécessite de parcourir tout le tableau
→ Principalement utilisé pour les petits caches
(ex : TLB)

& %

La taille d’une ligne de cache dépend du processeur (généralement entre 32 et 128 octets). On peut
retrouver cette information dans /proc/cpuinfo :

$ cat /proc/cpuinfo |grep cache_alignment


cache_alignment : 64

TELECOM SudParis — François Trahay — Avril 2016 — module CSC4508/M2 174


5 Hiérarchie mémoire 5.3 Memory Management Unit (MMU)
' $

5.3.2 Direct-mapped caches


 Utilisation des bits de poids faible de l’adresse
Adresse
pour trouver l’index de l’entrée dans le cache 31 30 ... 13 12 11 ... 2 1 0

Tag Index Offset


 Comparaison du Tag (bits de poids fort) de 20
10

l’adresse et de l’entrée. Index Valid Tag Data


# 24 0
1
→ Accès direct à la ligne de cache 2

 Attention : risque de collision ...

 exemple : 0x12345 678 et 0xbff72 678 1021


1022
1023
20 32

& %

' $

5.3.3 Set-associative caches


 Index pour accéder à un set de K lignes de
Adresse
cache 31 30 ...
0011 ... 1010
13 12 11 ... 2 1 0

Tag Index Offset


 Recherche du Tag parmi les adresses du set 20
10

→ Cache associatif K-voies (K-way associative Index Valid Tag Data

# 25 cache) 0

1001 ... 0110


... 0011 ... 1010

n-1

32

& %

De nos jours, les caches (L1, L2 et L3) sont généralement associatifs à 4 (ARM Cortex A9 par exemple),
8 (Intel Sandy Bridge), voire 16 (AMD Opteron Magny-Cours) voies.

TELECOM SudParis — François Trahay — Avril 2016 — module CSC4508/M2 175


Architecture 5 Hiérarchie mémoire
' $

5.3.4 Cohérence de cache


 Que faire si 2 threads accèdent à une même zone mémoire ?
# 26
 En lecture : réplication dans les caches locaux
 En écriture : nécessité d’invalider les données dans les autres caches
I Cache snooping : le cache envoie un message qui invalide les autres caches

& %

' $

5.4 Bibliographie
 « Computer Architecture, Fifth Edition : A Quantitative Approach », J. L. Hennessy
et D. A. Patterson, Morgan Kaufmann, 2011.
# 27
 « Computer Organization and Design, Fifth Edition : The Hardware/Software
Interface », J. L. Hennessy et D. A. Patterson, Morgan Kaufmann, 2013.
 « Computer Systems A Programmer’s Perspective » (2nd Edition), R. E. Bryant et
D. R. O’Hallaron, Prentice Hall, 2010

& %
Pour détailler un peu plus ce cours, je vous conseille cette page web : Modern microprocessors – A 90
minutes guide ! (http://www.lighterra.com/papers/modernmicroprocessors/).
Pour avoir (beaucoup) plus de détails, tournez vous vers les livres « Computer Systems A Programmer’s
Perspective » et « Computer Organization and Design, Fifth Edition : The Hardware/Software Interface »
décrivent en détail l’architecture d’un ordinateur. Si vous cherchez à connaître le fonctionnement très en
détail d’un point précis, lisez « Computer Architecture, Fifth Edition : A Quantitative Approach ».

TELECOM SudParis — François Trahay — Avril 2016 — module CSC4508/M2 176


Éléments d’architecture
client-serveur

Michel Simatic

module CSC4508/M2

Avril 2016

177
Éléments d’architecture client-serveur 1 Introduction
' $
Plan du document

1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
2 Serveur mono-tâche gérant un client à la fois . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
#2 3 Serveur avec autant de tâches que de clients . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
4 Serveur avec N tâches gérant tous les clients . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
5 Serveur mono-tâche gérant tous les clients à la fois . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
6 Conclusion. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .23

& %

' $
1 Introduction

#3 1.1 Définition d’une architecture client/serveur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4


1.2 Objectif de cette présentation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
2.0 À propos des communications . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7

& %

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 178


Éléments d’architecture client-serveur 1 Introduction
' $
1.1 Définition d’une architecture client/serveur
 Une application fonctionne selon une architecture client-serveur quand : les
machines « clientes » contactent une machine « serveur » afin que ce serveur leur
fournisse un service (via l’exécution d’un programme)
 Les clients envoient des requêtes
 Le serveur envoie des réponses

#4

& %

Exemples d’applications :

• Serveur d’impression

• Serveur Web

• Serveur d’autorisation de cartes bancaires

• Serveur de mail

• Traitement d’un appel (établissement, raccrochage. . .) dans un commutateur téléphonique

• Localisation d’un mobile GSM lors d’un appel vers ce mobile

' $
1.2 Objectif de cette présentation

 Étudier différentes architectures logicielles pour traiter le flux de requêtes arrivant au


niveau du serveur
4 types d’architecture :
 Serveur mono-tâche gérant un client à la fois
#5
 Serveur avec autant de tâches que de clients
 Serveur avec N tâches gérant tous les clients
 Serveur mono-tâche gérant tous les clients à la fois
 Analyser les qualités et les défauts de chacune de ces architectures
 Étudier comment combiner les différentes briques du système d’exploitation pour
disposer de l’architecture la plus propice à répondre aux besoins de l’application

& %

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 179


Éléments d’architecture client-serveur

Selon l’unité de multi-programmation utilisée, une « tâche » sera implémentée via un « processus » ou
un « thread ».

Ne pas hésiter à compléter ce cours avec la lecture de [Leitner, 2003] et [Kegel, 2006]. Ils contiennent de
nombreuses informations techniques.

' $
1.3 À propos des communications

 Tout processus (client ou serveur) peut recevoir des messages via un (ou plusieurs)
« point(s) d’accès » qui lui est (sont) propre(s)
 Pour qu’un autre processus lui envoie un message via un « point d’accès », il faut,
au préalable, que cet autre processus se « connecte » (au sens protocolaire du
terme) (exemple : TCP, tube nommé) ou non (exemple : UDP, Système de
#6 signalisation CCITT No 7)
 Par abus de langage, on dit aussi qu’un client se « connecte » au serveur lorsqu’il a
une suite d’échanges requête/réponse avec le serveur (cette « connexion client »
nécessitant une connexion protocolaire ou non).
Dans la suite de cette présentation, le terme de « connexion » désignera toujours
une « connexion client »
 L’aspect communication inter-machine est hors du contexte de ce cours. Donc, dans
les TPs, on utilisera des « files de messages » en guise de « points d’accès »
& %

Dans la pratique, un « point d’accès » est :

Cas UDP/TCP un couple adresseMachine/port

Cas Tube nommé un nom de fichier

Cas SS7 une adresse Système de signalisation CCITT No 7 (SS7 ou #7)

La programmation des « sockets » pour gérer des flux TCP ou des messages UDP est abordée dans l’U.V.
« Algorithmique et communications des applications » (CSC4509).

Même si les TPs se font en intra-machine, tous les éléments présentés ici sont valides en environnement
inter-machine.

Dans les TPs, nous utilisons des « files de messages ». On pourrait utiliser aussi des « tubes nommés ».

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 180


Éléments d’architecture client-serveur 2 Serveur mono-tâche gérant un client à la fois
' $
2 Serveur mono-tâche gérant un client à la fois

#7 2.1 Principe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
3.1 Analyse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11

& %

' $
2.1 Principe
 La tâche serveur traite les connexions client de bout en bout

#8

& %

• En 1, le serveur est en attente d’une « connexion client ». Le client C1 peut donc se connecter au
serveur pour envoyer la requête Requête11 (contenant la référence de son point d’accès sur lequel il
attend des réponses).

• En 2, le serveur traite cette requête de la manière suivante :

– Il ouvre un « point d’accès » dédié C1-Serveur


– Il envoie sa réponse Réponse11(avec la référence du « point d’accès » dédié)
– Il attend d’autres requêtes (ou un message de fermeture de « connexion client ») sur ce « point
d’accès » dédié.

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 181


Éléments d’architecture client-serveur 3 Serveur avec autant de tâches que de clients

• En 3, le serveur répond aux différentes requêtes du client C1. En revanche, comme il n’est pas à l’écoute
sur son « point d’accès » principal d’attente de « connexion client », il ne se rend pas compte que C2
a envoyé la requête Requête21 pour laquelle il attend une réponse.

Exemple d’applications basées sur cette architecture :

• Serveur d’impression

' $
2.2 Analyse

 Avantages
 Architecture simple
 Bien adaptée au cas de traitements courts
 Machine serveur peu chargée
#9  Pas de risque de surcharge
 Inconvénients
 Architecture inadaptée au cas de traitements longs, que cette longueur soit dûe
à:
I Temps de traitement long au niveau du serveur
I Nombreux échanges requête/réponse entre client et serveur avant que le client
ne se déconnecte

& %

' $
3 Serveur avec autant de tâches que de clients

3.1 Principe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
# 10 3.3 Variante . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
3.3 Analyse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
4.0 Réduction du temps de connexion des clients . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14

& %

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 182


Éléments d’architecture client-serveur 3 Serveur avec autant de tâches que de clients
' $
3.1 Principe
 La tâche serveur délègue les connexions client à des enfants

# 11

& %

• En 1, le serveur est en attente d’une « connexion client ». Le client C1 peut donc se connecter au
serveur pour envoyer la requête Requête11 (contenant la référence de son point d’accès sur lequel il
attend des réponses).

• En 2, le serveur crée un enfant (Enfant1) chargé de gérer les requêtes venant du client C1. Il lui fait
suivre via un « point d’accès » local (par exemple, un tube, une file de messages ou bien une zone
mémoire) la requête Requête11 :

– Le client ferme le « point d’accès » d’attente de « connexion client » (c’est le serveur qui est
responsable de surveiller ce « point d’accès »)
– Enfant1 ouvre un « point d’accès » dédié C1-Enfant1
– Il envoie sa réponse Réponse11(avec la référence du « point d’accès » dédié)
– Il attend d’autres requêtes (ou un message de fermeture de « connexion client ») sur ce « point
d’accès » dédié.

• En 3 :

– Enfant1 répond aux différentes requêtes en provenance du client C1


– Le serveur est en mesure de répondre instantanément aux requêtes venant d’autres clients. Pour
ce faire, il crée un enfant dédié qui se charge de répondre.

Exemples d’applications basées sur cette architecture :

• Serveur de mail

• La plupart des applications se connectant à un SGBD

• Serveur Web Apache (cf. ci-dessous)

Pour gagner en performance (4.000 forks de processus nécessitent environ 9 secondes sur une machine à
1 GHz, soit 2 millisecondes par fork ; de plus l’arrêt de ces 4.000 processus nécessitent environ 25 secondes,
soit 6 millisecondes par libération), au lieu de créer des enfants à chaque connexion de client, on peut créer
(au moment du démarrage du serveur) un « pool d’enfants disponibles ».
Le « pool d’enfants disponibles » est un patron de conception dénommé Thread pool en anglais. En
plus d’éviter les risques de surcharge, il permet d’économiser les opérations de création/nettoyage d’en-
fants/threads pendant que le système est sollicité.

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 183


Éléments d’architecture client-serveur 3 Serveur avec autant de tâches que de clients

Conceptuellement, le « pool d’enfants disponibles » consiste à avoir une structure de données dans laquelle
on range les identifiants des enfants créés lors du démarrage de l’application. Quand le serveur a besoin de
faire travailler un enfant, il prend l’identifiant du premier enfant de la structure de données et il lui donne la
tâche à faire. Quand l’enfant a fini, cet enfant stocke son identifiant dans la structure de données (il signifie
ainsi qu’il est à nouveau disponible pour recevoir du travail de la part du serveur).
Dans la pratique (cf. Cf. exercice « Éléments d’architecture client-serveur » numéro 2), le « pool d’enfants
disponibles » peut être implémenté de la manière suivante :

• Tous les enfants qui sont créés se mettent directement en attente (en lecture) sur le point d’entrée
du serveur. Ainsi, quand le serveur reçoit une requête, dans le cas où le système gère l’atomicité des
lectures sur le point d’entrée (i.e. un seul processus peut réussir à faire la lecture sur le point d’entrée,
même s’ils sont plusieurs à être en attente [en lecture] sur le point d’entrée), un seul enfant est réveillé
par le système et traite la requête qu’il a lue sur le point d’entrée. Quand cet enfant a fini, il se remet
en attente sur le point d’entrée.

• Tous les enfants qui sont créés se mettent en attente (en lecture) sur le même tube (ou la même file de
message, ou bien encore le même tampon contrôlé par un mécanisme de producteur consommateur).
Quand le serveur reçoit une requête, il « redirige » cette requête en écrivant sur le tube (ou file de
message ou tampon). Un enfant est donc réveillé. Il traite la requête. Quand il a fini, il se met en
attente (en lecture) sur le tube.

Le serveur Web Apache (http://httpd.apache.org/) utilise un « pool d’enfants disponibles ».

' $
3.2 Variante

 Dans le cas où le nombre de « points d’accès » est limité au niveau de la machine


du serveur (exemple : SS7), la tâche serveur sert de distributeur à ses enfants
 On parle alors de « multiplexage » des « connexions client » au niveau du serveur

# 12

& %

Noter que les enfants n’ont pas besoin de « point d’accès » dédié C-Enfant dans la mesure où seul le
serveur s’adresse directement à eux.

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 184


Éléments d’architecture client-serveur
' $
3.3 Analyse

 Avantages
 Adapté au cas de traitements longs
 Inconvénients
 Le serveur doit être hébergé par une machine puissante

# 13  Risque de surcharge à moins d’utiliser :


I des mécanismes de synchronisation de tâches à base de « cohorte »
I un pool d’enfants disponibles
 Sensibilité au nombre de clients : la charge du serveur augmente
proportionnellement au nombre de clients
Solutions possibles :
I Augmenter la puissance du serveur (par exemple, grappes de PC)
I Réduire le temps de connexion des clients

& %

• Évaluons la sensibilité au nombre de clients :


– Si on fait 4.000 fork d’un processus simple sur une machine RedHat 7.3, on observe que la mémoire
consommée est de 225 Mo, soit 57,6 Ko/processus forké.
– Si on considère un commutateur de communications GSM, ce commutateur a besoin de 25 tâches
en parallèle par abonné.
– Ce commutateur gère 100.000 abonnés. Il requiert donc 2.500.000 tâches, soit 137 Go de RAM.
– Pour information, grâce à l’architecture « Serveur avec N tâches gérant tous les clients » (cf.
section 4), le contexte requis par les 25 tâches représente dans la réalité 107 Ko par abonné, c’est-
à-dire 10 Go de RAM pour gérer les 100.000 abonnés (à comparer avec les 137 Go précédents).

' $
3.4 Réduction du temps de connexion des clients

 Principe :
 L’enfant n’est connecté avec le client que pendant le temps de traitement d’une
requête
 Si le client a une requête ultérieure faisant partie du même contexte, il transmet
# 14 le contexte avec la requête ultérieure
 Exemple : Google
 Limite : Solution inapplicable au cas d’applications où le client ne peut héberger le
contexte :
 Soit pour des raisons de capacité (mémoire, traitement. . .)
 Soit pour des raisons de sécurité

& %

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 185


Éléments d’architecture client-serveur 4 Serveur avec N tâches gérant tous les clients

La plupart des applications télécoms sont confrontées à ce problème.


' $
4 Serveur avec N tâches gérant tous les clients

# 15 4.1 Principe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
5.0 Analyse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18

& %

' $
4.1 Principe
 Le serveur héberge dans une « base de données »a le contexte de chacun de ses
clients potentiels
 Au démarrage, le serveur démarre N enfants
 Lors de la réception d’une requête, le serveur confie à l’un de ses enfants la tâche de
gérer la requête compte tenu du contexte client

# 16  L’enfant fait éventuellement évoluer le contexte

a. Cette base de données peut être une zone de mémoire partagée


& %
Exemples d’applications basées sur cette architecture :
• la plupart des applications télécoms (Commutateur, Service Control Point dans les « Réseaux Intelli-
gents », serveur de localisation dans le GSM. . .)
• le serveur Web Nginx (http://wiki.nginx.org/)
Les langages à base d’automate (Grafcet, SDL. . .) sont bien adaptés à l’écriture de la prise en compte de
requêtes (appelées « événements », « signaux ». . .) compte tenu d’un contexte. Le traitement de la requête
fait faire une « transition » à l’automate.
Lors de la connexion d’un client, les critères de choix de l’enfant par le serveur peuvent être variés :

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 186


Éléments d’architecture client-serveur

• Sélection de l’enfant traitant le moins de clients actifs à l’instant t

• Sélection de l’enfant dédié à ce type de requêtes

• Dans le cas où le serveur est multiprocesseur, la base de donnée peut être distribuée entre les différents
processeurs. Dans ce cas, sélection de l’enfant dont la base de données contient le contexte du client.

' $
4.2 Analyse

 Avantages
 Adapté au cas de connexions longues avec de longs temps d’inactivité
 Cas, par exemple, des applications télécomsa
# 17
 Inconvénients
 Complexité
 Risques de surcharge

a. Le gros du traitement d’appel se fait à l’établissement et au raccrochage (le temps de conver-


sation qui est le plus long en durée ne consomme pratiquement rien en ressources serveur)
& %

À propos des risques de surcharge :

• Un bon exemple est le comportement du réseau GSM français le 01/01/2002. Beaucoup de gens avaient
décidé d’envoyer leurs voeux par SMS le 01/01/2002 à 00h00. Certains voeux sont arrivés plusieurs
jours plus tard à leurs destinataires.

• En téléphonie, manières de se préserver contre la surcharge :

– « Espacement d’appel » (« call gapping ») : si le serveur détecte un risque de surcharge, il refuse


les appels sortants (i.e. les tentatives d’appel).
Exemple : le 21/09/01, lors de l’explosion de l’usine AZF, les commutateurs de communication
mobile de la région toulousaine sont rentrés en « régulation » pendant 4 heures (rendant les
appels entrants beaucoup plus prioritaires que les appels sortants) en constatant que le nombre
de tentatives d’appels départ étaient multipliés par 15 (les abonnés cherchaient à téléphoner plus
avec leur mobile, car les centraux fixes étaient saturés et on avait un taux d’efficacité des appels
départ de seulement 15%).

– Méthode de niveau « réseau téléphonique » : certains numéros de téléphone peuvent donner lieu
à beaucoup d’appels concentrés dans le temps (par exemple, numéro d’un concours lors d’une
émission télévisée). Dans ce cas, les commutateurs traversés lors de l’établissement de l’appel ne
laissent passer qu’un faible pourcentage d’appels vers ce numéro (par exemple, 1%).

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 187


Éléments d’architecture client-serveur 5 Serveur mono-tâche gérant tous les clients à la fois
' $
5 Serveur mono-tâche gérant tous les clients à la fois

# 18 5.1 Principe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
5.2 Aperçu de la programmation événementielle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
5.3 Analyse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22

& %

' $
5.1 Principe
 La tâche serveur travaille avec un modèle de programmation événementielle.

# 19

& %

• En 1, le serveur est en attente d’une « connexion client ». Le client C1 peut donc se connecter au
serveur pour envoyer la requête Requête11 (contenant la référence de son point d’accès sur lequel il
attend des réponses).

• En 2, le serveur traite cette requête de la manière suivante :

– Il ouvre un « point d’accès » dédié C1-Serveur


– Il envoie sa réponse Réponse11 (avec la référence du « point d’accès » dédié)
– Il attend d’autres requêtes (ou un message de fermeture de « connexion client ») sur ce « point
d’accès » dédié.

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 188


Éléments d’architecture client-serveur 5 Serveur mono-tâche gérant tous les clients à la fois

• En 3, le serveur répond aux différentes requêtes du client C1. Parce que le serveur travaille avec un
modèle de programmation événementiel, il est en mesure de prendre en compte : il ouvre un « point
d’accès » dédié C2-Serveur et envoie sa réponse Réponse12 (avec la référence du « point d’accès »
dédié). Il se met ensuite en attente d’événements sur les trois points d’accès.
Exemple d’applications basées sur cette architecture :
• Memcached (http://memcached.org/) : une mémoire distribuée pour cacher des objets
• Nginx (http://nginx.net/) : un serveur Web (notamment) très performant

' $
5.2 Aperçu de la programmation événementielle

 Principe
 On ne déclenche une opération de lecture sur un descripteur que quand on est
sûr qu’il y a effectivement quelque chose à lire sur ce descripteur.
 Pour savoir si c’est le cas, on s’appuie sur des primitives système comme
select, poll, epoll. . .

# 20
 Des intergiciels (middleware) facilitent le développement d’applications basées sur
ce modèle. Ils garantissent la portabilité entre systèmes différents tout en utilisant
les primitives système les plus performantes.
Exemple : libevent (http://monkey.org/~provos/libevent/
 event_set permet de spécifier le descripteur à surveiller et la fonction de rappel
(callback) à invoquer si un événement arrive sur le descripteur.
 event_add permet de donner cette spécification à la tâche principale de
libevent.
& %
' $
 event_dispatch démarre la tâche principale de libevent. Si un événement
arrive sur un descripteur, cette tâche appelle la fonction callback associée.

# 21

& %
L’exemple suivant présente un serveur (respectivement un client) inspiré de exemple_mkfifo_serveur.c
(respectivement exemple_mkfifo_client.c) étudié lors du cours « Communications inter-processus ». Mais

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 189


Éléments d’architecture client-serveur 5 Serveur mono-tâche gérant tous les clients à la fois

ici, le client envoie un message au serveur qui répond en signalant REQUETE_OK. Le client envoie ensuite le
même message au serveur qui répond à nouveau en signalant REQUETE_OK. Lorsque le client envoie le même
message pour la onzième fois, le serveur répond en signalant REQUETE_KO. Le client s’arrête alors.
serveurUtilisantLibevent.c illustre l’utilisation de libevent et la mémorisation du contexte du client
(en l’occurrence, le nombre de fois qu’il a déjà répondu à ce client).
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <ctype.h>
#include <error.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

#include <event2/event.h>
struct event_base *base;

#include "client-serveur.h"

typedef struct{
int nbReponsesAuClient;
int fdW;
char nomTube[TAILLE_POINT_ACCES];
struct event *ev;
} contexte_t;

#define NB_MAX_CLIENTS 600 // Vient du fait qu’on ne pourra pas ouvrir plus de 1024 descripteurs (cf. ulimit -a ou ulimit -n),
// et que chaque client requiert 2 descripteurs
contexte_t contexte[NB_MAX_CLIENTS];

#define NB_MAX_REPONSES 10

FILE *f = NULL;

void callbackTubeSpecifique(int fdR, short event, void *arg) {


contexte_t *pctxt = (contexte_t *)arg;
message_t reponse, requete;
int nbWrite, nbRead;

/* Lecture de la requete du client */


nbRead = read(fdR, &requete, sizeof(requete));
if (nbRead == sizeof(requete)) {
/* On traite la requete */
if (pctxt->nbReponsesAuClient < NB_MAX_REPONSES) {
pctxt->nbReponsesAuClient += 1;
reponse.typ = REQUETE_OK;
sprintf(reponse.infoSup.message, "Coucou pour la %2d-ieme fois", pctxt->nbReponsesAuClient);
//usleep(100000);
} else {
reponse.typ = REQUETE_KO;
}
nbWrite = write(pctxt->fdW, &reponse, sizeof(reponse));
if (nbWrite < sizeof(reponse))
error_at_line(EXIT_FAILURE, errno, __FILE__, __LINE__, "Pb ecriture sur tube nomme");
/* On reamorce l’evenement */
event_add(pctxt->ev, NULL);
} else {
/* Le client a ferme le tube : on fait le menage */
if (close(fdR) < 0)
error_at_line(EXIT_FAILURE, errno, __FILE__, __LINE__, "close");
if (close(pctxt->fdW) < 0)
error_at_line(EXIT_FAILURE, errno, __FILE__, __LINE__, "close");
if (unlink(pctxt->nomTube) < 0)
error_at_line(EXIT_FAILURE, errno, __FILE__, __LINE__, "unlink");
free(pctxt);
}
}

void traiterConnexion(message_t msg){


static int rangClient = 0;
message_t reponse;
int fdW, fdR;
int nbWrite;
contexte_t *pctxt = malloc(sizeof(contexte_t));

if (pctxt == NULL)
error_at_line(EXIT_FAILURE, 0, __FILE__, __LINE__, "malloc");

rangClient++;
printf("%6d-ieme client a traiter (point d’access client = %s)\n", rangClient, msg.infoSup.pointAcces);

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 190


Éléments d’architecture client-serveur 5 Serveur mono-tâche gérant tous les clients à la fois

/* On ouvre le tube nomme sur lequel le client pourra venir nous faire */
/* les demandes ultérieures. */
sprintf(pctxt->nomTube, "%s.%d", NOM_POINT_ACCES_SERV,rangClient);
if (mkfifo(pctxt->nomTube, S_IRUSR|S_IWUSR) < 0) {
if (errno != EEXIST)
error_at_line(EXIT_FAILURE, errno, __FILE__, __LINE__, "mkfifo pour requetes client-->serveur");
else
error_at_line(0, errno, __FILE__, __LINE__, "Tube nomme %s existe deja : on continue quand meme", pctxt->nomTube);
}

/* Connexion au point d’acces client et reponse */


fdW = open(msg.infoSup.pointAcces, O_WRONLY);
if (fdW == -1)
error_at_line(EXIT_FAILURE, errno, __FILE__, __LINE__, "Impossible de se connecter au tube %s", msg.infoSup.pointAcces);
reponse.typ = CONNEXION_OK;
strncpy(reponse.infoSup.pointAcces, pctxt->nomTube, TAILLE_POINT_ACCES);
reponse.infoSup.pointAcces[TAILLE_POINT_ACCES-1]=’\0’;
nbWrite = write(fdW, &reponse, sizeof(reponse));
if (nbWrite < sizeof(reponse))
error_at_line(EXIT_FAILURE, errno, __FILE__, __LINE__, "Pb ecriture sur tube nomme %s", msg.infoSup.pointAcces);

/* Ouverture de ce tube nomme */


fdR = open(pctxt->nomTube, O_RDONLY);
if (fdR == -1)
error_at_line(EXIT_FAILURE, errno, __FILE__, __LINE__, "open tube pour requetes client-->serveur");

/* On continue a preparer le contexte pour ce client. */


pctxt->nbReponsesAuClient = 0;
pctxt->fdW = fdW;

/* Definition de l’evenement sur le tube nomme principal */


pctxt->ev = event_new(base, fdR, EV_READ, callbackTubeSpecifique, pctxt);

/* Ajout aux evenements actifs, sans timeout */


event_add(pctxt->ev, NULL);
}

void callbackTubePrincipal(int fdR, short event, void *arg) {


message_t msg;
int nbRead;

nbRead = read(fdR, &msg, sizeof(msg));


if (nbRead != sizeof(msg))
error_at_line(EXIT_FAILURE, errno, __FILE__, __LINE__,
"read sur tube nomme %s KO",
NOM_POINT_ACCES_SERV);

/* On traite la requete */
traiterConnexion(msg);
}

int main() {
int fdR;
struct event *eventTubePrincipal;

/* Creation du tube nomme du serveur */


if (mkfifo(NOM_POINT_ACCES_SERV, S_IRUSR|S_IWUSR) < 0) {
if (errno != EEXIST)
error_at_line(EXIT_FAILURE, errno, __FILE__, __LINE__, "mkfifo(%s)",
NOM_POINT_ACCES_SERV);
else
error_at_line(0, errno, __FILE__, __LINE__, "mkfifo(%s) ==> ce tube existe deja\n"
"On suppose que c’est un pipe nomme et qu’on peut continuer le programme sans probleme",
NOM_POINT_ACCES_SERV);
}

/* Ouverture de ce tube nomme (equivalent a une ouverture de connexion).


NB : On l’ouvre en RDWR et pas en RDONLY,
car (cf. Blaess) : si nous ouvrons le client en lecture seule, à chaque
fois que le client se termine, la lecture du tube nomme va echouer
car le noyau detecte une fermeture de fichier. En demandant un tube
en lecture et en ecriture, nous evitons ce genre de situation, car il
reste toujours au moins un descripteur ouvert sur la sortie
*/
fdR = open(NOM_POINT_ACCES_SERV, O_RDWR);
if (fdR == -1)
error_at_line(EXIT_FAILURE, errno, __FILE__, __LINE__, "open(%s)",
NOM_POINT_ACCES_SERV);

/* Initialisation de la librairie event */


base = event_base_new();
if (!base) {
error_at_line(EXIT_FAILURE, errno, __FILE__, __LINE__, "Could not initialize libevent!\n");
}

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 191


Éléments d’architecture client-serveur 5 Serveur mono-tâche gérant tous les clients à la fois

/* Definition de l’evenement sur la file de messages principale */


eventTubePrincipal = event_new(base, fdR, EV_READ|EV_PERSIST, callbackTubePrincipal, NULL);

/* Ajout aux evenements actifs, sans timeout */


event_add(eventTubePrincipal, NULL);

/* Attente d’evenements */
event_base_dispatch(base);

return EXIT_SUCCESS;
}

Pour tester l’exécution de plusieurs clients en parallèle, on peut exécuter la


Noter que client.c n’a aucune conscience que le serveur utilise libevent.
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <ctype.h>
#include <error.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

#include "client-serveur.h"

int main(int argc, char *argv[]) {


message_t msgConnexion, msgRepConnexion, msgRequete, msgRepRequete;
int fdW, fdR;
int nbRead, nbWrite;
char *idClient;

if (argc != 2) {
printf("Usage = client identifiantClient\n");
exit(EXIT_FAILURE);
}

idClient = argv[1];

/* Préparation du message de connexion */


msgConnexion.typ = CONNEXION;
sprintf(msgConnexion.infoSup.pointAcces, "./serveur-client.%d", getpid());

/* Création du tube nommé du client */


if (mkfifo(msgConnexion.infoSup.pointAcces, S_IRUSR|S_IWUSR) < 0) {
if (errno != EEXIST)
error_at_line(EXIT_FAILURE, errno, __FILE__, __LINE__, "mkfifo(%s)", msgConnexion.infoSup.pointAcces);
else
error_at_line(0, errno, __FILE__, __LINE__, "mkfifo(%s) ==> ce tube existe deja\n"
"On suppose que c’est un pipe nomme et qu’on peut continuer le programme sans probleme",
msgConnexion.infoSup.pointAcces);
}

/* Envoi de la requête vers le serveur */


printf("Client %6s : Connexion au serveur...\n", idClient);
fdW = open(NOM_POINT_ACCES_SERV, O_WRONLY);
if (fdW == -1)
error_at_line(EXIT_FAILURE, errno, __FILE__, __LINE__, "open(%s)", NOM_POINT_ACCES_SERV);
nbWrite = write(fdW, &msgConnexion, sizeof(msgConnexion));
if (nbWrite < sizeof(msgConnexion))
error_at_line(EXIT_FAILURE, errno, __FILE__, __LINE__, "pb write sur tube nomme");

/* On n’aura pas d’autres messages à envoyer sur ce tube. On peut donc */


/* le fermer */
if (close(fdW) < 0)
error_at_line(EXIT_FAILURE, errno, __FILE__, __LINE__, "close");

/* On ouvre seulement maintenant le tube pour écouter la réponse, sinon */


/* on serait bloqué jusqu’à ce que le serveur ouvre le pipe en écriture */
/* pour envoyer sa réponse. Si on ouvrait le tube avant d’envoyer la */
/* requête, le serveur ne recevrait jamais la requête et donc n’ouvrirait*/
/* jamais le tube ==> On serait bloqué indéfiniment. */
fdR = open(msgConnexion.infoSup.pointAcces, O_RDONLY);
if (fdR == -1)
error_at_line(EXIT_FAILURE, errno, __FILE__, __LINE__, "open(%s)", msgConnexion.infoSup.pointAcces);

/* On lit la réponse */
nbRead = read(fdR, &msgRepConnexion, sizeof(msgRepConnexion));

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 192


Éléments d’architecture client-serveur 5 Serveur mono-tâche gérant tous les clients à la fois

if (nbRead != sizeof(msgRepConnexion))
error_at_line(EXIT_FAILURE, errno, __FILE__, __LINE__, "Communication avec le serveur probablement rompue\n");

/* On analyse la réponse */
if (msgRepConnexion.typ != CONNEXION_OK)
error_at_line(EXIT_FAILURE, 0, __FILE__, __LINE__, "Le serveur m’a renvoye un type de message inconnu : %d", msgRepConnexion.typ);

printf("Client %6s : Connecte !\n", idClient);

/* On fait "Coucou" au serveur jusqu’a ce qu’il nous reponde REQUETE_KO */


printf("Client %6s : Envoi de requetes vers le serveur...\n", idClient);
fdW = open(msgRepConnexion.infoSup.pointAcces, O_WRONLY);
if (fdW == -1)
error_at_line(EXIT_FAILURE, errno, __FILE__, __LINE__, "open(%s)", msgRepConnexion.infoSup.pointAcces);

msgRepConnexion.typ = REQUETE;
strncpy(msgRequete.infoSup.message,"Coucou",sizeof(msgRequete.infoSup.message));
do {
nbWrite = write(fdW, &msgRequete, sizeof(msgRequete));
if (nbWrite < sizeof(msgRequete))
error_at_line(EXIT_FAILURE, errno, __FILE__, __LINE__, "pb write sur tube nomme");
nbRead = read(fdR, &msgRepRequete, sizeof(msgRepRequete));
if (nbRead != sizeof(msgRepRequete))
error_at_line(EXIT_FAILURE, errno, __FILE__, __LINE__, "Communication avec le serveur probablement rompue\n");
if (msgRepRequete.typ == REQUETE_OK) {
printf("Client %6s : Nouvelle reponse du serveur = %s\n", idClient, msgRepRequete.infoSup.message);
}
usleep(10000);
} while (msgRepRequete.typ == REQUETE_OK);
printf("Client %6s : Envoi de requetes termine !\n", idClient);

/* On n’aura pas d’autres messages à envoyer sur ce tube. On peut donc */


/* le fermer */
if (close(fdW) < 0)
error_at_line(EXIT_FAILURE, errno, __FILE__, __LINE__, "close");

/* On ferme le tube côté réception */


if (close(fdR) < 0)
error_at_line(EXIT_FAILURE, errno, __FILE__, __LINE__, "close");

/* On détruit le tube nommé du client (puisque ce tube nommé ne servira */


/* plus jamais. */
if (unlink(msgConnexion.infoSup.pointAcces) < 0)
error_at_line(EXIT_FAILURE, errno, __FILE__, __LINE__, "unlink");

return EXIT_SUCCESS;
}

/********************/
/* client-serveur.h */
/********************/

/* Declarations communes au client et au serveur */

/** Point d’acces permettant sur lequel le serveur est tout le


* temps a l’ecoute.
*/
#define NOM_POINT_ACCES_SERV "./client-serveur"

/** Taille maximum d’un point d’acces */


#define TAILLE_POINT_ACCES 128

/** Taille maximum d’une requete ou de sa reponse */


#define TAILLE_REQUETE_REPONSE 256

/**
* \enum typMessage_t
* \brief Type de message circulant entre le client et le serveur.
*
* typMessage_t définit les types de message circulant dans
* les sens client-->serveur et serveur-->client
*/
typedef enum
{
CONNEXION, /*!< (sens client-->serveur) Demande de connexion. */
CONNEXION_OK, /*!< (sens serveur-->client) Demande acceptee. */
REQUETE, /*!< (sens client-->serveur) Requete. */
REQUETE_OK, /*!< (sens serveur-->client) Reponse positive a la requete. */
REQUETE_KO /*!< (sens serveur-->client) reponse negative a la requete. */
}
typMessage_t;

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 193


Éléments d’architecture client-serveur 5 Serveur mono-tâche gérant tous les clients à la fois

/**
* \struct message_t
* \brief Message circulant entre le client et le serveur.
*
* message_t définit les structures de message circulant dans les
* sens client-->serveur et serveur-->client
*/
typedef struct
{
typMessage_t typ; /*!< Type de message */
union {
char pointAcces[TAILLE_POINT_ACCES]; /*!< Nom du point d’acces. */
char message[TAILLE_REQUETE_REPONSE]; /*!< Message associe a la requete ou la reponse. */
} infoSup; /*!< Info supplementaire associee a ce message. */
}
message_t;

Voici plusieurs manipulations expériences à faire avec cet exemple :

1. Tester l’exécution de 600 clients en parallèle avec la ligne shell suivante : for((i=1;i<=600;i+=1));
do (./client $i &);done

2. Arrêtez le serveur. Décommentez la ligne usleep(100000) dans le fichier


serveurUtilisantLibevent.c. Recompilez et relancez le serveur. Lancer l’exécution de 600
clients en parallèle. Les performances du serveur sont catastrophiques. Pourquoi ?
Si la ligne usleep(100000) avait été une entrée-sortie lente (par exemple, un accès en lecture à un
fichier qui aurait pris 100 ms), on aurait pu préserver le parallélisme entre les clients sans pour autant
prendre l’architecture « Serveur avec autant de tâches que de clients ». Comment ?

3. En fait, il existe un nombre maximum de clients simultanés.


Pour l’observer, dans serveurUtilisantLibevent.c, remettez en commentaire la ligne
usleep(100000). Dans client.c, changez le usleep(10000) en sleep(100) (attention, sleep,
pas usleep). Arrêtez et redémarrez le serveur. Enfin lancez 600 clients en parallèle.
Le serveur affiche :

509-ieme client a traiter (point d’access client = ./serveur-client.15427)


./serveurUtilisantLibevent:serveurUtilisantLibevent.c:88: Impossible de se connecter
au tube ./serveur-client.15427: Too many open files

Avant d’aller plus loin, commencez par taper la commande killall -9 client (pour tuer tous les
clients qui seraient en attente sur leur tube nommé), puis la commande make clean (pour nettoyer
tous les tubes nommés qui n’ont pas été effacés par les clients du fait qu’ils sont morts brutalement).
L’erreur du serveur est due au fait que le noyau a été configuré pour autoriser au maximum 1024
descripteurs ouverts simultanément par processus. La commande shell ulimit -a (ou bien ulimit
-n pour avoir seulement cette limite) nous rappelle cette limite. De ce fait, comme le serveur ouvre
2 tubes nommés par client connectés, il atteint rapidement cette limite (508 ∗ 2 = 1016 + stdin +
stdout + stderr + descripteurP ourLeP ointDAccesP rincipal [il en manque 1024 − 1020 = 4, mais le
rédacteur de ce poly n’a pas d’idées. . .]).
Que proposez-vous pour qu’au niveau du serveur, on n’atteigne pas cette limite ?
La première idée qui vient à l’esprit est d’utiliser le paradigme de synchronisation entre processus de
type Cohorte : le serveur utilise un sémaphore nbClientsAutorises initialisé à 508 (puisque le serveur
plante au delà de 508 clients connectés simultanément), fait P(nbClientsAutorises) à chaque nouveau
client qui se connecte au serveur et V(nbClientsAutorises) à chaque client qui se déconnecte. Dans
le cas présent, ce n’est pas une bonne idée (alors que c’est une excellente idée lorsqu’un travaille avec
des enfants/threads. Selon vous, pourquoi ?
Que proposez-vous à la place ?

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 194


Éléments d’architecture client-serveur 5 Serveur mono-tâche gérant tous les clients à la fois
' $
5.3 Analyse

 Avantages
 Adapté au cas de connexions longues avec de longs temps d’inactivité
 Inconvénients
# 22
 Complexité
 Risques de surcharge
 Le traitement d’une requête ne doit pas comprendre des appels-systèmes lents (à
moins que ce ne sois des entrées-sorties) : le serveur se comporterait alors
comme un serveur mono-processus gérant un client à la fois

& %

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 195


Éléments d’architecture client-serveur
' $
6 Conclusion

 Ce cours a présenté 4 architectures logicielles de serveur. Elles mettent en œuvre


tous les mécanismes système étudiés au cours de cette U.V.

 Quelle est la meilleure architecture ?


# 23 La réponse à cette question dépend de critères dont l’importance varie selon le
projet considéré :
 Nombre maximum de clients simultanés à gérer,
 Durée de traitement d’une requête au niveau du serveur,
 Expérience des développeurs,
 Importance de la dimension Green Computing,
 ...

& %

À tort ou à raison, le Green Computing est une notion clairement en vogue actuellement. D’un point de
vue programmation et architecture logicielle, il semble que cette notion consiste essentiellement à optimiser
au maximum l’efficacité de l’application développée.
On peut y ajouter le besoin qu’une application consomme le moins de ressources possibles et surtout
soit en mesure de diminuer son utilisation de ressources en périodes creuses d’utilisation. Ainsi, pour une
application client-serveur, on peut imaginer que l’application soit très utilisée à des heures de pointe et très
peu le reste du temps. Dans ce cas, l’architecture « Serveur avec N tâches gérant tous les clients », ces
N tâches étant stockés dans un pool d’enfants disponibles, est moins Green que l’architecture « Serveur
mono-tâche gérant N clients à la fois ». En effet, en heures creuses, le pool d’enfants disponibles reste alloué
alors qu’il ne sert pas. On consomme donc de la RAM inutilement.
Bibliographie du chapitre
[Kegel, 2006] Kegel (2006). The C10K problem. http ://www.kegel.com/c10k.html. Last accessed in May
2011.
[Leitner, 2003] Leitner (2003). Scalable Network Programming Or : The Quest for a Good Web Server (That
Survives Slashdot). http ://bulk.fefe.de/scalable-networking.pdf. Last accessed in May 2011.

TELECOM SudParis — Michel Simatic — Avril 2016 — module CSC4508/M2 196


Récapitulatif des outils

François Trahay

module CSC4508/M2

Avril 2016

197
Récapitulatif des outils
' $
Plan du document

2 Infos sur l’architecture matérielle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4


#2 2 Debugging . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
4 Entrées/Sorties – Bases de données . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
4 IPC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
5 Traitement de tâches parallèles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7

& %

' $
1 Infos sur l’architecture matérielle

a
 HWLoc
 Fournit des informations sur la topologie de la machine (hyperthreads, cœurs,
nœuds NUMA, caches partagés, etc.)
#3  Outils permettant de placer les threads/processus sur la machine
b
 PAPI
 Donne accès aux compteurs matériels du processeur (nombre d’instructions
exécutées, nombre de cache miss, etc.)

a. https ://www.open-mpi.org/projects/hwloc/
b. http ://icl.cs.utk.edu/projects/papi/wiki/Main_Page
& %

TELECOM SudParis — François Trahay — Avril 2016 — module CSC4508/M2 198


Récapitulatif des outils
' $
2 Debugging
a
 Compiler avec Clang
 pour bénéficier de son analyse statique pour repérer certains bugs subtils.
 GDB
 Pour débugguer un programme : afficher l’instruction ayant causé un SIGSEGV,
observer les valeurs de variables, exécution pas à pas, etc.
 Pour faire du reverse debugging
#4  valgrind
 Pour détecter les problèmes mémoire (variables non initialisées, accès à de la
mémoire non allouée, etc.)
 Informations sur l’utilisation des caches (accès en lecture/écriture aux caches ou
en dehors des caches)
b
 Google Breakpad
 Système de crash-reporting multi plateforme
a. http://clang.llvm.org/
b. https://chromium.googlesource.com/breakpad/breakpad/
& %

Le reverse debugging est une fonctionnalité méconnue, mais très puissante de gdb. Elle per-
met d’exécuter une application en mode pas à pas, mais en reculant. Ceci permet, à partir
d’une erreur de segmentation (par exemple), de revenir en arrière pour comprendre pourquoi le
pointeur ptr est NULL (par exemple). Vous trouverez un tutoriel sur cette fonctionnalité ici :
http://jayconrod.com/posts/28/tutorial-reverse-debugging-with-gdb-7
Pour aller plus loin dans l’utilisation de gdb, je vous conseille la vidéo de Greg Law lors de la CppCon 2015
“Give me 15 minutes & I’ll change your view of GDB” : https://www.youtube.com/watch?v=PorfLSr3DDI

' $
3 Entrées/Sorties – Bases de données

 Bases de données
 NDBM
 GDBM
#5
 Oracle Berkeley DB
 Ces bases de données permettent de stocker des fichiers sous la forme clé/valeur
 Gestion de données locales au format SQL
a
 SQLite

a. https://www.sqlite.org/
& %

TELECOM SudParis — François Trahay — Avril 2016 — module CSC4508/M2 199


Récapitulatif des outils
' $
4 IPC

 ZeroMQ, nanomsg
#6
 RabbitMQ, NATS, Apache Kafka
 Pour échanger des messages entre processus sur une ou plusieurs machines, y
compris avec des architectures / langages différents

& %

' $
5 Traitement de tâches parallèles

 GNU parallel
#7
 Celery
 Task-spooler
 Permettent d’exécuter des listes de tâches en parallèle

& %

TELECOM SudParis — François Trahay — Avril 2016 — module CSC4508/M2 200


Bibliographie

Frédérique Silber-Chaussumier et Michel Simatic

module CSC4508/M2

Avril 2016

201
Bibliographie
' $
Plan du document

2 Concepts des systèmes d’exploitation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4


2 Ouvrages dédiés à Unix . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
#2 4 Documents dédiés à Linux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
4 Documents spécifiques threads . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
5 Divers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
6 Bibliographie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13

& %

' $
1 Concepts des systèmes d’exploitation
 [Kaiser, 2006] : est un cours du CNAM qui évoque les concepts évoqués dans
CSC4508/M2, mais sous la forme d’un document rédigé. Disponible sur Internet.
 [Silberschatz et al., 2002] : traite de la conception des systèmes d’exploitation. Il
aborde les différentes problématiques qui apparaissent lorsqu’on veut créer un
système d’exploitation efficace et détaille les différentes manières de résoudre ces
problèmes, leurs avantages et leurs inconvénients. Enfin, pour chaque grande
thématique (ordonnancement de l’allocation CPU, allocation de la mémoire, système
#3
de fichiers distribués. . .), ce livre explique comment les systèmes d’exploitation
existants (Linux, Solaris, Windows 2000 entre autres) ont résolu ces problèmes.
 [Tannenbaum, 2001] : une référence dans le domaine de la conception des systèmes
d’exploitation. Peut-être un peu moins complet que le [Silberschatz et al., 2002]
 [Bloch, 2008] : présente l’histoire des systèmes d’exploitation, leur fonctionnement
et les enjeux.
 [Downey, 2005] : une référence pour tout ce qui concerne les paradigmes de
synchronisation.
& %

[Downey, 2005] a un seul défaut : il utilise wait et signal en lieu et place de P() et V() ; cela peut prêter
à confusion avec les primitives wait et signal étudiées pendant le cours sur les threads.

TELECOM SudParis — Frédérique Silber-Chaussumier et Michel Simatic — Avril 2016 — module CSC4508/M2 202
Bibliographie
' $
2 Ouvrages dédiés à Unix

 [Leffler and Kuzick, 1997] : présentation des concepts qui ont guidé l’élaboration du
système UNIX BSD par deux maîtres du sujet
#4
 [Rifflet, 1995] : mon ancien livre de chevet avant de connaître [Blaess, 2002]
 [Stevens, 1992] : considéré comme une bible
 [UNIX, 2003]) et [GNU, 2003] : man et info sont des sources d’informations
inestimables.

& %

' $
3 Documents dédiés à Linux

 [Blaess, 2002] : un livre (voire une référence) très complet sur la programmation
système sous Linux
 [Mitchell et al., 2001] : un bon livre (désormais en français !) pour développer des
applications avancées (ou non) sous Linux
 [linuxCrossRef, 2005] : pour se promener aisément dans les sources Linux
#5  [Drepper, 1999] : pour ceux qui s’intéressent aux problèmes d’optimisation
 [HowTo, 2005] : l’endroit où chercher des réponses à des questions classiques sur
Linux
 [linuxCenter, 2005] : un index thématique de pages Web consacrées au système
d’exploitation Linux
 [linuxMag, 2005] : un magazine en ligne sur Linux
 [linuxDoc, 2005] : point central pour la documentation Linux (notamment les FAQ)
& %

TELECOM SudParis — Frédérique Silber-Chaussumier et Michel Simatic — Avril 2016 — module CSC4508/M2 203
Bibliographie
' $
4 Documents spécifiques threads

 [Bryant and O’Hallaron, 2003] : cf. commentaire


 [Robbins and Robbins, 2003] : cf. commentaire
 [Blaess, 2002] : cf. commentaire
 [Bovet and Cesati, 2003]

#6  Threads spécifiquement
 [Nichols et al., 1996] : décrit comment faire des programmes à base efficaces de
threads POSIX
 [Lewis et al., 1995] : présente les principes généraux des threadset, notamment,
leur implantation sous Linux, Windows. . .
 [Butenhof, 1997] : décrit comment faire des programmes efficaces à base de
threads POSIX
 [Zignin, 1996] : décrit les principes généraux des threads. Commence à dater,
mais présente l’avantage d’être disponible à la bibliothèque de TSP
& %
' $
 [Lampkin, 2006] : une semi-FAQ sur les threads POSIX, disponible sur le Web
 [Williams, 2012] : décrit le multi-threading en C++ 11, disponible à la
bibliothèque de TSP

#7

& %

Le cours [Bryant and O’Hallaron, 2003] est très bien fait. Il part des couches basses pour expliquer les
différentes notions : les images mémoire des processus pour expliquer la notion de thread, les instructions
liées à une lecture et à une écriture pour expliquer les problèmes de synchronisation avec pour exemple le
compteur.
Le livre [Robbins and Robbins, 2003] est très progressif. Il explique notamment le sens de thread-safe avec
strtok() comme exemple (p. 39) et y revient dans un chapitre sur les threads POSIX avec comme exemples
strerr() et errno (p. 432). Les threads sont expliqués sur trois chapitres avec beaucoup d’exemples, no-
tamment des exemples détaillés de gestion des signaux avec des threads. Introduit aussi des primitives non
implantées sur Linux comme les primitives pthread_rw_*.
Le livre [Blaess, 2002] fournit beaucoup d’explications sur l’implantation des threads sous Linux même
s’il est déjà un peu ancien par exemple ce qui concerne la directive _REENTRANT est déjà obsolète.

TELECOM SudParis — Frédérique Silber-Chaussumier et Michel Simatic — Avril 2016 — module CSC4508/M2 204
Bibliographie
' $
5 Divers

 Développement d’un serveur efficace et performant


 [Leitner, 2003] fournit de précieuses indications sur le développement d’un
serveur efficace.
#8  [Kegel, 2006] résume l’ensemble des stratégies utilisables pour qu’un serveur gère
de nombreuses entrées-sorties

 Symbian OS
 [Morris, 2007] présente Symbian OS.

& %

[Morris, 2007] est mis ici par curiosité intellectuelle. En effet, après l’annonce par Nokia de l’utilisation
de Windows Mobile pour ses téléphones, le projet Symbian OS a été arrété.

' $
6 Bibliographie
[Blaess, 2002] Blaess, C. (2002). Programmation système en C sous Linux : signaux,
processus, threads, IPC et sockets. Eyrolles, Paris, Paris, France.
[Bloch, 2008] Bloch, L. (2008). Les systèmes d’exploitation des ordinateurs : histoire,
fonctionnement, enjeux. Vuilbert, http ://greenteapress.com/semaphores/.
http ://www.laurent-bloch.org/spip.php ?article13.
[Bovet and Cesati, 2003] Bovet, D. P. and Cesati, M. (2003). Understanding the Linux
#9 kernel. O’Reilly, 2 edition.
[Bryant and O’Hallaron, 2003] Bryant, R. E. and O’Hallaron, D. R. (2003). Computer
Systems : A Programmer’s Perspective. Prentice Hall.
[Butenhof, 1997] Butenhof, D. (1997). Programming with POSIX Threads. Addison
Wesley.
[Downey, 2005] Downey, A. B. (2005). The Little Book of Semaphores. GreenTeaPress,
http ://greenteapress.com/semaphores/. Version 2.1.5.
[Drepper, 1999] Drepper, U. (1999). Optimizing applications with gcc & glibc.
Technical report, RedHat. http ://people.redhat.com/drepper/optimtut.ps.gz.
& %

TELECOM SudParis — Frédérique Silber-Chaussumier et Michel Simatic — Avril 2016 — module CSC4508/M2 205
Bibliographie
' $
[GNU, 2003] GNU (2003). Pages d’Info de GNU. GNU/Linux, Distribution RedHat 7.3
edition.
[HowTo, 2005] HowTo (2005). How-to linux. http ://www.traduc.org. Last accessed in
May 2011.
[Kaiser, 2006] Kaiser, C. (2006). Systèmes informatiques : les structures et paradigmes
des plates-formes informatiques.
http ://deptinfo.cnam.fr/Enseignement/CycleProbatoire/SRI/Systemes.

# 10 [Kegel, 2006] Kegel (2006). The C10K problem. http ://www.kegel.com/c10k.html.


Last accessed in May 2011.
[Lampkin, 2006] Lampkin (2006). POSIX Threads : Semi-FAQ Revision 5.2.
http ://www.cognitus.net/html/howto/pthreadSemiFAQ.html. Last accessed in May
2011.
[Leffler and Kuzick, 1997] Leffler, S. and Kuzick, J.-M. (1997). The design of the
4.4BSD.
[Leitner, 2003] Leitner (2003). Scalable Network Programming Or : The Quest for a
Good Web Server (That Survives Slashdot).
& %

' $
http ://bulk.fefe.de/scalable-networking.pdf. Last accessed in May 2011.
[Lewis et al., 1995] Lewis, B., Berg, D., and Lewis, B. (1995). Threads Primer : A
Guide to Multithreaded Programming. Prentice Hall.
[linuxCenter, 2005] linuxCenter (2005). Linux center. http ://www.linux-center.org/fr.
Last accessed in May 2011.
[linuxCrossRef, 2005] linuxCrossRef (2005). Cross-referencing linux.
http ://lxr.linux.no/source. Last accessed in May 2011.
# 11 [linuxDoc, 2005] linuxDoc (2005). The Linux Documentation Project.
http ://www.tldp.org/. Last accessed in May 2011.
[linuxMag, 2005] linuxMag (2005). Linux magazine. http ://www.linux-mag.com/. Last
accessed in May 2011.
[Mitchell et al., 2001] Mitchell, M., Oldham, J., and Samuel, A. (2001).
Programmation Avancée sous Linux (Advanced Linux Programming). New Riders
Publishing, http ://www.advancedlinuxprogramming-fr.org.
[Morris, 2007] Morris, B. (2007). The Symbian OS architecture sourcebook : Design
and evolution of a mobile phone OS. Wiley.
& %

TELECOM SudParis — Frédérique Silber-Chaussumier et Michel Simatic — Avril 2016 — module CSC4508/M2 206
Bibliographie
' $
[Nichols et al., 1996] Nichols, B., Buttlar, D., and Farrell, J. (1996). Pthreads
Programming : A POSIX Standard for Better Multiprocessing. O’Reilly and
Associates.
[Rifflet, 1995] Rifflet, J.-M. (1995). La programmation sous UNIX, 3è Édition.
Ediscience International, Paris, France.
[Robbins and Robbins, 2003] Robbins, K. A. and Robbins, S. (2003). UNIX SYSTEMS
Programming : communication, Concurrency and Threads. Prentice Hall.

# 12 [Silberschatz et al., 2002] Silberschatz, A., Baer, P., and Gagne, G. (2002). Operating
System Concepts. John Wiley & Sons Inc., 6th edition.
[Stevens, 1992] Stevens, W. (1992). Advanced Programming in The UNIX
Environment. Addison-Wesley, Reading, Massachusetts, USA.
[Tannenbaum, 2001] Tannenbaum, A. (2001). Modern Operating Systems. Hardcover,
2nd edition.
[UNIX, 2003] UNIX (2003). Pages du manuel en ligne UNIX. GNU/Linux, Distribution
RedHat 7.3 edition.
[Williams, 2012] Williams, A. (2012). C++ Concurrency in action, Practical
& %
' $
Multithreading. Manning Publications, first edition.
[Zignin, 1996] Zignin, B. (1996). Techniques du multithread : du parallèlisme dans les
processus. Hermès.

# 13

& %

TELECOM SudParis — Frédérique Silber-Chaussumier et Michel Simatic — Avril 2016 — module CSC4508/M2 207
Bibliographie

TELECOM SudParis — Frédérique Silber-Chaussumier et Michel Simatic — Avril 2016 — module CSC4508/M2 208
Index
Symbols cohérence de cache, 176
_aligned_free, 45 Cohorte, see Synchronisation, Cohorte
_aligned_malloc, 42, 45 Conditions, see Moniteurs
__attribute__ Consommateurs, see Synchronisation, Produc-
__attribute__ teurs/Consommateurs
aligned, 42 Contrôle de fichier, see Fichier, Contrôle de
__attribute__ creat, 58
packed, 42 Création de fichier, see Fichier, Création de
-fpackstruct, 42 Création de processus, see Processus, Création de

A D
abort, 16 D-Bus, 91
access, 74 Défauts de page, 43
Adresse logique, 32 Déplacement dans fichier, see Fichier, Déplacement
aio_error, 65 dans
aio_read, 65 Descripteur, 61, 72
aio_return, 65 Écriture, 59
aio_write, 65 Lecture, 59
Algorithme de la seconde chance, see Seconde chance, Descripteur de segments, 35
Algorithme de la dup, 61
Alignement, 42 dup2, 61
alloca, 48 Duplication de descripteur, 61
Appel procédural entre processus, see Synchronisa-
tion, Appel procédural entre processus E
Appels systèmes, 19 EAGAIN, 65
Architecture client/serveur, 178 Écriture fichier, see Fichier, Écriture
argc, 79 Écriture sur descripteur, see Descripteur, Écriture
argv, 79 Écroulement, 33
assert, 16 EINTR, 65
atexit, 82 electric fence, 51
Attente de processus, see Processus, Attente de environ, 79
AVX, see Instructions vectorielles SSE Environnement des processus, see Processus, Envi-
ronnement des
B Envoi de signal, see Synchronisation, Envoi de signal
Belady, 37 envp, 79
Berkeley DB, see Oracle Berkeley DB epoll, 189
bless, 56 Erreur système, 16
Blocage des entrées-sorties, 65 errno, 15, 16, 20, 138, 141
brk, 49 error, 16
error_at_line, 16
C ERROR_AT_LINE, 16
cache, 23 Espace vital, 33
Mémoire, 29 event_add, 189
Cache, 40 event_dispatch, 189
d’adresses, 33 event_set, 189
cache fully-associatif, 23 EWOULDBLOCK, 65
cache hit, 23 Exclusion mutuelle, see Synchronisation, Exclusion
cache miss, 23 mutuelle
cache snooping, 176 execl, 83
calloc, 45 execle, 83
Celery, 7 execlp, 83
Clé, see IPC, Clé Exécution nouveau programme, see Processus, Exé-
clearerr, 72 cution nouveau programme
Client serveur, see Architecture client/serveur execv, 83
close, 58, 95 execve, 83
closedir, 75 execvp, 83

209
exit, 82 getc, 68
getchar, 68
F getegid, 80
fallocate, 59 getenv, 79
fclose, 68 geteuid, 80
fcntl, 62 getgid, 80
feof, 72 getpgid, 80
Fermeture fichier, see Fichier, Fermeture getpid, 80
ferror, 72 getppid, 80
fflush, 71 getrlimit, 43
fgetc, 68 getrusage, 23, 39
fgets, 68 gets, 68
Fichier getuid, 80
Conseil, 64 GNU Parallel, 7
Contrôle de, 62, 71 Google Breakpad, 4
Création de, 58
Déplacement dans, 65, 72 H
Écriture, 59 hexdump, 56
Écriture de, 69, 70 HWLoc, 3
Fermeture de, 58, 68 Hyperthreading, 169
I-nœuds, 73, 74 Hypertransport, 171
Lecture, 59
Lecture de, 68, 69
I
ICE, 91
NFS, 76
Inclusion, see Principe d’inclusion
Offset, see Fichier, Déplacement dans
Informations concernant un processus, see Processus,
Ouverture de, 58, 68
Informations concernant un
Projection en mémoire, 72
I-nœuds, see Fichier, I-nœuds
Répertoire, 75
Instruction Level Parallelism (ILP), 166
Tampon du, 71
Instructions vectorielles, 167
Verrouillage de, 62
Instructions vectorielles AVX, see Instructions vecto-
FIFO, see First In First Out
rielles SSE
File de messages, 91
Instructions vectorielles SSE, 43
fileno, 72
insure++, 50, 51
Files de messages, 91
Intel, 29, 33
Fin de processus, see Processus, Terminaison de
Interblocage, see Synchronisation,Interblocage
FINUFO, see Seconde chance, Algorithme de la
Intergiciel, 189
First In First Out, 37
Inter Process Communication, see IPC
First In Not Used First Out, see Seconde chance, Al-
interruption, 65
gorithme de la
Interruption, 21
fopen, 68
Inversion de priorité, 24
fork, 81
IPC
fprintf, 70
Clé, 90
fputc, 69
Files de messages, see Files de messages
fputs, 69
Mémoire partagée, see Mémoire, partagée
fread, 68
free, 45 K
Algorithmes, 47 Kafka, 6
fscanf, 69
fseek, 72 L
fsync, 58, 71 Least Frequently Used, 37
ftruncate, 95 Least Recently Used, 37
fwrite, 69 Lecteurs/Rédacteurs, see Synchronisation,
Lecteurs/Rédacteurs
G Lecture de fichier, see Fichier, Lecture
gdb, 4 Lecture sur descripteur, see Descripteur, Lecture
gdb, 54 Lenteur des entrées-sorties, 65
GDBM, 5, 57, 65, 72 LFU, see Least Frequently Used

210
libevent, 66, 189 O
ligne de cache, 23 O_APPEND, 58
Limite, see Voir ulimit O_ASYNC, 62
LRU, see Least Recently Used O_CREAT, 58
lseek, 65 Offset, see Fichier, Déplacement dans
lstat, 75 offsetof, 43
O_NDELAY, 58
M O_NONBLOCK, 58, 62, 65
main, 79 open, 58
majeurs opendir, 75
Défauts de page, see Défauts de page Oracle Berkeley DB, 5, 57, 65, 72
malloc, 45, 48, 49 O_RDONLY, 58
Algorithmes, 47 Ordonnancement
mallopt, 45 Politique d’, 22
Memcached, 188 Priorité
memset, 45 dynamique, 25
Mémoire, 35 statique, 22, 25
partagée, 95 Priorité d’ordonnancement, 25
memory wall, 20 standard, 25
Middleware, 189 O_RDWR, 58
mineurs O_SYNC, 58, 71
Défauts de page, see Défauts de page O_TRUNC, 58
mkdir, 75 Ouverture fichier, see Fichier, Ouverture
mkfifo, 87 O_WRONLY, 58
mknod, 87
P
mlock, 43
P, 98, 101, 108, 143
mlockall, 43
Pages
mmap, 49, 72, 95
Utilisation des, 35, 39
Moniteurs, 123
Pagination, 31, 35, 43
mpatrol, 51
PAPI, 3, 12
mprotect, 54
Passage de témoin, see Synchronisation, Passage de
mq_close, 91 témoin
mq_open, 91 perror, 16, 20
mq_receive, 91 pipe, 85
mq_send, 91 Pipeline, 164
mq_unlink, 91 Politique
mtrace, 51 d’ordonnancement, see Ordonnancement, Poli-
munlock, 43 tique d’
munlockall, 43 poll, 189
munmap, 72, 95 popen, 86
Portable Operating System Interface, see POSIX
N POSIX, 91, 130
nanomsg, 6 Sémaphores, see Sémaphores,POSIX
NATS, 6 posix_fadvise, 64
NDBM, 5, 57, 65, 72 posix_fallocate, 59
Network File System, see Fichier, NFS posix_memalign, 42, 45
Network Information Service, 76 POSIX Pthreads, 132
NFS, see Fichier, NFS Principe d’inclusion, 29
Nginx, 188 printf, 70
nice, 25 Priorité
NIS, 76 dynamique, see Ordonnancement, Prorité dy-
Non Uniform Memory Architecture, 171 namique
notify, 123 statique, see Ordonnancement, Prorité statique
notifyAll, 123 Processeur, 162
Noyau, 19 Processus
NUMA, 171 Attente de, 82

211
Création de, 81 Q
Environnement des, 79 QPI, 171
Exécution nouveau programme, 83 Quantum de temps, 22, 25
Informations concernant un, 80 Quick Path Interconnect, 171
Terminaison de, 82
Producteurs/Consommateurs, see Synchronisation, R
Producteurs/Consommateurs RabbitMQ, 6
Programmation événementielle, 189 Rational Purify, 51
Projection de fichier en mémoire, see Fichier, Projec- read, 59
tion en mémoire readddir, 75
ps, 23, 35, 39 readlink, 75
pthread_atfork, 160 readv, 59
pthread_attr_destroy, 134 realloc, 45
pthread_attr_getdetachstate, 134 Rédacteurs, see Synchronisation,
pthread_attr_getschedparam, 135 Lecteurs/Rédacteurs
pthread_attr_getschedpolicy, 135 rename, 74
pthread_attr_getstackaddr, 135 Rendez-vous entre deux processus, see Synchronisa-
pthread_attr_getstacksize, 135 tion, Rendez-vous entre deux processus
pthread_attr_init, 134 Répertoire, see Fichier, Répertoire
pthread_attr_setdetachstate, 134 Retour
pthread_attr_setschedparam, 135 d’appel système, 15
pthread_attr_setschedpolicy, 135 de fonction, 15
pthread_attr_setstackaddr, 135 return, 15
rewinddir, 75
pthread_attr_setstacksize, 135
rmdir, 75
pthread_cancel, 157
pthread_cleanup_pop, 159
S
pthread_cleanup_push, 159
sbrk, 49
pthread_cond_broadcast, 149
scanf, 69
pthread_cond_destroy, 148
SCHED_FIFO, 22
pthread_cond_init, 148 SCHED_OTHER, 22, 25
pthread_cond_signal, 149 SCHED_RR, 22
pthread_cond_timedwait, 149 sched_rr_get_interval, 22
pthread_cond_wait, 149 sched_setscheduler, 22
pthread_create, 133 sched_yield, 22
pthread_detach, 133 Seconde chance
pthread_equal, 133 Algorithme de la, 37
pthread_exit, 133 Segmentation, 34, 35
pthread_get_specific, 141 Segments, 35
pthread_join, 133 select, 66, 189
pthread_key_create, 141 Sémaphores, 98, 108, 146
pthread_key_delete, 141 POSIX, 101
pthread_mutex_destroy, 144 sem_close, 101
pthread_mutex_init, 144 sem_destroy, 101, 146
pthread_mutex_lock, 145 sem_getvalue, 101, 146
pthread_mutex_trylock, 145 sem_init, 101, 146
pthread_mutex_unlock, 145 sem_open, 101
pthread_once_t, 160 sem_post, 101, 146
pthread_self, 133 sem_trywait, 146
pthread_setcancelstate, 157 sem_unlink, 101
pthread_setcanceltype, 157 sem_wait, 101, 146
pthread_set_specific, 141 sendfile, 61
pthread_testcancel, 159 Serveur, see Architecture client/serveur
purify, 51 setvbuf, 71
putc, 69 setgid, 80
putchar, 69 setitimer, 66
puts, 69 setpgid, 80

212
setpriority, 25 U
setrlimit, 43 ulimit, 16
shm_open, 95 Unix NDBM, see NDBM
shm_unlink, 95 unlink, 74, 75
SIGALARM, 66 Utilisation des pages, 35, 39
signaction, 66
signal, 65 V
Signal, see Synchronisation, Envoi de signal V, 98, 101, 108, 143
size, 35 valgrind, 4
SMP, 170 valgrind, 40, 51
SMT, 169 Variables-condition, see Moniteurs
splint, 50, 51 Verrouillage de fichier, see Fichier, Verrouillage de
sprintf, 70 vfork, 81
SQLite, 5 vmstat, 35
sscanf, 69
SSE, see Instructions vectorielles SSE W
stat, 73 wait, 82, 123
strerror, 16, 20 waitpid, 82
superscalaire, 165 watchpoint, 54
Swap, 35 WEXITSTATUS, 82
symlink, 75 WIFEXITED, 82
Symmetric Multi-Processing, 170 WIFSIGNALED, 82
Synchronisation Wine, 51
Appel procédural entre processus, 113 Working set, 33
Cohorte, 111 write, 59
writev, 59
disque, 58
WTERMSIG, 82
Envoi de signal, 112
Exclusion mutuelle, 110 Z
Interblocage, 122 ZeroMQ, 6
Lecteurs/Rédacteurs, 119 ZeroMQ, 91
Passage de témoin, 111
Producteurs/Consommateurs, 114
Rendez-vous entre deux processus, 112
synchronized, 123
Systèmes
temps réel, 8, 9
transactionnel, 9

T
Table des pages, 32
Taille, 35
Tampon du fichier, see Fichier, Tampon du
Task-spooler, 7
Temps réel, see Systèmes temps réel, 9
Linux, 23
Terminaison de processus, see Processus, Terminai-
son de
Thrashing, 33
Thread, 126
time, 23, 39
TLB, 33
top, 23, 35, 39
Transactionnel, see Systèmes transactionnels
Translation Look-aside Buffer, 33
Tubes
nommés, 87
ordinaires, 85

213
214

Vous aimerez peut-être aussi