Вы находитесь на странице: 1из 94

Concepts des systèmes

d’exploitation et mise en oeuvre


sous Unix

Coordonnateur : Michel Simatic

module CSC4508/M2

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

Table des matières 1 Point de vue système 26


1.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
Licence vii 1.2 Besoins des processus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
1.3 Pagination . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
Présentation du cours 1 1.3.1 Généralités . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
1.3.2 Adresse logique . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
Plan du document 2
1.3.3 Table des pages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
1 Objectifs du cours 2 1.3.4 Accélérateurs (caches d’adresses) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
1.3.5 Écroulement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
2 Public visé et pré-requis 3 1.4 Segmentation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
1.4.1 Généralités . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
3 Déroulement 4 1.4.2 Descripteur de segments . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
1.5 Pagination versus Segmentation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
Introduction : rôle d’un système d’exploitation 5 1.6 Algorithmes pour la gestion des pages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
1.6.1 Chargement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
Plan du document 6 1.6.2 Remplacement (déchargement) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
1 Systèmes informatiques 6 2 Point de vue processus 34
2.1 Espace d’adressage d’un processus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
2 Machine virtuelle 6 2.2 Observation et contrôle de l’utilisation de la mémoire . . . . . . . . . . . . . . . . . . . . . 35
2.2.1 Observation de l’utilisation des pages . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
3 Objectifs d’un système d’exploitation 7
2.2.2 Observation du type d’accès à la mémoire . . . . . . . . . . . . . . . . . . . . . . . . 36
4 Différents types de systèmes d’exploitation 7 2.2.3 Contrôle du volume de pages : être attentif à l’alignement des structures . . . . . . . 38
4.1 Temps réel souple versus temps réel strict . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 2.2.4 Contrôle des défauts de page . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
4.2 Caractéristiques des applications temps réel . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 2.3 Allocation/Désallocation dynamique de mémoire . . . . . . . . . . . . . . . . . . . . . . . . 39
4.3 Transactionnel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 2.3.1 Allocation/Désallocation mémoire standard . . . . . . . . . . . . . . . . . . . . . . . 40
2.3.2 Digression : algorithmes pour malloc . . . . . . . . . . . . . . . . . . . . . . . . . . 41
Interactions entre système multi-tâche et processus 11 2.3.3 Digression : algorithmes pour free . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
2.3.4 Désallocation automatique avec alloca . . . . . . . . . . . . . . . . . . . . . . . . . 42
Plan du document 12 2.3.5 Mécanisme d’allocation/désallocation dédié . . . . . . . . . . . . . . . . . . . . . . . 42
2.3.6 Allocation/Désallocation mémoire au niveau système . . . . . . . . . . . . . . . . . . 42
1 Point de vue processus 12 2.4 Déverminage des accès mémoire . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
1.1 Les applications et le système . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 2.4.1 Détecter statiquement les accès erronés à des zones allouées dynamiquement . . . . . 43
1.2 La programmation système . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 2.4.2 Détecter dynamiquement les accès erronés à des zones allouées dynamiquement . . . 45
1.3 Les fonctions de la libc . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 2.4.3 Visualiser quand une zone mémoire est accédée/modifée . . . . . . . . . . . . . . . . 47
1.4 Utilisation des appels systèmes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
1.5 Utilisation des fonctions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14 Les fichiers (et les entrées-sorties) 49
1.6 Test du retour des fonctions et appels système . . . . . . . . . . . . . . . . . . . . . . . . . 15
1.7 Variable errno et fonction perror . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 Plan du document 50
1.8 Que faire en cas d’erreur système ? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
1 Primitives Unix d’entrées-sorties 50
2 Point de vue système 18 1.1 Primitives de base . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
2.1 Généralités sur l’exécution des tâches système . . . . . . . . . . . . . . . . . . . . . . . . . . 18 1.1.1 Ouverture/fermeture de fichier . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
2.2 Traitement des appels système . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 1.1.2 Lecture de fichier . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
2.3 Prise en compte des interruptions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20 1.1.3 Écriture de fichier . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
1.2 Duplication de descripteur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
3 Ordonnancement des processus sous Linux 20 1.3 Contrôle des entrée-sorties . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
3.1 Priorité statique . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 1.3.1 Primitive fcntl . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
3.2 Politique d’ordonnancement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 1.3.2 Verrouillage de fichier . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
3.3 Ordonnancement temps-réel Linux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 1.4 Manipulation de l’offset . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
3.3.1 Inversion de priorité : énoncé du problème . . . . . . . . . . . . . . . . . . . . . . . . 23
3.3.2 Inversion de priorité : exemple de solution . . . . . . . . . . . . . . . . . . . . . . . . 23 2 Bibliothèque C d’entrées-sorties 58
3.4 Ordonnancement standard . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 2.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
2.2 Ouverture/fermeture de fichier . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
Gestion de la mémoire 25 2.3 Lecture de fichier . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
Lecture de fichier (suite) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
Plan du document 26 2.4 Écriture de fichier . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61

TELECOM SudParis — Coordonnateur : Michel Simatic — Mai 2010 — module CSC4508/M2 i TELECOM SudParis — Coordonnateur : Michel Simatic — Mai 2010 — module CSC4508/M2 ii
Concepts des systèmes d’exploitation et mise en oeuvre sous Unix Concepts des systèmes d’exploitation et mise en oeuvre sous Unix

Écriture de fichier (suite) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61 3 Résolution de problèmes de synchronisation typiques 101


2.5 Contrôle du tampon . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63 3.1 Exclusion mutuelle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102
2.6 Divers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63 3.2 Cohorte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102
3.3 Passage de témoin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102
3 Projection des fichiers en mémoire 64 3.3.1 Envoi de signal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
3.3.2 Rendez-vous entre deux processus . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
4 Manipulation des i-noeuds du système de fichiers Unix 65
3.3.3 Appel procédural entre processus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104
Manipulation des i-noeuds du système de fichiers Unix (suite) . . . . . . . . . . . . . . . . . . . 66
3.4 Producteurs/Consommateurs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105
5 Entrées-sorties sur répertoires 66 3.4.1 Objectif . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105
3.4.2 Principe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106
6 Limitations de NFS 67 3.4.3 Déposer et extraire . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106
3.4.4 Exemple . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107
Communications inter-processus 69 3.4.5 K producteurs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108
3.4.6 Exemple de problème avec 2 producteurs . . . . . . . . . . . . . . . . . . . . . . . . 108
Plan du document 70 Exemple de problème avec 2 producteurs (suite) . . . . . . . . . . . . . . . . . . . . . . . 108
3.4.7 Solution complète . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109
1 Gestion des processus 70
3.5 Lecteurs/rédacteurs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109
1.1 Environnement des processus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
3.5.1 Objectif . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110
1.2 Informations concernant un processus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
3.5.2 Solution de base . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110
1.3 Création de processus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
3.5.3 Analyse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111
1.4 Terminaison de processus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
3.5.4 Solution avec priorités égales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111
1.5 Exécution d’un nouveau programme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
4 Interblocage 112
2 Communications à l’aide de tubes 76
4.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112
2.1 Principe des tubes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
4.2 Généralités . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113
2.2 Tubes ordinaires (ou locaux) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
2.3 Tubes nommés . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
5 Mise en oeuvre dans un système d’exploitation 113
3 Communication à l’aide des IPC System V 81
Threads ou processus légers 115
3.1 Inter Process Communication System V . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
3.2 IPC System V versus POSIX IPC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83 Plan du document 116
3.3 Constantes relatives aux IPC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
3.4 Files de messages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84 1 Présentation 116
3.5 Mémoire partagée . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87 1.1 Bibliographie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116
3.6 Sémaphores IPC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90 1.2 Threads . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117
3.6.1 Introduction aux sémaphores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90 1.3 Détacher le flot d’exécution des ressources . . . . . . . . . . . . . . . . . . . . . . . . . . . . 118
3.6.2 Introduction aux sémaphores : analogie . . . . . . . . . . . . . . . . . . . . . . . . . 91 1.3.1 Vision traditionnelle d’un processus . . . . . . . . . . . . . . . . . . . . . . . . . . . 118
3.6.3 Introduction aux sémaphores : algorithmes P ET V . . . . . . . . . . . . . . . . . . 91 1.3.2 Autre vision d’un processus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119
3.6.4 Sémaphores IPC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92 1.3.3 Processus multi-thread . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119
3.6.5 Utilisation des sémaphores IPC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
2 Création/destruction de threads 120
4 Comparaison des mécanismes de synchronisation 94 2.1 Pthread “Hello world” . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 120
2.2 Ensemble de threads pairs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121
Synchronisation entre processus 97
2.3 Threads POSIX: création/destruction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122
Plan du document 98 2.3.1 Identification . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122
2.3.2 Utilisation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123
1 Introduction 98 2.3.3 Attributs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 124
1.1 Correspondance problèmes vie courante/informatique . . . . . . . . . . . . . . . . . . . . . 98 Attributs (2/2) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 124

2 Sémaphore = Outil de base 99 3 Partage des données 125


2.1 Généralités . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99 3.1 Notion de variable partagée . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126
2.2 Analogie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100 3.2 Partage non-intentionnel des données . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126
2.3 Algorithmes P ET V . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101 3.3 Code réentrant et thread-safe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 128

TELECOM SudParis — Coordonnateur : Michel Simatic — Mai 2010 — module CSC4508/M2 iii TELECOM SudParis — Coordonnateur : Michel Simatic — Mai 2010 — module CSC4508/M2 iv
Concepts des systèmes d’exploitation et mise en oeuvre sous Unix Concepts des systèmes d’exploitation et mise en oeuvre sous Unix

4 Synchronisation 131 6 Bibliographie 168


4.1 Synchronisation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131
4.2 Exclusions mutuelles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132 Références 171
Exclusions mutuelles (2/2) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133
Index 173
4.3 Sémaphores POSIX . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134
4.4 Attente de conditions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136
Attente de conditions (2/2) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137

5 Implantation des threads 141


5.1 Threads utilisateurs et threads noyaux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142
5.2 Linux threads . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143
5.3 Exploitation des architectures multi-processeurs . . . . . . . . . . . . . . . . . . . . . . . . . 143
5.3.1 Utilisation des threads . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144
5.4 Limitations des threads . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146

6 Autres fonctions de la bibliothèque POSIX threads 147


6.1 Annulation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148
Annulation (2/2) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149
6.2 Nettoyage des ressources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149
6.3 Initialisation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 150
6.4 Données privées . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 150

Éléments d’architecture client-serveur 153

Plan du document 154

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

2 Serveur mono-tâche 156


2.1 Principe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 156
2.2 Analyse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157

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


3.1 Principe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 158
3.2 Variante . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159
3.3 Analyse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 160
3.4 Réduction du temps de connexion des clients . . . . . . . . . . . . . . . . . . . . . . . . . . 161

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


4.1 Principe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 161
4.2 Analyse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 162

Bibliographie 165

Plan du document 166

1 Concepts des systèmes d’exploitation 166

2 Ouvrages dédiés à Unix 166

3 Documents dédiés à Linux 167

4 Documents spécifiques threads 167

5 Divers 168

TELECOM SudParis — Coordonnateur : Michel Simatic — Mai 2010 — module CSC4508/M2 v TELECOM SudParis — Coordonnateur : Michel Simatic — Mai 2010 — module CSC4508/M2 vi
Concepts des systèmes d’exploitation et mise en oeuvre sous Unix Concepts des systèmes d’exploitation et mise en oeuvre sous Unix
' $
Nous espérons que vous regardez cette page avec un navigateur libre: Mozilla ou Firefox par exemple.
Licence
Comme l’indique le choix de la licence GNU/FDL, tous les éléments permettant d’obtenir ces supports sont
libres.

Ce document est une documentation libre, placée sous la Licence de Documentation Libre GNU (GNU
Free Documentation License).
Copyright (c) 2003-2010 Frédérique Silber-Chaussumier et Michel Simatic
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;

TELECOM SudParis — Coordonnateur : Michel Simatic — Mai 2010 — module CSC4508/M2 vii TELECOM SudParis — Coordonnateur : Michel Simatic — Mai 2010 — module CSC4508/M2 viii
Présentation du cours
' $
Plan du document

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

Présentation du cours
& %

' $
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)
Michel Simatic ¥ Objectifs
¨ (Re)découvrir les concepts de base d’un système d’exploitation (processus,
#3
gestion de la mémoire, fichiers, threads)
module CSC4508/M2 ◮ Prendre conscience des hypothèses choisies par les concepteurs/développeurs
de système d’exploitation
◮ 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 :
AP11 : Initiation à l’algorithmie et la programmation
AM12 : Interactions entre un programme et la machine sur laquelle il tourne
– 2è année :
CSC4508 (partie interaction entre les programmes et le système d’exploitation) : Interactions :
– entre un programme et le système d’exploitation de la machine sur laquelle il tourne
Mai 2010 – entre plusieurs programmes sur la même machine

1 TELECOM SudParis — Michel Simatic — Mai 2010 — module CSC4508/M2 2


Présentation du cours Présentation du cours
' $
CSC4509 : Algorithmique et communications des applications réparties (Interactions entre processus
3 Déroulement
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 ¥ Cours
' $ ¨ Introduction
2 Public visé et pré-requis ¨ Interactions entre système multi-tâche et processus
¨ Gestion de la mémoire
¨ Entrées/Sorties
#5 ¨ Communications inter-processus
¨ Synchronisation entre processus
¥ Public visé ¨ Threads
¨ Futurs ingénieurs (développeurs, spécifieurs, architectes. . .) dont le cœur de ¨ Éléments d’architecture client-serveur
métier (Télécoms, spatial. . .) utilise l’outil informatique ¥ TPs (sous Linux)
#4 ¨ Futurs chercheurs en systèmes (répartis) ¨ Exercices d’application
¥ Pré-requis ¨ Exercice de synthèse ou d’approfondissement
¨ Algorithmie (notions) ¥ Travail personnel
¨ Architectures matérielles (notions)
¨ Langage C (bonne pratique) & %
¨ Unix utilisateur (bonne pratique)
Les « signaux » ne sont pas traités 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 :
Si un participant veut parfaire ses connaissances liées aux pré-requis, il pourra se reporter avec profit – Logiciels libres : mcheck, mtrace, ccmalloc, electric fence, splint et valgrind
vers les sites suivants : – Produits commerciaux : Insure (société Parasoft) et Rational Purify (société IBM )
– Algorithmie : cours Algorithmique et programmation (CSC3002, En ce qui concerne le travail personnel, voici les résultats d’un sondage réalisé sur des étudiants de la
http://cours.it-sudparis.eu/moodle) session 2005-2006 (7 réponses sur 22 étudiants) :
– Architectures matérielles : cours Architecture matérielle et logicielle d’un ordinateur (CSC3501, – Pas de préparation nécessaire avant une séance de 3 heures de Cours Intégré.
http://cours.it-sudparis.eu/moodle) – En moyenne, 24 heures de travail personnel (minimum de 7 heures, maximum de 50 heures).
– 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 — Mai 2010 — module CSC4508/M2 3 TELECOM SudParis — Michel Simatic — Mai 2010 — module CSC4508/M2 4
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

Introduction : rôle d’un système


d’exploitation
& %

' $
1 Systèmes informatiques

Michel Simatic

module CSC4508/M2 ¥ But : résoudre un problème de la vie quotidienne


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

& %

Mai 2010

5 TELECOM SudParis — Michel Simatic — Mai 2010 — module CSC4508/M2 6


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

¥ 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 ¥ Problèmes différents : pas de système universel
au niveau supérieur ¥ À l’origine (et encore aujourd’hui pour certaines applications), traitement par lots ou
batch (enchaînement automatique des exécutions)
Utilisateurs ¥ Aujourd’hui trois grandes catégories de systèmes
#4 #6
1. Systèmes temps réel : contrôle de processus industriels (notion de respect de
temps de réponse prépondérante)
Logiciel d’application
2. Systèmes transactionnels : traitements à distance (nombreux accès interactifs,
Utilitaires opérations prédéfinies, grande quantité d’informations)
Logiciel de base
Système d’exploitation 3. Systèmes temps partagé : développement d’applications et activités avec moins
de contraintes. Mode interactif avec un maximum de fonctionnalités
Matériel

& % & %

' $ ' $
3 Objectifs d’un système d’exploitation 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


¥ Deux objectifs majeurs
non-respect des contraintes de temps
¨ Transformer une machine matérielle en une machine utilisable, c’est-à-dire fournir
1. Temps réel souple (ou doux)
des outils adaptés aux besoins indépendamment des caractéristiques physiques
¨ Temps à l’échelle humaine et un retard ne provoque que des désagréments
¨ Optimiser l’utilisation du matériel principalement pour des raisons économiques.
mineurs (impatience de l’utilisateur)
¥ Mais il faut également la garantie d’un bon niveau en matière de :
#5 #7 ¨ Informatique interactive, réservation de places, gestion, traitement d’appel
¨ Sécurité : intégrité, contrôle des accès, confidentialité. . .
dans un central téléphonique. . .
¨ Fiabilité : degré de satisfaction des utilisateurs même dans des conditions hostiles
2. Temps réel strict (ou dur)
et imprévues
¨ Systèmes autonomes de contrôle de processus industriels avec des exigences
¨ Efficacité : performances du système
très fortes au niveau du respect des contraintes de temps : tout retard entraîne
→ Optimisations pour éviter tout surcoût (overhead) en terme de temps et place
de graves conséquences telles qu’une perte d’information (un message sur un
consommés par le système au détriment de l’application
réseau), un accident (crash d’un avion, explosion d’une raffinerie). . .
→ Compromis
¨ Robotique, pilotage d’avions, surveillance médicale, acheminement de la voix
→ Différents types de systèmes d’exploitation
dans un central téléphonique, contrôle de raffineries, systèmes embarqués. . .

& % & %

TELECOM SudParis — Michel Simatic — Mai 2010 — module CSC4508/M2 7 TELECOM SudParis — Michel Simatic — Mai 2010 — module CSC4508/M2 8
Introduction : rôle d’un système d’exploitation 4 Différents types de systèmes d’exploitation Introduction : rôle d’un système 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. . .)
◮ Mode maître/esclave
◮ 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 — Mai 2010 — module CSC4508/M2 9 TELECOM SudParis — Michel Simatic — Mai 2010 — module CSC4508/M2 10
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. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .14
3 Ordonnancement des processus sous Linux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16

Interactions entre système


multi-tâche et processus
& %

' $
1 Point de vue processus

Michel Simatic

module CSC4508/M2 1.1 Les applications et le système . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4


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

& %

Mai 2010

11 TELECOM SudParis — Michel Simatic — Mai 2010 — module CSC4508/M2 12


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

¥ 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
¥ Deux types fondamentaux :
¨ L’espace utilisateur : où résident les applications
1. Les appels système
#4 Espace utilisateur #6 ¨ Ce sont les fonctions permettant la communication avec le noyau
Application1 Application2
¨ Exemples : open, read, write, fcntl. . .
2. Les fonctions
¨ Ce sont les fonctions standard du langage C
¨ Exemples : printf, fopen, fread, strcmp. . .
Relations avec
les périphériques
Espace système (noyau)

& % & %

' $
1.2 La programmation système
' $
1.4 Utilisation des appels systèmes
¥ 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 ¥ Travaillent en relation directe avec le noyau
Espace utilisateur ¥ Retournent un entier positif ou nul en cas de succès et -1 en cas d’échec
Application1 Application2 #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
libc ¥ 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
Relations avec espace par allocation de variable (statique, pile) ou de mémoire (malloc(). . .)
les périphériques
Espace système (noyau)

& %

& %

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 — Mai 2010 — module CSC4508/M2 13 TELECOM SudParis — Michel Simatic — Mai 2010 — module CSC4508/M2 14
Interactions entre système multi-tâche et processus 1 Point de vue processus Interactions entre système multi-tâche et processus 1 Point de vue processus
' $ ' $
1.5 Utilisation des fonctions 1.7 Variable errno et fonction perror

¥ Retournent une valeur de type divers (entier, caractère, pointeur. . .). Voir le manuel ¥ Lorsqu’un appel système échoue, le noyau positionne la variable externe errno à une
de référence pour chacune d’entre elles valeur significative de l’erreur
#8 ¥ Lorsqu’elles rendent un pointeur, celui-ci est le pointeur NULL en cas d’échec # 10 ¨ errno est de type entier, à 0 par défaut (lorsqu’il n’y a pas eu d’erreur)
¥ Certaines peuvent utiliser un appel système (fopen s’appuie sur open, fread sur ¨ Le fichier errno.h associe des mnémoniques à chaque erreur « standard »
read. . .) ¥ La fonction perror()
¥ Les fonctions rendant un pointeur ont généralement alloué de la mémoire dans ¨ perror("message") affiche le message indiqué suivi de « : » et du message
l’espace du processus et le pointeur rendu y donne accès système correspondant à l’erreur

& % & %

La fonction strerror() permet d’obtenir la chaîne de caractères affichée par perror() sans pour autant
provoquer d’affichage.
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 ?
1.6 Test du retour des fonctions et appels système Sinon que se passe-t-il ?
– 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 ?
¥ Il faut toujours tester la valeur de retour d’un appel système ' $
Si valeur rendue est égale à −1 1.8 Que faire en cas d’erreur système ?
¨ Il faut gérer le problème
¨ Une variable externe de nom errno est positionnée à une valeur indiquant l’erreur
#9
¥ Il faut presque toujours tester la valeur de retour d’une fonction ¥ Utiliser la macro assert()
Pour les fonctions rendant un pointeur, si la valeur rendue est NULL rc = appelSysteme(...);
¨ Il faut gérer le problème assert(rc >= 0);
¥ Envoi de messages d’erreurs à l’aide des fonctions ¥ Se définir une macro FATAL
¨ perror() (ou strerror()) if (appelSysteme(...) < 0)
¨ fprintf() (ou fputs) # 11 FATAL(unMessage);
en définissant FATAL de la manière suivante
#define FATAL(msg) { \

& %
fprintf(stderr,"%s:%s %s:%d:%s\n", \
strerror(errno), msg, \
__FILE__, __LINE__, __func__);\
abort(); \
}

& %
Un exemple de « presque toujours » est la fonction printf(3). Pourquoi ?

TELECOM SudParis — Michel Simatic — Mai 2010 — module CSC4508/M2 15 TELECOM SudParis — Michel Simatic — Mai 2010 — module CSC4508/M2 16
Interactions entre système multi-tâche et processus 1 Point de vue processus Interactions entre système multi-tâche et processus 2 Point de vue système

La fonction abort() termine de manière anormale le processus en cours. Un core dump est généré #define FATALF(msg) { \
(analysable ultérieurement par un débuggueur), si la limite fixée par le shell l’autorise. Par exemple sous bash, fprintf(stderr,"%s %s:%d:%s\n", \
ulimit -c permet de voir la taille limite du core dump (il faut taper la commande ulimit -c unlimited msg, \
pour n’avoir aucune limite). __FILE__, __LINE__, __func__);\
Voici un exemple d’utilisation de la fonction assert : abort(); \
/*******************/
}
/* exempleAssert.c */
/*******************/

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


#include <stdlib.h> 2 Point de vue système
#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; # 12 2.2 Généralités sur l’exécution des tâches système . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14


} 2.2 Traitement des appels système . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
Le message d’erreur obtenu à l’exécution est le suivant : 3.0 Prise en compte des interruptions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
assertion "rc >= 0" failed: file "essaiAssert.c", line 17
Aborted (core dumped)

On a bien détection d’un problème, mais pas son origine ! C’est pourquoi on peut lui préférer la macro
FATAL dont voici un exemple d’utilisation :
/******************/
/* exempleFATAL.c */
/******************/

/* Ce programme a pour objectif d’illustrer le role de la macro FATAL */ & %


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

' $
#include <sys/stat.h>
#include <unistd.h>

#include <stdio.h> 2.1 Généralités sur l’exécution des tâches système


#include <string.h>
#include <errno.h>
#define FATAL(msg) { \
fprintf(stderr,"%s:%s %s:%d:%s\n", \
strerror(errno), msg, \
__FILE__, __LINE__, __func__);\
abort(); \
}

int main(){
struct stat buf;
int rc;
¥ 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
rc = stat("unFichierQuiNExistePas", &buf); # 13
if (rc < 0){ ¥ Le système ne se déroule pour son propre compte que dans très peu de cas
FATAL("Pb au moment de l’appel a stat");
} ¥ On distingue deux types d’actions
return EXIT_SUCCESS; ¨ Le traitement des appels système
} ¨ La prise en compte des interruptions
Le message d’erreur obtenu à l’exécution est le suivant :
No such file or directory:Pb au moment de l’appel a stat essaiFATAL.c:26:main
Aborted (core dumped)

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 on peut utiliser la macro assert() (comme pour les appels système), l’utili- & %
sation de la macro FATAL n’a pas de sens (puisque la variable errno n’est pas positionnée). En revanche, on
peut définir une macro FATALF, dédiée à ces fonctions, de la manière suivante :

TELECOM SudParis — Michel Simatic — Mai 2010 — module CSC4508/M2 17 TELECOM SudParis — Michel Simatic — Mai 2010 — module CSC4508/M2 18
Interactions entre système multi-tâche et processus 2 Point de vue système Interactions entre système multi-tâche et processus
' $
– Une courte phrase peut être obtenue par la fonction char *strerror (int errnum). Cette
2.2 Traitement des appels système
phrase peut être affichée par void perror (const char *s) .
' $
Programme

...
UneFonction ( ... ) { 2.3 Prise en compte des interruptions
... Bibliothèque standard
LaPrimitive ( P1, P2, ... ) ;
... ...
} LaPrimitive ( T1 P1, T2 P2, ... ) {
... Empile ( NuméroPrimitive, P1, P2, ... ) ;
main ( ) { Trappe ( NuméroTrappePrimitive ) ;
# 14 ... Retourne ValeurRetour ;
} }

...
Espace utilisateur
¥ Les interruptions sont acquittées par le processus en cours
Espace noyau GestionnaireTrappe ( int NuméroTrappe ) { # 15
PrimitiveNoyau ( ) ; ¥ Il peut être amené à passer du mode utilisateur au mode noyau
LeTableau } ¥ Il fait passer les processus qui attendaient l’événement lié à cette interruption dans le
LeCode ( void * Args, ... ) {
IdentifieParamètres ( ) ;
PrimitiveNoyau ( ) { mode prêt pour exécution
CopieParamètres ( & Args ) ;
...
( * LeTableau [ Numéro ] ) ( Args, &ValeurRetour ) ;
}
RetourUtilisateur ( ..., ValeurRetour ) ;

& %
}

– 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
Le changement d’espace se fait par une trappe (écrite en assembleur)
– Précisions sur le déroulement des appels système 3 Ordonnancement des processus sous Linux
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 3.3 Priorité statique . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
# 16 3.3 Politique d’ordonnancement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
– En ce qui concerne les requêtes d’E/S, on distingue :
– Les E/S interruptibles (elles peuvent être avortées par un signal) 3.3 Ordonnancement temps-réel Linux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
– Les E/S ininterruptibles (elles ne peuvent pas être avortées) 3.4 Ordonnancement standard . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
– 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’ordonnancement

– Un processus réalisant une E/S interruptible peut sortir de l’attente lors de la réception d’un Sous Linux, on a 4 notions liées à l’ordonnancement :
signal – La priorité statique,
– Il défait alors sa demande au niveau noyau et retourne en mode utilisateur avec un compte-rendu – La politique d’ordonnancement,
d’erreur – La priorité d’ordonnancement,
3. Retour au mode utilisateur – La priorité dynamique.
– Les appels système retournent un code Ces notions permettent :
– En cas d’erreur, la variable errno est modifiée pour expliciter l’erreur – Un ordonnancement temps-réel « souple »
– Les valeurs de errno sont décrites dans le fichier <errno.h>. – Un ordonnancement standard privilégiant une exécution équitable des différents processus

TELECOM SudParis — Michel Simatic — Mai 2010 — module CSC4508/M2 19 TELECOM SudParis — Michel Simatic — Mai 2010 — module CSC4508/M2 20
Interactions entre système multi-tâche et processus 3 Ordonnancement des processus sous Linux Interactions entre système multi-tâche et processus 3 Ordonnancement des processus sous Linux
' $
NB : Sous Linux, on n’a aucune garantie sur le temps d’exécution d’une tâche (axiome de base du
3.3 Ordonnancement temps-réel Linux
temps-réel « strict »).

' $
3.1 Priorité statique

# 19 3.4.0 Inversion de priorité : énoncé du problème . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22


3.4.0 Inversion de priorité : exemple de solution . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
¥ int sched_setscheduler(pid_t pid, int policy, const struct
sched_param *p) ;
# 17 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
& %
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.

' $
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
◮ Il fait une E/S
◮ Il fait un appel à int sched_yield(void) (il laisse alors passer les autres
processus prêts de même priorité)
# 18
◮ Il change sa politique d’ordonnancement
◮ 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
◮ 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 — Mai 2010 — module CSC4508/M2 21 TELECOM SudParis — Michel Simatic — Mai 2010 — module CSC4508/M2 22
Interactions entre système multi-tâche et processus 3 Ordonnancement des processus sous Linux 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 3.4 Ordonnancement standard

Tâche C
¥ La politique SCHED_OTHER est la politique utilisée par défaut par Linux
Priorité forte C prêt : C
¥ Le processus à exécuter est choisi dans la liste des processus de priorité statique
prend la main
Besoin R.
nulle, en utilisant une priorité dynamique qui ne s’applique que dans cette liste. NB :
Déjà réservée Valeurs fortes = Faibles priorités
par A : C se ¥ La priorité dynamique est fonction, entre autres
met en attente
¨ d’une priorité d’ordonnancement (caractère « gentil » du processus fixé avec les
# 20 Tâche B # 22 appels système int nice(int inc) ou int setpriority(int which, int
Priorité moy.
B prêt. who, int prio))
B plus prioritaire que A. Donc B prend la
A prêt : A main.
¨ du fait que le processus ait relâché le processeur avant expiration de son délai
prend la main On constate que C attend la fin de l’exécution ¥ Le processus le plus prioritaire (dynamiquement) est exécuté jusqu’à ce qu’il
Réservation de B alors que C est plus prioritaire que B : ¨ Fasse une E/S
ressource les priorités de B et C sont inversées.
¨ Devienne moins prioritaire (dynamiquement) qu’un autre processus
critique R
Tâche A ¥ La priorité dynamique diminue à chaque quantum de temps (10ms) où un processus
Priorité faible
est prêt, mais non sélectionné (ceci garantit une progression équitable de tous les
t0 t1 t2 t3 t4 processus de priorité statique nulle)
& % & %

Le quantum de temps utilisé pour l’ordonnancement standard (valeur = 10ms) est totalement décorrélé
du quantum de temps attribué aux processus SCHED_RR (valeur = sched_rr_get_interval(...)).
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.
' $
3.3.2 Inversion de priorité : exemple de solution

Tâche C
Priorité forte C prêt. C de Réservation
meme priorité ressource
que A : C attend critique R

B prêt.
B moins prioritaire
que A : B attend
# 21 Tâche B
Priorité moy. A a fini (et a C a fini (et a
A prêt : A relâché R) : A relâché R) :
prend la main retrouve sa B prend la
Réservation priorité faible. main
ressource De plus, C est
critique R : plus prioritaire
A reçoit une que B. Donc C
priorité forte prend la main
Tâche A
Priorité faible

t0 t1 t2 t4 t2’ t3 t4’
& %

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 — Mai 2010 — module CSC4508/M2 23 TELECOM SudParis — Michel Simatic — Mai 2010 — module CSC4508/M2 24
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 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20

Gestion de la mémoire
& %

' $
1 Point de vue système
Michel Simatic

module CSC4508/M2
1.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.2 Besoins des processus. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .5
#3 1.3 Pagination . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
1.4 Segmentation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
1.5 Pagination versus Segmentation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
1.6 Algorithmes pour la gestion des pages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18

& %

Mai 2010

25 TELECOM SudParis — Michel Simatic — Mai 2010 — module CSC4508/M2 26


Gestion de la mémoire 1 Point de vue système 1 Point de vue système 1.3 Pagination
' $
Windows-2000 et Windows-XP). . . ont ces 4 besoins. Comme la gestion mémoire à base de pagination et
1.1 Introduction
de segmentation y répond pleinement, ces systèmes d’exploitation utilise 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 les 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
¥ Un processus a besoin d’être présent en mémoire centrale pour s’exécuter frustre, mais beaucoup moins gourmande en ressources CPU. Les problèmes spécifiques de ce type de gestion
¥ Mémoire centrale divisée en deux parties : 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
¨ L’espace réservé au système d’exploitation
donc très proches.
' $
¨ 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 1.3 Pagination
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
1.3.2 Généralités . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7

& %
#6 1.3.4 Adresse logique. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .10
1.3.4 Table des pages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
À propos du principe d’inclusion, dans une architecture Intel, on a le cache L1 (Level 1) qui est inclus 1.3.4 Accélérateurs (caches d’adresses) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
dans le cache L2 (Level 2), lui-même inclus dans la RAM, elle-même incluse dans le swap (disque). 1.4.0 Écroulement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
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
RAM DDR2 (en €/Go) - 37,00 21,85
Disque dur, 7200 tr/mn (en €/Go) 0,50 0,32 0,10

& %
DVD Rom (en €/Go) 0,34 0,31 0,63
Clé USB 16 Go (en €/Go) - - 1,64

' $
' $
1.2 Besoins des processus 1.3.1 Généralités

Allocation Trouver une zone mémoire disponible pour y stocker un programme


¥ Objectif : offrir un espace adressable plus grand que la mémoire physique
Libération quand un programme se termine, récupérer les espaces (en faisant des
¥ Principe
regroupements s’il y a eu fragmentation) qu’il occupait pour les consacrer à d’autres
¨ Espace adressable de chaque programme découpé en pages
processus. ¨ Mémoire physique divisée en cadres de pages
Protection Garantir l’intégrité de l’espace mémoire associé à chaque processus → ¥ À l’exécution, un processus possède
¥ Dispositif (matériel) de contrôle empêchant tout accès en dehors de l’espace ¨ Peu de pages en mémoire centrale (pages actives)
#5
attribué #7 ¨ Le reste est
¥ Toute détection d’une violation mémoire est récupérée par le système ◮ Inexistant en mémoire (pages inactives jamais écrites)
(déroutement) ◮ En mémoire secondaire (pages inactives qui ont déjà été écrites)
Adressage Traduction des adresses logiques en adresses physiques : fonction appelée ¥ Le dispositif de pagination
topographie ou mapping ¨ Effectue la correspondance d’adresse
¨ Charge les pages nécessaires (déroutement par défaut de page)
¥ À la traduction/édition des liens
¨ (Éventuellement) décharge des pages actives en mémoire secondaire
¥ Au chargement initial
¥ Donc, le programme n’est plus présent intégralement en mémoire centrale et il n’y a plus de
¥ À l’exécution
continuité physique

& %
& %
Les systèmes multi-utilisateurs et multi-tâches actuels tels Unix, Windows-NT (et ses successeurs

TELECOM SudParis — Michel Simatic — Mai 2010 — module CSC4508/M2 27 TELECOM SudParis — Michel Simatic — Mai 2010 — module CSC4508/M2 28
1 Point de vue système 1.3 Pagination Gestion de la mémoire 1 Point de vue système
' $
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).
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
1.3.2 Adresse logique ¨ 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
¥ Espace adressable divisé à partir des bits de poids forts de l’adresse ¨ Sinon utilisation de la table des pages

Adresse logique sur k bits # 10 Np d


Adresse logique

Numéro de Page Déplacement dans la page Registres d’accélérateur


Numéro de page Numéro de cadre
( p bits ) ( (k − p) bits ) R0
R1
Mémoire Centrale
Np d cadre 0
#8 Rz Np Nc
→ 2 pages et une page contient 2
cadre 1
p
octets k−p
Nc d
Rt cadre Nc
¥ Taille d’une page Adresse physique
Mot adressé
¨ À l’origine : 512 octets ou 1 Ko
¨ Aujourd’hui : 2 Ko, 4 Ko (k-p = 12 bits, donc p = 20 bits) et plus cadre y

& %
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 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
1.3.3 Table des pages

¥ La correspondance entre adresse logique et adresse physique se fait avec une table ¥ Effet secondaire de la pagination : écroulement (thrashing) dû à un taux de défauts de page trop
important
des pages contenant
¥ Deux facteurs principaux : taille mémoire et degré de multiprogrammation (nombre de processus)
¨ Numéro de cadre de page
Probabilité de défaut de page Probabilité de défaut de page
¨ Bits d’information (présence, accès, référence, écriture, date de chargement. . .)
1 1

Np d
Adresse logique # 11
#9 Table des Pages
0 Taille mémoire 0 Nombre de processus
Information Numéro de cadre
0
¥ Solutions
1
Mémoire Centrale ¨ Augmentation de la capacité mémoire
Np Nc
cadre 0 ¨ Ordre des modules utilisés
cadre 1
¨ Prise en compte lors du développement
x Nc d
cadre Nc
¨ Régulation de la charge (load levelling) : limitation du nombre de processus
Adresse physique
¨ Définition d’un espace vital (working set) nécessaire
Mot adressé
¨ Gestion du taux de défauts de page : moduler le nombre de pages allouées

& %
cadre y

& %

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


moire virtuelle.

TELECOM SudParis — Michel Simatic — Mai 2010 — module CSC4508/M2 29 TELECOM SudParis — Michel Simatic — Mai 2010 — module CSC4508/M2 30
1 Point de vue système 1.4 Segmentation Gestion de la mémoire 1 Point de vue système
' $ ' $
1.4 Segmentation 1.4.2 Descripteur de segments

Ns d
Adresse logique

Descripteur de Segments
Information Longueur Base
0
1
# 12 # 14 Mémoire Centrale
1.4.2 Généralités . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
1.4.2 Descripteur de segments . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14 Ns lg b

x b + d
segment Ns b
Adresse physique
Mot adressé b+d
b+lg−1

¥ 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
1.4.1 Généralités ¨ Taille des pages fixes vs Taille des segments variable (avec contrôle de non-dépassement)
¥ On utilise donc pagination et segmentation !
# 15
Adresse logique sur k bits
¥ Objectif : refléter en mémoire centrale la structure des programmes (code, données, pile)
N° de Segment Déplacement dans le segment
¥ L’utilisateur définit des segments (entités logiques)
¥ Programme = ensemble de segments ayant une taille et des attributs propres (lecture seule, N° de Segment N° de Page Déplacement dans la page
écriture, partage) Ns Np d
¥ Pour disposer d’une segmentation, il faut un dispositif chargé de
¨ Effectuer la correspondance entre une adresse logique et l’adresse physique ¨ Avantage : mémoire virtuelle (segments chargés en partie) et organisation logique
# 13
¨ Assurer la protection ¨ Inconvénient : Surcoût (limité par l’utilisation de caches d’adresses dans le processeur)
¨ 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 Les commandes Unix suivantes donnent des informations sur la pagination et la segmentation des pro-
¨ Informations de contrôle d’accès cessus de la machine :
¥ Protection assurée par un double contrôle : si le déplacement est négatif ou supérieur à la – Pagination
longueur du segment, alors il y a violation mémoire et déroutement – 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
– 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

TELECOM SudParis — Michel Simatic — Mai 2010 — module CSC4508/M2 31 TELECOM SudParis — Michel Simatic — Mai 2010 — module CSC4508/M2 32
1 Point de vue système 1.6 Algorithmes pour la gestion des pages Gestion de la mémoire 2 Point de vue processus
' $
sur la charge de la machine
– Segmentation
1.6.2 Remplacement (déchargement)
– La commande size nomExécutable liste la taille des différentes sections de nomExécutable
¥ 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
1.6 Algorithmes pour la gestion des pages
LFU (Least Frequently Used) Remplacer la page la moins fréquemment utilisée
Algorithme de Belady dit « optimal » (OPT ou MIN) Remplacer la page
qui ne sera plus utilisée s’il en existe une et sinon celle qui le sera le plus
# 18 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
# 16 1.6.2 Chargement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 ¨ Un bit U associé à chaque page et mis à un lors de chaque accès
1.6.2 Remplacement (déchargement) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 ¨ 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)

' $ ' $
1.6.1 Chargement 2 Point de vue processus

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


¨ À la demande c’est-à-dire en cas de besoin
¨ Par anticipation 2.1 Espace d’adressage d’un processus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
# 17 Exemple : un programme travaille en général (principe de la séquentialité des # 19 2.2 Observation et contrôle de l’utilisation de la mémoire . . . . . . . . . . . . . . . . . . . . . . . . . . 22
programmes) sur des pages contiguës en y accédant de manière croissante : il 2.3 Allocation/Désallocation dynamique de mémoire . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
accède à une page, puis à la suivante (dans la plage d’adresse), puis à la 2.4 Déverminage des accès mémoire . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
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 — Mai 2010 — module CSC4508/M2 33 TELECOM SudParis — Michel Simatic — Mai 2010 — module CSC4508/M2 34
2 Point de vue processus 2.2 Observation et contrôle de l’utilisation de la mémoire 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 2.2.1 Observation de l’utilisation des pages

3 Go
env Environnement

stack Pile ¥ ps -u et top donnent des indications


Variables automatiques
¥ int getrusage (int who, struct rusage *usage)
permet de connaître
# 20 Tas # 22 ¨ ru_minflt : nombre de défauts de page mineurs, c’est-à-dire pages inactives
jamais écrites finalement chargées en mémoire
_end ¨ ru_majflt : nombre de défauts de page majeurs, c’est-à-dire pages inactives
bss Variables globales non initialisées déjà écrites et déchargées (swappées) sur disque
_data
¥ /usr/bin/time nomExécutable
Variables statiques
data affiche toutes ces informations pour le processus nomExécutable lancé en mode
Variables globales initialisées
_etext commande
text Code exécutable
0

& % & %

– 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 Observation et contrôle de l’utilisation de la mémoire
2.2.2 Observation du type d’accès à la mémoire

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


# 21 2.2.2 Observation du type d’accès à la mémoire . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
# 23 ¥ L’outil valgrind (logiciel libre, http://valgrind.org) permet d’observer tous les
2.2.3 Contrôle du volume de pages : être attentif à l’alignement des structures . . . . . . 24
types d’accès à la mémoire effectués par un programme
2.3.0 Contrôle des défauts de page . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
valgrind --tool=cachegrind nomProgramme

& %
& %

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).

TELECOM SudParis — Michel Simatic — Mai 2010 — module CSC4508/M2 35 TELECOM SudParis — Michel Simatic — Mai 2010 — module CSC4508/M2 36
2 Point de vue processus 2.2 Observation et contrôle de l’utilisation de la mémoire 2 Point de vue processus 2.2 Observation et contrôle de l’utilisation de la mémoire

Rappelons le contenu de gentil.c : ==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.
/* gentil.c */ ==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.
#include "constantes.h" ==5560== For more details, rerun with: -v
==5560==
char t[NBRE][PAGE]; --5560-- warning: Pentium 4 with 12 KB micro-op instruction trace cache
--5560-- Simulating a 16 KB I-cache with 32 B lines
int main() { ==5560==
int i,j,k; ==5560== I refs: 94,524,470
for (i=0 ; i<2 ; i++) { ==5560== I1 misses: 921
for (j=0 ; j<NBRE ; j++) { ==5560== L2i misses: 526
for (k=0 ; k<PAGE ; k++) { ==5560== I1 miss rate: 0.00%
t[j][k] = 1; ==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)
return 0; ==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% )
Et celui de mechant.c : ==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)
/* mechant.c */ ==5560== L2 miss rate: 7.1% ( 0.0% + 99.8% )
/*************/

#include "constantes.h" En écriture, on constate qu’avec gentil, les données ne sont pas trouvées dans le cache dans 1, 5% des
char t[NBRE][PAGE];
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).
int main() {
int i,j,k;
NB :
for (i=0 ; i<2 ; i++) { – Pour mechant, on fait globalement 2 × N BRE × P AGE = 2 × BY T ES = 10.485.760 écritures sur le
for (k=0 ; k<PAGE ; k++) {
for (j=0 ; j<NBRE ; j++) {
tableau t, ce qui explique le nombre d’écritures observées avec valgrind.
t[j][k] = 1; – 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,
}
return 0; [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
Pour ne pas avoir des temps d’exécution trop longs avec valgrind, le fichier constantes.h définit un est muni d’un cache en écriture de 64 octets (cf. 64 B dans la ligne D1 cache). Par conséquent, dans
tableau de « seulement » 5 Mo :
le cas de gentil qui fait des accès contigus, le processeur n’a besoin d’accéder à la mémoire physique
/* constantes.h */ que tous les 64 octets. Or 10.485.760/64 = 163.840 : on retrouve le nombre de D1 misses en écriture
de gentil.
' $
#define NBMEG 5

#define BYTES (NBMEG * 1<<20)


#define PAGE 4096
#define NBRE (BYTES/PAGE) 2.2.3 Contrôle du volume de pages : être attentif à l’alignement
des structures
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. ¥ Par défaut, le compilateur aligne sur des frontières de 4 octets (2 octets pour les
==5534== Copyright (C) 2000-2005, and GNU GPL’d, by Julian Seward et al.
==5534== For more details, rerun with: -v short)
==5534==
--5534-- warning: Pentium 4 with 12 KB micro-op instruction trace cache ¥ Pour récupérer cet espace perdu
--5534-- Simulating a 16 KB I-cache with 32 B lines # 24 ¨ Le mieux est de réordonnancer les feuilles des structures
==5534==
==5534== I refs: 94,485,037 ¨ On peut aussi compacter toutes les structures d’un source
==5534== I1 misses: 921
==5534== L2i misses: 526 gcc -fpack-struct
==5534== I1 miss rate: 0.00%
==5534== L2i miss rate: 0.00% ¨ Ou bien compacter juste une structure donnée
==5534==
==5534== D refs: 52,486,117 (41,985,225 rd + 10,500,892 wr)
typedef struct {
==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% ) } __attribute__((packed)) uneStructure ;
==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 :

TELECOM SudParis — Michel Simatic — Mai 2010 — module CSC4508/M2 37 TELECOM SudParis — Michel Simatic — Mai 2010 — module CSC4508/M2 38
Gestion de la mémoire 2 Point de vue processus 2 Point de vue processus 2.3 Allocation/Désallocation dynamique de mémoire
' $
Dans le cas où le compactage d’une structure donne lieu à un alignement inefficace (par exemple,
int à cheval sur une frontière de 4 octets), le compilateur ne produit aucun warning du type WARNING :
2.3 Allocation/Désallocation dynamique de mémoire
inefficient alignment.

http://www.gamasutra.com/view/feature/3942/data_alignment_part_1.php (et
http://www.gamasutra.com/view/feature/3975/data_alignment_part_2_objects_on_.php) présente(nt)
une autre motivation liée à l’alignement (sur des frontières de 16 octets) : l’utilisation d’instructions 2.3.1 Allocation/Désallocation mémoire standard . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
vectorielles SSE des processeurs Intel/AMD. Ces instructions permettent de faire des opérations sur 4 2.3.3 Digression : algorithmes pour malloc . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
flottants (au sens float du langage C) en parallèle, mais requièrent que chaque groupe de 4 flottants soit # 26 2.3.3 Digression : algorithmes pour free . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
aligné sur une frontière de 16 octets. 2.3.6 Désallocation automatique avec alloca . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
2.3.6 Mécanisme d’allocation/désallocation dédié . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
2.3.6 Allocation/Désallocation mémoire au niveau système . . . . . . . . . . . . . . . . . . . . . . . . . 32

' $
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 2.3.1 Allocation/Désallocation mémoire standard
¨ 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
# 25 ¨ int munlock(const void *addr, size_t len)
déverrouille ¥ void *malloc (size_t size)
¨ int mlockall(int flags) renvoie un pointeur sur une zone de size octets
permet de tout verrouiller en mémoire centrale. flags vaut une combinaison de ¥ void *realloc (void *ptr, size_t size)
change la taille d’une zone mémoire réservée précédemment par malloc(3)
◮ MCL_CURRENT : Verrouiller toutes les pages correspondant actuellement à # 27
l’espace d’adressage du processus ¥ void *calloc (size_t nmemb, sizeoi_t size)
◮ MCL_FUTURE : Verrouiller toutes les pages qui seront dans l’espace Même rôle que malloc, mais avec initialisation de la mémoire à 0
d’adressage ¥ void free (void *ptr)
¨ int munlockall(void) Libération de zone

& %
déverrouille tout ¥ int mallopt (int parametre, int valeur)
Contrôle de paramètres de fonctionnement des fonctions précédentes

& %

– 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 :
Sous Linux, on ne peut verrouiller au maximum que la moitié de la mémoire physique du système : – 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).
– De plus, en affectant __malloc_hook, __realloc_hook et __free_hook, on peut personnaliser le
– mlock(2) et mlockall(2) peuvent donc échouer. comportement des routines d’allocation/libération standard.
– Dans le cas où ils réussissent, un malloc(3) subséquent peut échouer. – http://blog.pavlov.net/2007/11/21/malloc-replacements évoque des alternatives à l’algorithme stan-

TELECOM SudParis — Michel Simatic — Mai 2010 — module CSC4508/M2 39 TELECOM SudParis — Michel Simatic — Mai 2010 — module CSC4508/M2 40
2 Point de vue processus 2.3 Allocation/Désallocation dynamique de mémoire 2 Point de vue processus 2.3 Allocation/Désallocation dynamique de mémoire

dard de malloc. de gcc) :


– Quand on libère une zone avec free, il est vivement conseillé d’affecter NULL au pointeur qu’on avait – La recherche d’une zone libre se fait selon un ordre best-fit.
sur cette zone. Cela permet un plantage net si, par erreur, dans la suite du programme, on cherche à – L’utilisation d’index permet de traiter une demande d’allocation en une douzaine d’instructions.
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 poin- ' $
teur avant de faire malloc, c’est-à-dire d’avoir systématiquement la séquence : assert(p != NULL) ;
p = malloc(taille) ; ...
2.3.4 Désallocation automatique avec alloca
' $
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
¥ void *alloca (size_t size)
¥ Meilleur choix (best fit) : trous dans l’ordre croissant des tailles
# 30 alloue size octets dans l’espace de pile de l’appelant
(x1 < x2 < · · · < xn ) et choix du plus petit trou de taille suffisante
¥ Donc, quand on retourne de la fonction qui a appelé alloca, l’espace alloué est
¥ Plus mauvais choix (worst fit) : trous dans l’ordre décroissant des tailles
automatiquement libéré
# 28 (x1 > x2 > · · · > xn ) et allocation de tout ou partie du premier trou
¥ Problème : On risque de déborder de la pile. . . sans aucun avertissement du système
¥ 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.5 Mécanisme d’allocation/désallocation dédié
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 ¥ Principe
la plus grande possible ¨ Au démarrage du programme, on construit une liste chaînée de blocs de N octets
# 31 ¨ Quand on a besoin d’allouer un bloc de N octets, on récupère le premier élément
¥ Objectif : reconstruire la liste des zones libres en cherchant à reconstruire le plus
# 29 grand trou possible de cette liste
¥ Plusieurs cas à envisager ¨ Quand on veut désallouer un bloc de N octets, on remet ce bloc en tête de liste
¨ Zone libérée entre 2 zones occupées ¥ Avantage : Vitesse d’allocation/désallocation
¨ Zone libérée entre une zone occupée et une zone libre ¥ Inconvénient : Les blocs alloués/désalloués ont tous la même taille
¨ 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

& %
L’algorithme de malloc étant très efficace, le mécanisme présenté ici n’est en général pas nécessaire.
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 Il peut toutefois s’imposer dans le cas où l’on doit faire une allocation/désallocation dans un gestionnaire
indications pour comprendre l’implémentation visible dans les sources mmalloc.c, mfree.c et mmprivate.h de signal (dans lequel les opérations de malloc/free sont proscrites).

TELECOM SudParis — Michel Simatic — Mai 2010 — module CSC4508/M2 41 TELECOM SudParis — Michel Simatic — Mai 2010 — module CSC4508/M2 42
2 Point de vue processus 2.4 Déverminage des accès mémoire 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 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,


¥ void *sbrk(ptrdiff_t incrément)
au moment de la compilation, que
incrémente l’espace de données du programme de incrément octets
¨ Il n’accède pas en dehors de la zone (ou que la zone n’est pas libérée)
¥ void * mmap(void *start, size_t length, int prot, int flags, int
# 32 # 34 ¨ Il ne lit pas un octet jamais initialisé
fd, off_t offset)
¨ ...
Permet de mapper un fichier en mémoire. En plus des options qui seront étudiées
¥ Outils
dans le chapitre « Entrées/sorties », flags peut prendre la valeur MAP_ANON (ou
¨ insure++ (société Parasoft) : propose un outil d’analyse des sources
MAP_ANONYMOUS) pour indiquer qu’on ne souhaite pas réellement travailler sur un
¨ splint (logiciel libre, http://www.splint.org/) : outre des contrôles
fichier, mais sur une zone mémoire vierge emplie de zéros
« 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 ces outils, étudiez le programme C suivant (qui s’exécute correctement). Il
contient 3 erreurs. Les voyez-vous ?
/********************/
/* jeuDes3Erreurs.c */
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). /* 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>

2.4 Déverminage des accès mémoire #define CITATION "Ce n’est pas le chemin qui est difficile, c’est le difficile qui est chemin" // Manu Larcenet (Le Combat Ordinaire T2)

int main(){
char *p;

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


p = (char*)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 */
# 33 2.4.1 Détecter statiquement les accès erronés à des zones allouées dynamiquement . . 34 p[1] = ’\0’;
printf("Contenu pointe par p avant strcpy : \"%s\"\n", p);
2.4.2 Détecter dynamiquement les accès erronés à des zones allouées dynamiquement35
/* C’est parti pour le strcpy */
2.4.3 Visualiser quand une zone mémoire est accédée/modifée . . . . . . . . . . . . . . . . . . . . . . 36 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);

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


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

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

TELECOM SudParis — Michel Simatic — Mai 2010 — module CSC4508/M2 43 TELECOM SudParis — Michel Simatic — Mai 2010 — module CSC4508/M2 44
2 Point de vue processus 2.4 Déverminage des accès mémoire 2 Point de vue processus 2.4 Déverminage des accès mémoire

jeuDes3Erreurs.c: (in function main) strcpy(p,CITATION);


jeuDes3Erreurs.c:25:58: Passed storage p not completely defined (*p is
undefined): printf (..., p, ...) /* On regarde l’affichage */
Storage derivable from a parameter, return value or global is not defined. printf("Contenu pointe par p apres strcpy : \"%s\"\n", p);
Use /*@out@*/ to denote passed or returned storage which need not be defined.
(Use -compdef to inhibit warning) /* On libere la zone pointee par p puisqu’elle est desormais inutile */
jeuDes3Erreurs.c:16:3: Storage *p allocated free(p);
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 /* On verifie que l’affichage de p conduit desormais a une erreur */
or assigning to an only global). (Use -usereleased to inhibit warning) printf("Contenu pointe par p apres free : \"%s\"\n", p);
jeuDes3Erreurs.c:34:8: Storage p released
return EXIT_SUCCESS;
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 L’exécution de valgrind (valgrind --̇tool=memcheck --̇leak-check=yes jeuDes3Erreurs) sur un
(si on met p[0], l’erreur disparaît). 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.
2.4.2 Détecter dynamiquement les accès erronés à des zones ==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.
allouées dynamiquement ==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)
¥ Objectif : quand un programme accède à une zone allouée dynamiquement, vérifier, à ==2402== by 0xB2F942: printf (in /lib/libc-2.3.5.so)
==2402== by 0x80484DC: main (jeuDes3Erreurs.c:25)
l’exécution, que ==2402==
¨ Il n’accède pas en dehors de la zone (ou que la zone n’est pas libérée) ==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
# 35 ¥ Outils ==2402== at 0x1B909222: malloc (vg_replace_malloc.c:130)
¨ insure++ (société Paradox ) : au moment de la compilation, cet outil ajoute aux sources ==2402== by 0x8048499: main (jeuDes3Erreurs.c:16)
==2402==
(.c) des instructions de contrôle au niveau des différentes instructions d’affectation ==2402== Invalid read of size 1
¨ Rational Purify (société IBM) : au moment du link, cet outil ajoute aux objets (.o) ==2402== at 0xB2A4CF: vfprintf (in /lib/libc-2.3.5.so)
==2402== by 0xB2F942: printf (in /lib/libc-2.3.5.so)
des instructions de contrôle au niveau des différentes instructions (assembleur) ==2402== by 0x804850C: main (jeuDes3Erreurs.c:31)
==2402== Address 0x1B92B073 is 0 bytes after a block of size 75 alloc’d
d’affectation ==2402== at 0x1B909222: malloc (vg_replace_malloc.c:130)
¨ valgrind (logiciel libre, http://valgrind.org) : l’exécutable est lu par un simulateur de ==2402== by 0x8048499: main (jeuDes3Erreurs.c:16)
==2402==
processeur x86 qui se charge de détecter toutes les anomalies d’accès ==2402== Invalid read of size 1
==2402== at 0xB2A4CF: vfprintf (in /lib/libc-2.3.5.so)
¥ Limite = On n’est jamais sûr d’être passé par toutes les branches ==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==
Pour illustrer la puissance de ces outils, étudiez le programme C suivant (qui s’exécute correctement). Il ==2402== Invalid read of size 1
contient 3 erreurs. Les voyez-vous ? ==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)
/* jeuDes3Erreurs.c */ ==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)
/* Ce programme a pour objectif d’illustrer la fonction strcpy */ ==2402== by 0x804851A: main (jeuDes3Erreurs.c:34)
/* Il s’execute correctement. Pourtant, il contient 3 erreurs ! */ ==2402==
/* Pouvez-vous les retrouver ? */ ==2402== Invalid read of size 1
#include <string.h> ==2402== at 0xB4B95F: _IO_file_xsputn@@GLIBC_2.1 (in /lib/libc-2.3.5.so)
#include <stdlib.h> ==2402== by 0xB2A140: vfprintf (in /lib/libc-2.3.5.so)
#include <stdio.h> ==2402== by 0xB2F942: printf (in /lib/libc-2.3.5.so)
==2402== by 0x804852D: main (jeuDes3Erreurs.c:37)
#define CITATION "Ce n’est pas le chemin qui est difficile, c’est le difficile qui est chemin" // Manu Larcenet (Le Combat Ordinaire T2) ==2402== Address 0x1B92B071 is 73 bytes inside a block of size 75 free’d
==2402== at 0x1B909743: free (vg_replace_malloc.c:152)
int main(){ ==2402== by 0x804851A: main (jeuDes3Erreurs.c:34)
char *p; ==2402==
==2402== Invalid read of size 1
/* Reservation d’une zone pour accueillir la citation */ ==2402== at 0xB57465: mempcpy (in /lib/libc-2.3.5.so)
p = (char*)malloc(strlen(CITATION)); ==2402== by 0xB2A140: vfprintf (in /lib/libc-2.3.5.so)
if (p == NULL){ ==2402== by 0xB2F942: printf (in /lib/libc-2.3.5.so)
fprintf(stderr,"malloc p NOK"); ==2402== by 0x804852D: main (jeuDes3Erreurs.c:37)
return EXIT_FAILURE; ==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)
/* On met ’\0’ au niveau du premier caractere pointe par p pour que */ ==2402==
/* le printf qui suit n’affiche pas n’importe quoi */ ==2402== Invalid read of size 2
p[1] = ’\0’; ==2402== at 0xB5746A: mempcpy (in /lib/libc-2.3.5.so)
printf("Contenu pointe par p avant strcpy : \"%s\"\n", p); ==2402== by 0xB2A140: vfprintf (in /lib/libc-2.3.5.so)
==2402== by 0xB2F942: printf (in /lib/libc-2.3.5.so)
/* C’est parti pour le strcpy */ ==2402== by 0x804852D: main (jeuDes3Erreurs.c:37)

TELECOM SudParis — Michel Simatic — Mai 2010 — module CSC4508/M2 45 TELECOM SudParis — Michel Simatic — Mai 2010 — module CSC4508/M2 46
2 Point de vue processus 2.4 Déverminage des accès mémoire 0 Point de vue processus 0.4 Déverminage des accès mémoire

==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 : 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 valgrind.
NB2 : 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 utilisables (contrainte de temps d’exécution, par exemple) :
– mtrace permet un suivi intégré des allocations/désallocations
On peut ainsi repérer des fuites mémoire
– mcheck permet la surveillance automatique des zones allouées
On peut ainsi répérer des débordements de zone
– Citons également ccmalloc et electric fence
' $
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.
# 36
¥ 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.

TELECOM SudParis — Michel Simatic — Mai 2010 — module CSC4508/M2 47 TELECOM SudParis — Michel Simatic — Mai 2010 — module CSC4508/M2 48
Les fichiers (et les entrées-sorties)
' $
Plan du document

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


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

Les fichiers (et les entrées-sorties)


& %

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
Michel Simatic 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
module CSC4508/M2 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.
– 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 explicitement
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.
– 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
Mai 2010 séquentiel indexé, il faut s’appuyer sur une librairie (Unix NDBM, GDBM, Oracle Berkeley DB. . .).

49 TELECOM SudParis — Michel Simatic — Mai 2010 — module CSC4508/M2 50


1 Primitives Unix d’entrées-sorties 1.1 Primitives de base 1 Primitives Unix d’entrées-sorties 1.1 Primitives de base
' $ ' $
1 Primitives Unix d’entrées-sorties 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
1.1 Primitives de base . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
#3 #5 ¨ O_RDWR : écriture et lecture
1.2 Duplication de descripteur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
associée à :
1.3 Contrôle des entrée-sorties . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
¨ O_NDELAY : pas de blocage en lecture
2.0 Manipulation de l’offset . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
¨ 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
¥ int close(int desc)

& % & %

' $
1.1 Primitives de base 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
#4 1.1.1 Ouverture/fermeture de fichier . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
mémoire
1.1.2 Lecture de fichier . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 – Il y aussi risque d’incohérence sur les données qu’on trouve dans le fichier
1.1.3 Écriture de fichier . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 – 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)

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 — Mai 2010 — module CSC4508/M2 51 TELECOM SudParis — Michel Simatic — Mai 2010 — module CSC4508/M2 52
1 Primitives Unix d’entrées-sorties 1.1 Primitives de base Les fichiers (et les entrées-sorties) 1 Primitives Unix d’entrées-sorties
' $
Noter que quand le fichier est ouvert avec l’option O_APPEND, si P1 et P2 écrivent simultanément (à la fin
1.1.2 Lecture de fichier
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 fichiers exécutant simultanément les opérations :
ssize_t read(int fd, void *buf, size_t count) : retour = nombre d’octets
lseek(fd,0,SEEK_END); /* Fixe la position courante à la fin du fichier */
lus write(fd,data,taille);
#6 ¥ Au retour de la lecture, la zone buf a été modifiée avec le résultat de la lecture
¥ Le nombre d’octets lus peut ne pas être égal à count : En effet, dans ce dernier cas, l’une des écritures risque d’être écrasée par l’autre.
¨ On se trouve à la fin du fichier
¨ On a fait une lecture non bloquante et les données étaient verrouillées de Le fichier copier.c de la page suivante illustre l’utilisation de open, read, write et close.
manière exclusive
/************/
/* copier.c */
/************/
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>

& %
#include <fcntl.h>
#include <string.h>
#include <stdio.h>

Dans le cas où la fonction read est utilisé sur un descripteur autre qu’un fichier (tube, socket), le fait #define USAGE "USAGE = copier nomSource nomDest\n"
#define ERRECRIT "Erreur d’ecriture (disque plein ?)\n"
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 int source, dest;
int buf;
fermé son extrémité du tube. int nbCarLu, nbCarEcrit;
– Pour une socket (cf. cours ASR4), le protocole réseau utilise des paquets de données de taille inférieure int main(int argc, char *argv[]) {
à celle qui est réclamée.
' $
if (argc != 3) {
write(STDERR_FILENO, USAGE, strlen(USAGE));
return EXIT_FAILURE;
}
1.1.3 Écriture de fichier 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]);
ssize_t write(int fd, const void *buf, size_t count) : retour = nombre return EXIT_FAILURE;
}
d’octets écrits while ((nbCarLu = read(source, (void*)&buf, sizeof(buf))) > 0) {

#7 ¥ Le retour (sans erreur) de l’écriture signifie que : nbCarEcrit = write(dest, (void*)&buf, nbCarLu);
if (nbCarEcrit <= 0) {
¨ Les octets ont été écrits dans les caches du noyau si pas de O_SYNC au moment if (nbCarEcrit == 0) {
write(STDERR_FILENO, ERRECRIT, strlen(ERRECRIT));
du open }
else {
¨ Les octets ont été écrits sur disque si O_SYNC au moment du open. perror("write");
¥ Si le nombre d’octets écrits est différent de count, cela signifie une erreur (disque }
return EXIT_FAILURE;
plein, par exemple) }
}
if (nbCarLu < 0) {
perror("read");
return EXIT_FAILURE;
}
if (close(source) < 0) {

& %
perror(argv[1]);
return EXIT_FAILURE;
}
if (close(dest) < 0) {
L’écriture sur diques est atomique : si deux processus P1 et P2 écrivent simultanément dans le même perror(argv[2]);
fichier au même endroit, lorsque les deux processus auront fini leur écriture, on trouvera à cet endroit : return EXIT_FAILURE;
}
– Soit l’écriture faite par P1 , return EXIT_SUCCESS;
– Soit l’écriture faite par P2 , }

– Mais jamais un mélange des écritures faites par P1 et P2 .

TELECOM SudParis — Michel Simatic — Mai 2010 — module CSC4508/M2 53 TELECOM SudParis — Michel Simatic — Mai 2010 — module CSC4508/M2 54
1 Primitives Unix d’entrées-sorties 1.3 Contrôle des entrée-sorties 1 Primitives Unix d’entrées-sorties 1.3 Contrôle des entrée-sorties
' $ ' $
1.2 Duplication de descripteur 1.3.1 Primitive fcntl

¥ Mécanisme principalement utilisé pour réaliser des redirections des trois fichiers
¥ int fcntl(int fd, int cmd, ...) : retour fonction du type d’opération
d’entrée-sorties standard.
permet la réalisation d’un certain nombre d’opérations à différents niveaux dans les
¥ int dup(int fdACloner) : retour = nouveauFd
tables du système
#8 associe le plus petit descripteur disponible du processus appelant à la même entrée # 10
¥ Les commandes possibles sont :
dans la table des fichiers ouverts que le descripteur fdACloner
¨ F_GETFL : Valeur des attributs utilisés au moment de l’ouverture du fichier
¥ int dup2(int fdACloner, int fdClone)
¨ F_SETFL : Positionnement de certains de ces attributs (NB : ces attributs sont
force le descripteur fdClone à devenir un synonyme du descripteur fdACloner. Si le
communs à tous les processus manipulant le fichier concerné)
descripteur fdClone n’est pas disponible, le système réalise au préalable une
¨ Opérations liées au verrouillage
opération de fermeture close(fdClone)

& % & %

' $
1.3.2 Verrouillage de fichier

Ces primitives seront mises en œuvre dans le chapitre « Communications inter-processus ».

' $
¥ 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)
1.3 Contrôle des entrée-sorties correspondant à ce nœud
# 11 ¥ Un verrou est la propriété d’un processus : ce processus est le seul habilité à le
modifier ou l’enlever
¥ Les verrous ont une portée de [entier1 : entier2] ou [entier : ∞]
¥ Les verrous ont un type :
¨ partagé (shared)
¨ exclusif (exclusive)

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


1.3.2 Verrouillage de fichier . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
& %

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 — Mai 2010 — module CSC4508/M2 55 TELECOM SudParis — Michel Simatic — Mai 2010 — module CSC4508/M2 56
1 Primitives Unix d’entrées-sorties 1.3 Contrôle des entrée-sorties Les fichiers (et les entrées-sorties)

perror("open"); – Si on exécute vExcl en premier, un autre vExcl ou un vPart doivent attendre avant de pouvoir poser
}
exit(EXIT_FAILURE);
leur verrou.
– Si on exécute vPart en premier, un autre vPart peut poser le verrou (partagé). En revanche, un vExcl
/* Verrou exclusif sur le 15è caractère */
verrou.l_type = F_WRLCK;
doit attendre avant de pouvoir poser son verrou.
verrou.l_whence = SEEK_SET; – Noter qu’on peut avoir une famine de vExcl :
verrou.l_len = 1;
verrou.l_start = 15;
– On démarre un 1er vPart.
/* A cause du parametre F_SETLKW, on se bloque sur le fcntl si jamais */ – On démarre vExcl : il se met en attente.
/* le verrou ne peut pas etre pris */
printf("tentative de pose de verrou (exclusif) par processus %d...\n",
– On démarre un 2e vPart. Le 1er vPart se termine. Mais comme le 2è vPart est en train de s’exécuter,
getpid()); vExcl attend encore.
if (fcntl(fd, F_SETLKW, &verrou) < 0){
perror("Pose verrou");
– On démarre un 3e vPart. Le 2e vPart se termine. Mais comme le 3e vPart est en train de s’exécuter,
exit(EXIT_FAILURE); vExcl attend encore.
}
printf("... Verrou (exclusif) pose par processus %d\n", getpid());
– 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.
/* Ici on pourrait faire le traitement qui necessitait d’etre protege */
/* par le verrou */
Pour empêcher cette famine, il faut ajouter une exclusion mutuelle (cf. chapitre « Synchronisation de
sleep(10); processus »).
/* Liberation du verrou */
printf("Relachement verrou par processus %d...\n", getpid());
verrou.l_type = F_UNLCK;
if (fcntl(fd, F_SETLK, &verrou) < 0){
perror("Relachement verrou");
exit(EXIT_FAILURE);
}

' $
printf("...OK\n");

return EXIT_SUCCESS;
} 1.4 Manipulation de l’offset
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> ¥ off_t lseek(int fd, off_t unOffset, int origine) : retour = nouvel
int main(){
int fd;
emplacement
struct flock verrou; # 12 permet de manipuler l’offset du fichier
fd = open("/tmp/ficTest",O_RDWR|O_CREAT, S_IRWXU|S_IRWXG|S_IRWXO); ¥ origine permet de préciser la manière de prendre en compte le paramètre offset :
if (fd < 0) {
perror("open"); ¨ SEEK_SET : of f set = unOf f set
}
exit(EXIT_FAILURE);
¨ SEEK_CUR : of f set = of f set + unOf f set
¨ SEEK_END : of f set = f inF ichier + unOf f set
/* Verrou partage sur le 15e caractere */
verrou.l_type = F_RDLCK;
verrou.l_whence = SEEK_SET;
verrou.l_len = 1;
verrou.l_start = 15;
/* 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;
if (fcntl(fd, F_SETLK, &verrou) < 0){
perror("Relachement verrou"); lseek peut être utilisé pour faire un fichier d’index permettant de retrouver plus rapidement des infor-
exit(EXIT_FAILURE); mations dans un fichier textuel (cf. exercice Entrées-sorties/5). Toutefois, avant d’engager un développement
}
printf("...OK\n"); 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
return EXIT_SUCCESS;
} section 2.6).

TELECOM SudParis — Michel Simatic — Mai 2010 — module CSC4508/M2 57 TELECOM SudParis — Michel Simatic — Mai 2010 — module CSC4508/M2 58
Les fichiers (et les entrées-sorties) 2 Bibliothèque C d’entrées-sorties 2 Bibliothèque C d’entrées-sorties 2.3 Lecture de fichier
' $ ' $
2 Bibliothèque C d’entrées-sorties 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
2.2 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
¨ FILE *fdopen (int fd, const char *mode)
2.2 Ouverture/fermeture de fichier . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
# 13 # 15 permet d’associer un flux au descripteur de fichier fd obtenu, par exemple, à
2.3 Lecture de fichier. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .17
l’aide de open
2.4 Écriture de fichier . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
¨ FILE *freopen (const char *path, const char *mode, FILE *stream)
2.6 Contrôle du tampon . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
Fonctionnalité similaire à dup2
2.6 Divers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
¨ 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.1 Introduction
2.3 Lecture de fichier

¥ int fgetc (FILE *stream)


¥ L’ensemble des fonctions vues jusqu’à présent sont des appels système spécifiques lit un caractère
Unix : leur utilisation risque de compromettre la portabilité d’une application ¥ int getc (FILE *stream)
# 14 ¥ Le langage C définit des mécanismes standard d’entrées-sorties. Leur utilisation équivalent de fgetc, mais implémenté sous forme de macro (→ a priori, plus efficace)
garantit donc la portabilité d’une application ¥ int getchar (void)
# 16 lit un caractère sur stdin
¥ Néanmoins l’implémentation de ces mécanismes standards est une surcouche au
¥ char *fgets (char *s, int size, FILE *stream)
dessus des primitives systèmes : ces mécanismes standard ont une pénalité en terme
lit au plus size − 1 caractères jusqu’à retour-chariot ou EOF
de performances
¥ 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 — Mai 2010 — module CSC4508/M2 59 TELECOM SudParis — Michel Simatic — Mai 2010 — module CSC4508/M2 60
2 Bibliothèque C d’entrées-sorties 2.4 Écriture de fichier 2 Bibliothèque C d’entrées-sorties 2.4 Écriture de fichier
' $ ' $
Lecture de fichier (suite) Écriture de fichier (suite)
¥ int fscanf (FILE *stream, const char *format, ...) ¥ int fprintf (FILE *stream, const char *format, ...)
fait une lecture formatée fait une écriture formatée
¥ int scanf (const char *format, ...) ¥ int printf (const char *format, ...)
fait une lecture formatée sur stdin fait une écriture formatée sur stdout
¥ Pour toutes les opérations de lecture, la lecture en fin de fichier déclenche le
positionnement d’un indicateur testable par feof()

# 17 # 19

& % & %
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
Toujours tester la valeur renvoyée par fscanf ou consœurs. Cela évitera des surprises. 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.
La fonction sscanf permet de faire une lecture formatée d’une zone mémoire.
/*************/
/* copier2.c */

' $
/*************/
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
2.4 Écriture de fichier #include <errno.h>

FILE *source, *dest;


char buf[1024];
int nbElemLu, nbElemEcrit;

¥ int fputc (int c, FILE *stream) int main(int argc, char *argv[]) {
if (argc != 3) {
écrit un caractère fputs("USAGE = copier2 nomSource nomDest\n", stderr);
return EXIT_FAILURE;
¥ int putc (int c, FILE *stream) }
équivalent de fputc, mais implémenté sous forme de macro (→ a priori, plus efficace) source = fopen(argv[1], "r");
if (source == NULL) {
¥ int putchar (int c) fprintf(stderr,
# 18 écrit un caractère sur stdout "Erreur ’%s’ lors ouverture fichier %s\n",
strerror(errno),
¥ int fputs (const char *s, FILE *stream) argv[1]);
return EXIT_FAILURE;
écrit une chaîne de caractères }
¥ int puts (const char *s) dest = fopen(argv[2], "w");
if (dest == NULL) {
écrit une chaîne de caractères sur stdout fprintf(stderr,
¥ size_t fwrite (const void *ptr, size_t size, size_t nmemb, FILE *stream) : "Erreur ’%s’ lors ouverture fichier %s\n",
strerror(errno),
retour = nombre d’éléments écrits (et non le nombre d’octets écrits) argv[2]);
return EXIT_FAILURE;
écrit un tableau d’objets }
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 — Mai 2010 — module CSC4508/M2 61 TELECOM SudParis — Michel Simatic — Mai 2010 — module CSC4508/M2 62
Les fichiers (et les entrées-sorties) 2 Bibliothèque C d’entrées-sorties Les fichiers (et les entrées-sorties)

return EXIT_FAILURE; Il existe aussi des fonctions de « bases de données », plus précisément des accès aux fichiers plus évolués
}
if (fclose(source) < 0) {
que l’accès séquentiel ou direct : fichiers séquentiels indexés (avec des clés d’accès), arbres binaires. . .
perror(argv[1]); – Package Unix NDBM : fonction dbm_open. . . (utilisé pour services réseau NIS, par exemple),
return EXIT_FAILURE;
}
– Package GDBM : extensions GNU (fonction gdbm_open. . . ),
if (fclose(dest) < 0) { – Package Oracle Berkeley DB : fonction dbopen. . . (fichiers accessible à partir du langage C, Java,
perror(argv[2]);
return EXIT_FAILURE;
Perl. . .).
} Voir man ou bien [Blaess, 2002] pour plus d’informations.
' $
return EXIT_SUCCESS;
}

' $
3 Projection des fichiers en mémoire
2.5 Contrôle du tampon

¥ void * mmap(void *start, size_t length, int prot, int flags, int
fd, off_t offset)
¥ int setvbuf (FILE *stream, char *buf, int buf, size_t size) # 22 permet d’établir une projection en mémoire d’un fichier (ou d’un périphérique) de
contrôle le tampon utilisé par la librairie standard pour cacher certaines opérations manière à accéder au contenu du fichier avec des instructions de manipulation
# 20 vis-à-vis du système de fichier mémoire
¥ int fflush (FILE *flux) ¥ int munmap(void *start, size_t length)
vide le tampon d’un flux supprime une projection
NB : ceci ne force pas l’écriture physique des données qui n’est assurée que par
O_SYNC ou fsync

& %

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


/************/
L’instruction setvbuf(stream, NULL, _IONBF, 0) (ou bien setbuf(file,NULL)) permet de garantir /* affic8.c */
qu’aucun buffer ne sera utilisé par la librairie standard (aucune opération d’écriture ne sera donc bufferisée). /************/

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


/* le nom est fourni en parametre */
2.6 Divers #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 fseek (FILE *stream, long offset, int whence)
int main(int argc, char *argv[]) {
contrôle l’offset int fd;
int i,j;
¥ int fileno (FILE *stream) char *tab;
int rc;
renvoie le descripteur de fichier associé au flux
# 21
¥ void clearerr (FILE *stream) if (argc < 3){
fputs("USAGE = permut nomFic valeurDeI\n", stderr);
efface indicateur d’erreur et de fin de fichier return EXIT_FAILURE;
}
¥ int feof (FILE *stream)
fd = open(argv[1], O_RDONLY);
teste l’indicateur de fin de fichier (effaçable uniquement par clearerr) if (fd < 0) {
perror("open fic");
¥ int ferror (FILE *stream) exit(1);
teste l’indicateur d’erreur (effaçable uniquement par clearerr) }
i = atoi(argv[2]);

/* mmap */
tab = (char *)mmap(0,

& %
i+7,
PROT_READ,

TELECOM SudParis — Michel Simatic — Mai 2010 — module CSC4508/M2 63 TELECOM SudParis — Michel Simatic — Mai 2010 — module CSC4508/M2 64
Les fichiers (et les entrées-sorties) Les fichiers (et les entrées-sorties)

MAP_PRIVATE, fputs("USAGE = tailleFic nomFic\n", stderr);


fd, return EXIT_FAILURE;
0); }
if (tab == MAP_FAILED){
perror("mmap"); fd = open(argv[1], O_RDONLY);
exit(1); if (fd < 0) {
} perror("open fic");
exit(1);
/* Affichage */ }
for (j=0 ; j<8 ; j++){ if (fstat(fd,&sts) < 0) {
putc(tab[i+j], stdout); perror("fstat");
} exit(1);
puts(""); }
printf("Fichier %s a %ld octets\n", argv[1], sts.st_size);
/* On ferme tout */ if (close(fd) < 0){
rc = munmap(tab,i+7); perror("close fic");
if (rc < 0){ exit(1);
perror("munmap"); }
return EXIT_FAILURE; return EXIT_SUCCESS;
} }
if (close(fd) < 0){
perror("close fic");
exit(1);
}

return EXIT_SUCCESS;
}

' $
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
' $
ino_t st_io Numéro i-nœud
# 23
Manipulation des i-noeuds du système de fichiers Unix (suite)
mode_t st_mode Protection
nlink_t st_nlink Nombre de liens matériels ¥ int access(const char *pathname, int mode)
uid_t st_uid Numéro du propriétaire teste les droits d’accès d’un processus vis-à-vis du fichier pathname
gid_t st_gid Numéro du groupe
¥ int unlink(const char *pathname)
dev_t st_rdev Type périphérique
permet de détruire un nom dans le système de fichiers
off_t st_size Taille en octets
¥ int rename(const char *OLD, const char *NEW)
time_t st_atime Date dernier accès
permet de renommer un fichier
time_t st_mtime Date dernière modification
... ... ... # 24

& %

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>
#include <stdio.h>

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


int fd;
struct stat sts;

if (argc < 2){

TELECOM SudParis — Michel Simatic — Mai 2010 — module CSC4508/M2 65 TELECOM SudParis — Michel Simatic — Mai 2010 — module CSC4508/M2 66
Les fichiers (et les entrées-sorties) 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
# 25
¨ 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
# 26 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)
& %
L’exercice 6 illustre certaines de ces limitations de NFS.

TELECOM SudParis — Michel Simatic — Mai 2010 — module CSC4508/M2 67 TELECOM SudParis — Michel Simatic — Mai 2010 — module CSC4508/M2 68
Communications inter-processus 1 Gestion des processus
' $
Plan du document

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


#2 2 Communications à l’aide de tubes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
3 Communication à l’aide des IPC System V . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
4 Comparaison des mécanismes de synchronisation. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .25

Communications inter-processus
& %

' $
1 Gestion des processus
Denis Conan et Michel Simatic

module CSC4508/M2
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

& %

Mai 2010

69 TELECOM SudParis — Denis Conan et Michel Simatic — Mai 2010 — module CSC4508/M2 70
Communications inter-processus 1 Gestion des processus Communications inter-processus 1 Gestion des processus
' $ ' $
1.1 Environnement 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
¥ Environnement = liste de chaînes de caractères (couples <nom>=<valeur>) ¨ gid_t PPID : numéro du processus chef d’un groupea , unique et modifiable
¥ Variables exportées du shell ¨ uid_t UID : numéro de l’utilisateur réel, unique et non modifiable
¥ Accessible à partir de envp ou de la variable externe environ ¨ gid_t GID : numéro du groupe de l’utilisateur réel, unique et non modifiable
#4 int main(int argc, char **argv, char **envp) #5 ¨ gid_t EUID : numéro de l’utilisateur effectif, unique et modifiable
◮ Cette forme du main est aujourd’hui dépréciée ¨ gid_t EGID : numéro du groupe de l’utilisateur effectif, unique et modifiable
¨ envp et environ possèdent la même valeur ¥ Primitives correspondantes :
¥ getenv récupère le contenu d’une variable d’environnement ¨ pid_t getpid(), pid_t getppid(), int getpgid(), int setpgid(pid_t
¨ char *getenv(char *name) pid, pid_t pgid)
¨ Valeur de retour = pointeur sur la chaîne <valeur> ou NULL ¨ 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
Voici un exemple de recherche de variables d’environnement (fichier exemple_getenv.c) : 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 à 640 Mo, le nombre permis est 4091 :
/********************/ lisible en regardant dans /proc/sys/kernel/threads-max.
/* exemple_getenv.c */
/********************/
La sémantique associée à chacune de ces primitives système est la suivante : getpid et getppid retour-
nent respectivement le numéro du processus courant et le numéro du parent du processus courant ; getuid
#include <stdio.h>
#include <stdlib.h>
et getgid retournent respectivement l’UID réel et le GID réel associés au processus courant. Les valeurs du
extern char **environ; propriétaire et du groupe effectifs déterminent les droits du processus lors des accès aux objets du système
int main(int argc, char **argv, char **envp) {
char *p_shell;
(fichiers et processus). Ces valeurs sont modifiées lorsqu’un processus correspond à l’exécution d’un pro-
char **ancien_environ = environ; gramme binaire pour lequel le bit setuid (lettre s en lieu et place du droit x) est positionné. Le propriétaire
/* parcours de l’environnement */
printf("---> envp :\n");
réel reste l’identifiant de l’utilisateur ayant lancé la commande et le propriétaire effectif devient alors l’i-
while(*envp != NULL) { dentifiant du propriétaire du fichier exécuté. C’est ainsi qu’il est possible d’exécuter des commandes comme
printf("%s\n", *envp);
envp++;
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.
printf("---> environ :\n"); Le rôle des groupes de processus permet de gérer ensemble, par exemple pour l’envoi de signaux, les
while(*environ != NULL) {
printf("%s\n", *environ);
processus créés par une même application importante. Par exemple, pour supprimer tous les processus de
environ++; 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
/* affichage de la variable SHELL si elle existe */ CTL-D qui envoie un signal d’arrêt aux deux processus mais pas au shell qui a lancé ces deux processus.
printf("---> recherche de la variable SHELL :\n");
if ((p_shell = getenv("SHELL")) != NULL) {
Le signal est donc envoyé au groupe de processus qui englobe ces deux processus. Voici un exemple de
printf("SHELL : %s\n", p_shell); manipulation des informations relatives à un processus (fichier exemple_getpid.c) :
} else {
printf("variable SHELL non définie\n"); /********************/
} /* exemple_getpid.c */
environ = ancien_environ; /********************/
printf("---> recherche de la variable SHELL (environ réinitialisée):\n");
if ((p_shell = getenv("SHELL")) != NULL) { #include <stdio.h>
printf("SHELL : %s\n", p_shell); #include <stdlib.h>
} else { #include <sys/types.h>
printf("variable SHELL non définie\n"); #include <unistd.h>
}
exit(0); 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());
Veuillez regarder aussi les fonctions putenv(3), setenv(3), unsetenv(3). printf("pgrp = %d\n\n", getpgrp());

TELECOM SudParis — Denis Conan et Michel Simatic — Mai 2010 — module CSC4508/M2 71 TELECOM SudParis — Denis Conan et Michel Simatic — Mai 2010 — module CSC4508/M2 72
Communications inter-processus 1 Gestion des processus Communications inter-processus 1 Gestion des processus

printf("uid = %d\n", getuid()); default : /* parent */


printf("euid = %d\n", geteuid()); printf("je suis le parent\n");
printf("gid = %d\n", getgid()); printf("pid = %d\n", getpid());
printf("egid = %d\n", getegid()); printf("ppid = %d\n", getppid());
printf("changement d’identité effective = %d, %d\n", setuid(0), setgid(0)); printf("pgrp = %d\n", getpgrp());
printf("uid = %d\n", getuid()); printf("uid = %d\n", getuid());
printf("euid = %d\n", geteuid()); printf("euid = %d\n", geteuid());
printf("gid = %d\n", getgid()); printf("gid = %d\n", getgid());
printf("egid = %d\n", getegid()); printf("pid de l’enfant= %d\n", pid);
break;
exit(0); }
} exit(0);

' $
}

' $
1.3 Création de processus
1.4 Terminaison de processus

¥ Terminaison :
¥ Héritage de presque tous les attributs du parent : ¨ Code retour du processus
¨ Segment text : copié ou partagé ¨ Libération des segments de mémoire
¨ Segment data : copié ¨ Entrée dans la table des processus (cf. /proc) :
¨ UID / GID réels et effectifs : conservés ◮ Gardée : stockage code retour, temps CPU. . .
#6 ¨ PID / PPID : modifiés ◮ Libérée par le processus parent (dans le wait)
#7 ¥ Attente de la terminaison de l’enfant :
¨ PGRP : conservé
¨ Informations obtenues lors de la fin du processus enfant
¨ Signaux interceptés : conservés
◮ Mode trace → octet poids fort = signal reçu, octet poids faible = 0xb1
¨ Descripteurs de fichiers / tubes : copiés
◮ exit → octet poids fort = code retour enfant, octet poids faible = 0x00
¨ Temps U.C. : remis à 0 ◮ Signal → octet poids fort = 0xb1, octet poids faible = signal reçu
¥ Primitives correspondantes : ¥ Primitives correspondantes :
pid_t fork(), pid_t vfork() 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 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 La primitive exit permet de quitter le processus courant en retournant un code de retour ; atexit permet
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 d’empiler les fonctions devant être exécutées à la terminaison du processus, le paramètre consistant en un
de l’enfant ; dans l’enfant, la valeur retournée est 0 ; en cas d’erreur, comme pour la plupart des primitives pointeur sur la fonction devant être exécutée. Le système GNU/Linux possède une primitive (on_exit) qui
systèmes, la valeur retournée par l’appel à fork dans le parent est -1. vfork est une version allégée de fork étend la primitive atexit.
ne devant être utilisée que si elle est suivie d’un exec immédiatement après. Voici un exemple de duplication Les primitives wait et waitpid permettent d’attendre la fin d’un processus enfant. Dans le premier cas,
de processus (fichier exemple_fork.c) : 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
/* exemple_fork.c */ 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
#include <stdio.h>
le PID indiqué. Le dernier paramètre permet de spécifier le comportement de la primitive ; en particulier,
#include <stdlib.h> WNOHANG permet de retourner même si aucun enfant ne s’est terminé. Voici un exemple de terminaison de
#include <sys/types.h> processus (fichier exemple_wait.c) :
#include <unistd.h>
/******************/
int main() { /* exemple_wait.c */
pid_t pid; /******************/
printf("démonstration de fork\n");
pid = fork(); #include <stdio.h>
switch (pid) { #include <stdlib.h>
case -1 : #include <sys/wait.h>
fprintf(stderr, "echec de fork\n"); #include <unistd.h>
exit(1);
case 0 : /* enfant */ void f(void) {
sleep(2); printf("On va bientot sortir\n");
printf("je suis l’enfant\n"); }
printf("pid = %d\n", getpid());
printf("ppid = %d\n", getppid()); void g(void) {
printf("pgrp = %d\n", getpgrp()); printf("On sort\n");
printf("uid = %d\n", getuid()); }
printf("euid = %d\n", geteuid());
printf("gid = %d\n", getgid()); int main() {
break; int pid, code_retour;

TELECOM SudParis — Denis Conan et Michel Simatic — Mai 2010 — module CSC4508/M2 73 TELECOM SudParis — Denis Conan et Michel Simatic — Mai 2010 — module CSC4508/M2 74
Communications inter-processus 1 Gestion des processus Communications inter-processus 2 Communications à l’aide de tubes

pid = fork(); printf("pgrp = %d\n", getpgrp());


if (pid) { /* parent */ printf("uid = %d\n", getuid());
wait(&code_retour); printf("euid = %d\n", geteuid());
code_retour = WEXITSTATUS(code_retour); printf("gid = %d\n", getgid());
printf("Parent : code de retour obtenu : %d\n", code_retour); code_retour = execlp("ps", "ps", (char *)NULL);
return(code_retour); printf("code_retour vaut %d\n",code_retour);
} else { /* enfant */ perror("pb sur exec");
atexit(g); printf("fin du programme appelant\n");
atexit(f); exit(-1);
printf("enfant : terminaison avec code de retour %d\n", 5); }
exit(5);
}

' $
return 0;
}

' $
2 Communications à l’aide de tubes
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 2.2 Principe des tubes. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .11
#9
¥ Primitives correspondantes : 2.2 Tubes ordinaires (ou locaux) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
#8 exec{l|v}[e|p] → man 3 exec 2.3 Tubes nommés . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
◮ l : liste d’arguments : fournir la liste (char *arg0, char *arg1,..., 0)
◮ v : tableau d’arguments : fournir un pointeur sur le tableau (char **argv
avec dernier élément = ’0’)
◮ e : passer un pointeur d’environnement (char **envp)
◮ 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 2.1 Principe des tubes
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) ¥ Premier moyen d’échange de données entre processus sous UNIX
– int execve(char *nom_absolu, char **argv, char **envp)
¥ Fichiers spéciaux gérés selon une méthodologie FIFO
– int execlp(char *nom_relatif, char *arg0, char *arg1,..., 0)
– int execvp(char *nom_relatif, char **argv) ¥ Taille limitée, généralement 10 blocs
Les primitives execlp et execvp agiront comme le shell dans la recherche du fichier exécutable si le nom # 10 ¥ Deux types de tubes :
fourni ne contient pas de « slash » (/). Le chemin de recherche est spécifié dans la variable d’environnement ¨ Tubes ordinaires, non visibles dans l’arborescence, entre processus d’une même
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) : filiation
¨ Tubes nommés, visibles dans l’arborescence, entre processus non forcément liés
/******************/
/* exemple_exec.c */ par filiation
/******************/
¥ Création ou ouverture de tube retournent des descripteurs de fichiers ouverts gérés
#include <stdio.h>
#include <stdlib.h> comme les autres fichiers
#include <sys/types.h>
#include <unistd.h>

& %
int main() {
int code_retour;
system("ps");
printf("pid = %d\n", getpid());
printf("ppid = %d\n", getppid());

TELECOM SudParis — Denis Conan et Michel Simatic — Mai 2010 — module CSC4508/M2 75 TELECOM SudParis — Denis Conan et Michel Simatic — Mai 2010 — module CSC4508/M2 76
Communications inter-processus 2 Communications à l’aide de tubes Communications inter-processus 2 Communications à l’aide de tubes
' $
}
2.2 Tubes ordinaires (ou locaux)
int main() {
int pid, descfich[2], code_retour;
char tampon[80];
¥ Accès par création ou héritage pipe(descfich);
pid = fork();
¥ Perte par un processus = perte d’accès définitive par ce processus switch (pid) {
case -1 :
¥ Position courante entièrement déterminée par les lectures/écritures fprintf(stderr, "echec de fork\n");
exit(1);
¨ Primitive lseek interdite case 0 : /* enfant */
¥ Taille maximum déterminée par PIPE_BUF définie dans <limits.h> close(descfich[1]); /* ... n’ecrit pas dans le tube mais lit */
enfant(descfich[0]);
¥ Opération de lecture par défaut bloquante break;
# 11 default : /* parent */
¨ Bloquant jusqu’à lecture de la taille demandée ou disparition de tous les écrivains close(descfich[0]); /* ... ne lit pas dans le tube mais ecrit */
parent(descfich[1], tampon);
¨ Pour non bloquante : utiliser fcntl avec le drapeau 0_NONBLOCK wait(&code_retour);
¥ Opération d’écriture atomique (tout est écrit) si opération bloquante }
break;

¨ Signal SIGPIPE si aucun lecteur return 0;


}
¥ Primitive correspondante : – Cet exemple permet à un parent d’envoyer des octets à un enfant. L’enfant sait que le parent a terminé
int pipe(int descfich[2]) d’écrire ses données quand le parent ferme le tube. Dans la pratique, ce moyen de savoir que le parent
◮ 0 pour la lecture et 1 pour l’écriture a terminé d’envoyer un bloc de données à l’enfant n’est pas pratique puisque le tube ne peut servir
→ Se mettre d’accord pour l’utilisation (celui qui écrit, ceux qui lisent) qu’une seule fois ! Trois méthodes plus adaptées sont à disposition :

& %
– 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 descrip-
teurs que les écrivains ne ferment pas, il est fortement conseillé de ne conserver que les descripteurs
1. Le parent envoie à l’enfant des données qui ont une taille fixe (un entier, une structure de don-
utiles et de fermer systématiquement tous les autres.
nées. . .). Le parent écrit ses données et l’enfant fait un read de taille fixe octets. read renvoie
– La taille maximum déterminée par PIPE_BUF détermine la taille des atomic write, i.e. la taille maximale
donc taille fixe (le parent a écrit une donnée) ou 0 (le parent a fermé le tube).
au delà de laquelle l’écriture risque de ne pas être atomique. En effet, si 2 processus écrivent sur le
2. Le parent peut aussi envoyer des données d’une taille variable (par exemple, une chaîne de carac-
même tube et que la taille des données écrites est proche de PIPE_BUF, il y a un fort risque que le
tères). Dans ce cas, le parent commence par envoyer un entier contenant la taille de la donnée qu’il
contenu de ces écritures soit mélangées dans le tube (un morceau de l’écriture du processus 1, puis un
s’apprête à envoyer, puis il envoie cette donnée. L’enfant commence donc par faire un premier
morceau de l’écriture du processus 2, puis un morceau de la suite de l’écriture du processus1. . .).
read de sizeof(int) pour déterminer la taille du read qu’il doit faire juste après, puis il fait
– La fonction popen engendre un processus en créant un tube, exécutant un fork et en invoquant le
le-dit read.
shell. Le tube permet, selon le paramètre fourni à popen d’être en relation avec l’entrée ou la sortie du
NB : si les tailles sont faibles (quelques dizaines, voire centaines d’octets), il est plus perfor-
processus engendré.
– Voici un exemple de communication de données avec un tube ordinaire (fichier exemple_pipe.c) : mant d’appliquer la méthode 1 en écrivant/lisant systématiquement le nombre maximum d’octets.
/******************/ Certes, on envoie des octets pour rien, mais on économise des 2 appels système par échange d’in-
/* exemple_pipe.c */
/******************/ formation 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
#include <assert.h>
#include <stdio.h> parent fait des fputs pendant que l’enfant fait des fgets.
#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"); L’implémentation de ces 3 méthodes est laissée à titre d’exercice.

TELECOM SudParis — Denis Conan et Michel Simatic — Mai 2010 — module CSC4508/M2 77 TELECOM SudParis — Denis Conan et Michel Simatic — Mai 2010 — module CSC4508/M2 78
Communications inter-processus 2 Communications à l’aide de tubes Communications inter-processus 2 Communications à l’aide de tubes
' $
2.3 Tubes nommés sprintf(reponse, "%s", "PONG");
/* Affichage */
printf("Serveur a recu \"%s\" et repond \"%s\" sur le tube nomme \"%s\"\n",
typRequete,
reponse,
¥ Création par la primitive mknod qui permet de créer les fichiers spéciaux ou par nomTubeClient );
/* Réponse sur le tube nommé du client */
mkfifo fdW = open(nomTubeClient, O_WRONLY);
if (fdW == -1) {
¨ mknod était préalablement réservée au super-utilisateur perror("open(nomTubeClient)");
exit(-1);
¥ Ouverture possible par les processus connaissant le nom du tube }
¨ Par défaut, ouverture bloquante : lecteurs et écrivains s’attendent → nbWrite = write(fdW, reponse, sizeof(reponse));
if (nbWrite < sizeof(reponse)) {

# 12 synchronisation perror("pb ecriture sur tube nomme");


}
¥ Suppression du tube lorsque explicitement demandé et pas d’ouverture en cours /* Dans cette application, le client ne renvoie pas de requête ultérieure*/
/* nécessitant une réponse ==> On peut fermer ce tube */
¨ Si suppression alors qu’il existe des lecteurs et écrivains, fonctionnement comme close(fdW);
}
un fichier ordinaire
¥ Primitives correspondantes : int main() {
char requete[TAILLEMSG];
int mknod(const char *nom_fich, mode_t mode, dev_t n_periph) int fdR;
int nbRead;
◮ Mode = S_IFIFO (ou 0010000) plus les droits sur les trois derniers octets
/* Création du tube nommé côté serveur (qui permettra les communications */
int mkfifo(const char *nom_fich, mode_t mode) /* client-serveur) */
◮ Mode = droits if (mkfifo(NOMTUBESERVEUR, S_IRUSR|S_IWUSR) < 0) {
if (errno != EEXIST) {

& %
perror("mkfifo(tube nommé serveur");
exit(1);
}
else {
Le mode indique les permissions d’accès. Ces permissions sont modifiées par la valeur d’umask du pro- printf("%s existe deja : on suppose que c’est un pipe nomme\n",
cessus : les permissions d’accès effectivement adoptées sont (mode & NOT(umask)). NOMTUBESERVEUR);
printf("et qu’on peut continuer le programme sans probleme\n");
L’open d’un tube nommé en lecture (respectivement écriture) est bloquant jusqu’à ce qu’un écrivain puts("");
(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 /* Ouverture de ce tube nommé */
de clients décrits ci-après) sur le tube nommé ./client-serveur. fdR = open(NOMTUBESERVEUR, O_RDWR);
if (fdR == -1) {
Noter que exemple_mkfifo_serveur.c ouvre le le tube qu’il crée en lecture/écriture au lieu d’une lecture perror("open(tube nommé)");
simple de sorte que : exit(-1);
}
– Il ne se bloque pas au moment du open en attendant qu’un processus ouvre le tube en écriture /* Attente de requêtes */
– Si nous ouvrons le tube en lecture seule, à chaque fois que le client qui s’est connecté se termine, do{
nbRead = read(fdR, requete, sizeof(requete));
la lecture du tube nommé va échouer car le noyau détecte une fermeture de fichier. En demandant if (nbRead != sizeof(requete)) {
l’ouverture d’un tube en lecture et en ecriture, nous évitons ce genre de situation, car il reste toujours perror("read sur tube nomme NOK");
return EXIT_FAILURE;
au moins un descripteur ouvert sur la sortie. }
/* On traite la requête */
/****************************/ traiterRequete(requete);
/* exemple_mkfifo_serveur.c */ }while(1);
/****************************/ }

#include
#include
<sys/types.h>
<unistd.h>
Le client exemple_mkfifo_client.c envoie au serveur des requêtes contenant le message PING et le nom
#include <sys/wait.h> du tube (./serveur-client.<numéroProcessusClient>) sur lequel le client attend la réponse PONG.
#include <sys/stat.h>
#include <fcntl.h> /***************************/
#include <stdio.h> /* exemple_mkfifo_client.c */
#include <stdlib.h> /***************************/
#include <string.h>
#include <errno.h> #include <sys/types.h>
#include <unistd.h>
#define TAILLEMSG 128 #include <sys/wait.h>
#define NOMTUBESERVEUR "./client-serveur" #include <sys/stat.h>
#include <fcntl.h>
void traiterRequete(char *requete){ #include <stdio.h>
char reponse[TAILLEMSG]; #include <stdlib.h>
char typRequete[128]; #include <string.h>
char nomTubeClient[128]; #include <errno.h>
int fdW;
int nbWrite; #define TAILLEMSG 128
#define NOMTUBESERVEUR "./client-serveur"
/* Analyse de la requete */
/* NB : sscanf presente l’inconvenient que si le client envoie */ int main() {
/* une chaine de caractere contenant un espace (par exemple : */ char nomTubeClient[128];
/* "PING PING") la chaine lue s’arrete au premier espace ! */ char requete[TAILLEMSG];
/* fgets serait probablement meilleur. */ char reponse[TAILLEMSG];
sscanf(requete,"%s\n%s", typRequete, nomTubeClient); int fdW, fdR;
/* Preparation de la reponse */ int nbRead, nbWrite;

TELECOM SudParis — Denis Conan et Michel Simatic — Mai 2010 — module CSC4508/M2 79 TELECOM SudParis — Denis Conan et Michel Simatic — Mai 2010 — module CSC4508/M2 80
Communications inter-processus Communications inter-processus 3 Communication à l’aide des IPC System V
' $
/* Préparation de nomTubeClient (tube qui permettra les communications */ 3 Communication à l’aide des IPC System V
/* 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 );
3.1 Inter Process Communication System V . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
printf("et qu’on peut continuer le programme sans probleme\n"); 3.2 IPC System V versus POSIX IPC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
puts("");
} # 13 3.4 Constantes relatives aux IPC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
}
/* Ouverture de ce tube */ 3.4 Files de messages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
fdR = open(nomTubeClient, O_RDWR);
if (fdR == -1) { 3.5 Mémoire partagée . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
perror("open(tube nommé)");
exit(-1);
3.6 Sémaphores IPC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
}
/* 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(-1);

& %
}
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 */ 3.1 Inter Process Communication System V
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 */ ¥ Trois types d’objets système :
close(fdR);
/* On détruit le tube nommé du client (puisque ce tube nommé ne servira */ ¨ Files de messages : échange de de flots de données formatées
/* plus jamais. */
if (unlink(nomTubeClient) < 0) { ¨ Mémoire partagée
perror("unlink"); # 14 ¨ Sémaphores : outils de synchronisation
exit(EXIT_FAILURE);
} ¥ Utilisation d’une clé pour identifier les objets système :
return EXIT_SUCCESS;
} ¨ Primitive key_t ftok(char *nom_fichier, int num_projet)
¥ Gestion des ressources :
¨ Obtention d’information sur les objets existants : ipcs
¨ Suppression d’un objet : ipcrm {sem|shm|msg} {id}+

& %

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 sont extérieurs au système de gestion
des fichiers et sortent donc de la philosophie initiale de UNIX. Ils ne sont pas désignés dans les processus
par des descripteurs de fichiers ; il n’est donc pas possible de re-diriger les entrées/sorties sur un objet de
ce type. Le système d’exploitation gère une table par type d’objets et les identifiants (ou clés) sont uniques
seulement dans un type donné.
Les IPC nécessitent une clé pour leur création. Si l’objet de type IPC n’est utilisé que par le processus qui
le crée et/ou ses descendants, la clé spéciale IPC_PRIVATE peut être utilisée ; sinon, une même clé doit être
fournie par les processus désirant accéder à l’objet. La fonction ftok permet de générer ces clés. Pour cela,
Dans cet exemple, on a donc un tube nommé pour les communications client-serveur et un tube nommé elle nécessite deux paramètres : le premier est le nom d’un fichier existant dans l’arborescence du système
(par client) pour les communications serveur-client. de fichiers et le second est un « numéro de projet » permettant de générer plusieurs clés pour un même nom

TELECOM SudParis — Denis Conan et Michel Simatic — Mai 2010 — module CSC4508/M2 81 TELECOM SudParis — Denis Conan et Michel Simatic — Mai 2010 — module CSC4508/M2 82
Communications inter-processus 3 Communication à l’aide des IPC System V Communications inter-processus 3 Communication à l’aide des IPC System V

de fichier (attention, même si ce paramètre est un int, seuls ses 8 bits de poids faible sont pris en compte). – N’intègre les POSIX Messages, sous le nom message queue, que depuis le kernel linux-2.6.6-rc1 (rap-
Par conséquent, le rôle de la fonction ftok est de relier l’espace de nommage des objets IPC au système pelons que la commande uname -r fournit le numéro de version d’un noyau Linux) :
de gestion de fichiers : une clé unique est associée à une référence de fichier plus un identifiant fourni par – man mq_open décrit la fonction d’ouverture d’une message queue et contient la liste de toutes les
l’utilisateur. La référence utilisée pour construire la clé doit être celle d’un fichier existant et si un fichier est fonctions associées pour leur manipulations.
physiquement déplacé (l’inœud a changé) entre deux appels à la fonction, la clé fournie par les deux appels – http://www.geocities.com/wronski12/posix_ipc et http://posixtest.sourceforge.net/ of-
sera différente. frent en téléchargement des exemples d’utilisation.
De nos jours, num_proj, le deuxième paramètre de ftok, est un int. Mais seuls les huit bits de poids – Les message queues sont associées aux fonctions temps réel de Linux. Donc, il faut linker les exé-
faibles sont utilisés. L’habitude veut qu’on utilise un caractère ASCII comme valeur de num_proj (ce qui cutables avec la librairie librt.a (option -lrt dans la commande d’édition de lien) et exécuter les
explique pourquoi le comportement est indéfini lorsque num_proj vaut zéro. programmes en tant que root.
Voici un exemple démontrant le lien entre les clés et les inœuds : – N’offre pas la POSIX Shared Memory,
– Offre les POSIX Semaphores (qui seront étudiés lors du chapitre « threads ») avec la restriction suiv-
/******************/
/* exemple_ftok.c */ ante : les POSIX Semaphores ne peuvent être utilisés que pour synchroniser des threads (pas des
/******************/ processus).
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>

' $
int main() {
system("touch fich1; touch fich2; ls -i fich*");
printf("Cle fich1/ projet 0 et 1 = %d, %d\n",
(int) ftok("fich1", 0), (int) ftok("fich1", 1));
printf("Cle fich2/ projet 0 et 1 = %d, %d\n",
3.3 Constantes relatives aux IPC
(int) ftok("fich2", 0), (int) ftok("fich2", 1));
/* ecrasement de fich1 par fich2 */
system("mv fich2 fich1; ls -i fich*");
printf("Cle fich1/ projet 0 et 1 = %d, %d\n",
(int) ftok("fich1", 0), (int) ftok("fich1", 1));
system("rm fich1; ls -i fich*");
exit(0);
¥ Drapeaux :
} ¨ IPC_CREAT : créer une entrée si elle n’existe pas
¨ IPC_EXCL : échouer si elle existe
Il est possible, depuis l’interpréteur de commandes de gérer les objets de type IPC. Pour cela, deux
commandes sont mises à disposition des utilisateurs. La première — ipcs — permet de lister les objets ¨ IPC_NOWAIT : ne pas attendre
ayant été créés ainsi que leurs attributs : identifiant, identifiant du propriétaire, droits d’accès. . . ; la seconde # 16 ¨ IPC_PRIVATE : clé privée, pas de lien avec le système de fichiers
— ipcrm — permet de retirer un objet de la liste (ceci est particulièrement intéressant lorsqu’une application ¥ Opérations :
ne s’est pas terminée convenablement et laisse après son exécution des objets système). La commande ipcrm ¨ IPC_RMID : suppression
doit indiquer le type d’objets concerné en plus de lídentifiant.
' $
¨ IPC_SET : mise à jour des attributs
¨ IPC_STAT : lecture des attributs
3.2 IPC System V versus POSIX IPC ¥ Permissions :
¨ Analogues à celles du système de fichiers UNIX

& %
¥ POSIX = Portable Operating System Interface (défini par IEEE).
¥ Jusqu’à présent, on a systématiquement étudié les implémentations de concepts en
présentant leur interface POSIX (garantie de portabilité).
# 15
¥ En ce qui concerne les IPC, il existe des POSIX IPC (qui sont une variation des IPC
System V).
¥ Mais les POSIX IPC ne sont que partiellement présents sur certains systèmes (dont La première opération qu’on doit réaliser sur un objet de type IPC est la prise de cet objet. Si l’objet
LINUX) alors qu’en général les IPC System V sont complètement implémentés. n’existe pas déjà dans le système, il faut préciser qu’on désire le créer (avec le drapeau IPC_CREAT), sinon la
¥ C’est pourquoi dans la suite de ce cours on s’intéresse au IPC System V. prise échoue ; si l’on désire être le seul à l’utiliser, il faut exclure les accès concurrents par l’intermédiaire du
drapeau IPC_EXCL. IPC_NOWAIT permet d’éviter d’attendre un événement lors de l’utilisation des opérations
bloquantes. Dans le cas de IPC_PRIVATE, un nouvel objet sera créé dont l’identité ne pourra être demandée
ultérieurement au système d’exploitation (équivalent des tubes ordinaires sans nom).

& %
Parmi les opérations intéressantes à réaliser, on peut noter IPC_RMID qui permet de détruire un objet de
Concernant les POSIX IPC, le noyau Linux : type IPC et IPC_SET qui permet de mettre à jour la valeur de l’objet.

TELECOM SudParis — Denis Conan et Michel Simatic — Mai 2010 — module CSC4508/M2 83 TELECOM SudParis — Denis Conan et Michel Simatic — Mai 2010 — module CSC4508/M2 84
Communications inter-processus 3 Communication à l’aide des IPC System V Communications inter-processus 3 Communication à l’aide des IPC System V
' $
3.4 Files de messages };
char texte[256];

int main() {
¥ Création : int msgrep, msgreq, i;
struct msgforme req, rep;
int msgget(key_t clé, int d) : retour = f_id /* file des requetes */
if ((msgreq = msgget(MSGREQ, 0600|IPC_CREAT)) < 0) {
¥ Émission (bloquante si file pleine et d ne contient pas IPC_NOWAIT) : perror("Serveur : msget");
exit(1);
int msgsnd(int f_id, const void *s_msg, size_t t_texte, int d) }
¨ avec struct s_msg {long type ; char *texte ;} /* file des reponses */
if ((msgrep = msgget(MSGREP, 0600|IPC_CREAT)) < 0) {
¥ Réception (bloquante si file vide et d ne contient pas IPC_NOWAIT) : perror("Serveur : msget");
exit(1);
# 17 int msgrcv(int f_id, void *s_msg, size_t t_texte, long typ, int d) }
for (;;) {
¨ typ = 0 : premier message lu /* attente requete de n’importe quel type */
¨ typ > 0 : premier message de type typa if (msgrcv(msgreq, &req, sizeof(req.texte), 0, 0) < 0) {
perror("Serveur : msgrcv");
¨ typ < 0 : premier message de la file avec un type inférieur ou égal à la valeur exit(1);
}
absolue de typ printf("Serveur : message recu %ld : %s \n",
req.type, req.texte);
¥ Opérations de contrôle : rep.type = req.type; /* type de la reponse = PID demandeur */
int msgctl(int f_id, int cmde, struct msqid_ds *structure) if (req.texte[0] == ’@’) { /* test fin */
break;
¨ cmde = IPC_RMID, IPC_STAT, IPC_SET }
for (i = 0; req.texte[i] != 0; i++) {
rep.texte[i] = req.texte[i];

& %
a. Si d contient MSG_EXCEPT, premier message de type différent de typ }
rep.texte[i] = ’\0’;
if (msgsnd(msgrep, &rep, strlen(rep.texte) + 1, 0) < 0) {
perror("Serveur : msgsnd");
Une file de messages est composée d’un ensemble de files dans lesquelles il est possible de placer des exit(1);
messages. À chaque file est associée un type (strictement positif). Lorsque l’on envoie un message dans une }
printf("Serveur : message emis %ld : %s \n",
file, on précise son type et le message est placé à la fin de la file ; lorsque l’on désire recevoir un message, on rep.type, rep.texte);
lit le premier message disponible (le plus ancien) correspondant au type précisé. Ceci se traduit par quatre }
strcpy(rep.texte, "Fin du serveur");
opérations. if (msgsnd(msgrep, &rep, strlen(rep.texte) + sizeof(’\0’), 0) < 0) {
msgget permet d’obtenir un identifiant de file de messages à partir d’une clé et des droits d’accès et les perror("Serveur : msgsnd");
exit(1);
conditions de création associées. }
msgsnd permet d’envoyer un message dans la file identifiée par le premier paramètre et dont l’adresse sleep(10);
if (msgctl(msgreq, IPC_RMID, 0) < 0) { /* suppression */
et la taille sont respectivement les deuxième et troisième paramètres. Le dernier paramètre est un drapeau perror("Serveur : msgctl");
permettant par exemple de ne pas générer d’erreur si le message est trop long (avec MSG_NOERROR). exit(1);
msgrcv demande la réception d’un message à partir de l’identifiant de file de messages. Ce message }
if (msgctl(msgrep, IPC_RMID, 0) < 0) { /* suppression */
devra être placé à l’adresse indiquée par le deuxième paramètre et ne pas dépasser la taille indiqué par le perror("Serveur : msgctl");
troisième. Le quatrième paramètre précise le type de message que l’on désire recevoir (en posant n le type exit(1);
de message attendu, si n = 0 alors le premier message quel que soit son type est retourné, si n > 0 alors }
exit(0);
le premier message de type n est retourné, si n < 0 alors le premier message dont le type est inférieur }
ou égal à |n| est retourné). La réception des messages est bloquante. Le dernier paramètre est un drapeau
permettant, en particulier, de ne pas générer d’erreur si le message est trop long (MSG_NOERROR) ou de ne /************************/
pas attendre si aucun message du type donné n’est présent IPC_NOWAIT). Si le contenu du message est plus /* exemple_msg_client.c */
/************************/
long que t_texte octets, et si l’argument d contient MSG_NOERROR, alors le message sera tronqué (et la
partie tronquée sera perdue). Sinon le message ne sera pas extrait de la file et l’appel système échouera en /* Processus Client : appel : client texteSansEspace
indiquant E2BIG dans errno. Voici un exemple de communication à l’aide de files de messages IPC (fichiers */
Resultat : affichage du texte renvoye en echo par le serveur

exemple_msg_serveur.c et exemple_msg_client.c) : #include <stdio.h>


#include <stdlib.h>
/*************************/ #include <unistd.h>
/* exemple_msg_serveur.c */ #include <sys/types.h>
/*************************/ #include <sys/ipc.h>
#include <sys/msg.h>
/* Processus Serveur : appel : server& #include <string.h>
Resultat : echo des requetes soumises par les clients
Arret : sur reception d’un message commencant par @ #define MSGREQ 10 /* file des requetes */
*/ #define MSGREP 20 /* file des reponses */
#include <stdio.h>
#include <stdlib.h> struct msgforme {
#include <unistd.h> long type;
#include <sys/types.h> char texte[256];
#include <sys/ipc.h> };
#include <sys/msg.h>
#include <string.h> int main(int argc, char **argv) {
#define MSGREQ 10 int msgreq,msgrep;
#define MSGREP 20 struct msgforme rep, req;
if (argc != 2) {
struct msgforme { printf("Client : Usage : %s texteSansEspace\n",argv[0]);
long type; exit(1);

TELECOM SudParis — Denis Conan et Michel Simatic — Mai 2010 — module CSC4508/M2 85 TELECOM SudParis — Denis Conan et Michel Simatic — Mai 2010 — module CSC4508/M2 86
Communications inter-processus 3 Communication à l’aide des IPC System V Communications inter-processus 3 Communication à l’aide des IPC System V

} shmctl permet de réaliser des opérations (en particulier celles vues sur le transparent « Constantes
if ((msgreq = msgget(MSGREQ, 0)) == -1
|| (msgrep = msgget(MSGREP, 0)) == -1) {
relatives aux IPC ») en précisant l’identifiant du segment de mémoire partagé, le type d’opération à réaliser
perror("Client : msgget"); et les paramètres associés à l’opération (voir le manuel avec man shmctl).
exit(1); 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
}
strcpy(req.texte, argv[1]); du processus sont automatiquement détachés. Voici un exemple de communication à l’aide de segments de
req.type = getpid(); /* type des messages = pid */ mémoire partagée IPC (fichiers exemple_shm.c :
/* envoi requete */
if (msgsnd(msgreq, &req, strlen(req.texte) + + sizeof(’\0’), 0) == -1) { /*************************/
perror("Client : msgsnd"); /* exemple_shm.c */
exit(1); /*************************/
}
/* attente reponse d’un message de type = pid */ /* Processus permettant de gerer un pile en memoire partagee
if (msgrcv(msgrep, &rep, sizeof(rep.texte), getpid(), 0) == -1) { Appel:
perror("Client : msgrcv"); exemple_shm c # Cree la memoire partage
exit(1); exemple_shm s # Supprime la memoire partagee
} exemple_shm u valeur # pUsh valeur dans la pile
printf("Client : message emis %d : %s \n", exemple_shm o # pOp une valeur de la pile
getpid(), req.texte); Resultat : Message indiquant le resultat de l’operation
printf("Client : message recu %d : %s \n", */
getpid(), rep.texte); #include <stdio.h>
exit(0); #include <stdlib.h>
} #include <sys/ipc.h>
#include <sys/shm.h>

' $ /* Cle d’acces a la memoire partagee */


#define SHMKEY 30
3.5 Mémoire partagée
/* Nombre maximum d’elements dans la pile */
#define MAX_ELEMENTS 3

/* Structure contenant la pile */


¥ Création : typedef struct {
int shmget(key_t clé, size_t taille, int d) int contenuPile[MAX_ELEMENTS];
int nbElements;
¨ Création (d contient IPC_CREAT) ou recherche } Pile;
¨ Retour = identifiant (m_id)
int main(int argc, char *argv[]) {
¥ Attachement :
int shm_id;
void *shmat (int m_id, void *adr, int d) Pile *pile;
¨ adr = 0 → choix de l’adresse par le système d’exploitation int valeur;
# 18 ¨ Une même région peut être attachée plusieurs fois à des adresses différentes if (argc < 2){
¨ Retour : adresse d’attachement effective ou -1 fputs( "USAGE = exemple_shm c # Cree la memoire partage\n\
\texemple_shm s # Supprime la memoire partagee\n\
¥ Détachement :
\texemple_shm u valeur # pUsh valeur dans la pile\n\
int shmdt(void *adr) \texemple_shm o # pOp une valeur de la pile\n", stderr);
¨ Libération de l’espace mémoire uniquement lors du dernier détachement exit(EXIT_FAILURE);
}
¨ Positionnement d’un drapeau interdisant des attachements
¨ Retour : 0 ou -1 switch (argv[1][0]) {
¥ Opérations de contrôle :
case ’c’:
int shmctl(int m_id, int cmde, struct shmid_ds *structure) /***************************************************************/
¨ cmde = IPC_RMID, IPC_STAT, IPC_SET, SHM_(UN)LOCK ((dé)verrouillage) /* Creation (et initialisation) d’une zone de memoire partagee */
/***************************************************************/

& % /*creation segment partage*/


if ((shm_id = shmget(SHMKEY, sizeof(Pile), 0700|IPC_CREAT)) < 0) {
perror("shmget pour creer la pile");
Un segment de mémoire partagée est un morceau de mémoire qui peut être commun à plusieurs pro- exit(EXIT_FAILURE);
}
cessus, l’espace mémoire virtuel est ainsi partagé. Toute modification effectuée par l’un des processus peut /* attachement */
automatiquement être connue des autres processus partageant ce morceau de mémoire. Trois étapes jalonnent if ((pile = (Pile *)shmat(shm_id, NULL, 0)) < (Pile *) 0) {
perror("shmat");
l’utilisation de la mémoire partagée. exit(EXIT_FAILURE);
shmget permet d’obtenir un identifiant associé au segment de mémoire partagée dont la clé (clé) et la }
/* Initialisation de la structure de donnee */
taille (taille) sont précisées. Le dernier paramètre (d) précise les droits d’accès et les conditions de création pile->nbElements = 0;
si nécessaire du segment de mémoire partagé. Un nouveau segment mémoire, de taille arrondie au multiple /* Detachement */
if (shmdt(pile)<0) {
supérieur de PAGE_SIZE, est créé si clé possède la valeur IPC_PRIVATE ou si aucun segment de mémoire perror("shmdt");
partagée n’est associé à clé, et IPC_CREAT est présent dans d. Pour la recherche (IPC_CREAT non présent }
exit(EXIT_FAILURE);

dans d), la taille peut être inférieure à celle qui a été utilisée lors de la création. /* Affichage du resultat */
shmat permet d’attacher (c’est-à-dire d’insérer) le segment de mémoire partagé dans l’espace d’adressage printf("Pile cree et initialisee\n");
break;
virtuel du processus en précisant l’identifiant 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 case ’s’:
/**********************************************/
grand) et un drapeau permettant — entre autres — de considérer le segment de mémoire comme étant en /* Suppression de la zone de memoire partagee */
lecture seul (SHM_RDONLY). En retour, la primitive retourne l’adresse, dans l’espace d’adressage virtuel du /**********************************************/

processus, où le segment a été attaché. /* Recherche segment partage */

TELECOM SudParis — Denis Conan et Michel Simatic — Mai 2010 — module CSC4508/M2 87 TELECOM SudParis — Denis Conan et Michel Simatic — Mai 2010 — module CSC4508/M2 88
Communications inter-processus 3 Communication à l’aide des IPC System V 3 Communication à l’aide des IPC System V 3.6 Sémaphores IPC

if ((shm_id = shmget(SHMKEY, sizeof(Pile), 0)) < 0) { default:


perror("shmget pour supprimer la pile"); fprintf(stderr, "Argument ’%s’ inconnu\n", argv[1]);
exit(EXIT_FAILURE); fputs( "USAGE = exemple_shm c # Cree la memoire partage\n\
} \texemple_shm s # Supprime la memoire partagee\n\
/* Suppression */ \texemple_shm u valeur # pUsh valeur dans la pile\n\
if (shmctl(shm_id, IPC_RMID, NULL) < 0) { \texemple_shm o # pOp une valeur de la pile\n", stderr);
perror("shmctl"); exit(EXIT_FAILURE);
exit(EXIT_FAILURE);
} }
/* Affichage du resultat */
printf("Pile supprimee\n"); exit(EXIT_SUCCESS);
break; }

' $
case ’u’:
/***************************/
/* Empilement d’une valeur */
/***************************/ 3.6 Sémaphores IPC
/* Verification de la presence de la valeur a ajouter */
if (argc < 3){
fputs( "USAGE = exemple_shm c # Cree la memoire partage\n\
\texemple_shm s # Supprime la memoire partagee\n\
\texemple_shm u valeur # pUsh valeur dans la pile\n\
\texemple_shm o # pOp une valeur de la pile\n", stderr);
exit(EXIT_FAILURE);
}
/* Recherche segment partage */
if ((shm_id = shmget(SHMKEY, sizeof(Pile), 0)) < 0) {
perror("shmget pour empiler valeur");
3.6.1 Introduction aux sémaphores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
exit(EXIT_FAILURE);
# 19 3.6.3 Introduction aux sémaphores : analogie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
}
/* attachement */ 3.6.3 Introduction aux sémaphores : algorithmes P ET V . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
if ((pile = (Pile *)shmat(shm_id, NULL, 0)) < (Pile *) 0) {
perror("Serveur : shmat"); 3.6.5 Sémaphores IPC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
exit(EXIT_FAILURE);
}
3.6.5 Utilisation des sémaphores IPC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
/* La pile est-elle pleine ? */
if (pile->nbElements >= MAX_ELEMENTS) {
fputs("Trop d’elements dans la pile pour pouvoir en ajouter un autre\n", stderr);
exit(EXIT_FAILURE);
}
/* On empile l’element */
pile->contenuPile[pile->nbElements] = atoi(argv[2]);
pile->nbElements += 1;
/* Detachement */

& %
if (shmdt(pile)<0) {
perror("shmdt");
exit(EXIT_FAILURE);

' $
}
/* Affichage du resultat */
printf("Valeur ’%d’ empilee dans la pile\n", atoi(argv[2]));
break; 3.6.1 Introduction aux sémaphores
case ’o’:
/***************************/
/* Depilement d’une valeur */
/***************************/
¥ Sémaphore = objet composé :
/* Recherche segment partage */
if ((shm_id = shmget(SHMKEY, sizeof(Pile), 0)) < 0) {
¨ D’une variable (sa valeur)
perror("shmget pour depiler valeur"); ¨ D’une file d’attente (les processus bloqués)
exit(EXIT_FAILURE);
} ¥ Primitives associées :
/* attachement */
if ((pile = (Pile *)shmat(shm_id, NULL, 0)) < (Pile *) 0) { ¨ Initialisation (avec une valeur positive ou nulle)
perror("Serveur : shmat"); # 20 ¨ Manipulation :
exit(EXIT_FAILURE);
} ◮ Prise (P ou Wait) = demande d’autorisation
/* La pile est-elle vide ? */
if (pile->nbElements <= 0) { ◮ Validation (V ou Signal) = fin d’utilisation
fputs("Aucun element dans la pile ==> Impossible d’en depiler un\n", stderr);
exit(EXIT_FAILURE); ¥ Principe : sémaphore associé à une ressource
}
/* On depile l’element */ ¨ Prise = demande d’autorisation (Puis-je ?)
pile->nbElements -= 1; si valeur > 0 accord, sinon blocage
valeur = pile->contenuPile[pile->nbElements];
/* Detachement */ ¨ Validation = restitution d’autorisation (Vas-y)
if (shmdt(pile)<0) {
perror("shmdt"); si valeur < 0 déblocage d’un processus
exit(EXIT_FAILURE);

& %
}
/* Affichage du resultat */
printf("Valeur ’%d’ depilee de la pile\n", valeur);
break;
Interprétation de la valeur du sémaphore :

TELECOM SudParis — Denis Conan et Michel Simatic — Mai 2010 — module CSC4508/M2 89 TELECOM SudParis — Denis Conan et Michel Simatic — Mai 2010 — module CSC4508/M2 90
3 Communication à l’aide des IPC System V 3.6 Sémaphores IPC 3 Communication à l’aide des IPC System V 3.6 Sémaphores IPC
' $
– si valeur >= 0 : nombre d’autorisations
3.6.3 Introduction aux sémaphores : algorithmes P ET V
– si valeur <= 0 : nombre de processus bloqués

¥ Initialisation(sémaphore,n)
valeur = valeurInitiale − nb(P ) + nb(V ) valeur[sémaphore] = n
¥ P(sémaphore)
valeur[sémaphore] = valeur[sémaphore] - 1
La valeur initiale n’est jamais négative. si (valeur[sémaphore] < 0) alors
étatProcessus = Bloqué
# 22 mettre processus en file d’attente
Le sémaphore est l’outil de base pour résoudre les problèmes de synchronisation entre processus (cf. finSi
chapitre « Synchronisation entre processus »). 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
3.6.2 Introduction aux sémaphores : analogie finSi
invoquer l’ordonnanceur
& %
¥ Parking de N places contrôlé par un feu
' $
3.6.4 Sémaphores IPC

# 21
P V
1111
0000
0000
1111 1111
0000
0000
1111 ¥ Création :
int semget ( key_t clé, int nsems, int semflg ) : retour = semid
# 23
¥ Opérations :
−1 Valeur du +1 int semop ( int semid, struct sembuf *spos, int nsops )
sémaphore ¥ Opérations de contrôle :

& %
int semctl ( int semid, int semno, int cmd, union semun arg )

& %
– État initial : parking vide ==> valeur sémaphore = N
– Capteur d’entrée = rôle de P Trois primitives sont utilisées pour manipuler les sémaphores IPC.
−1 sur valeur semget permet de créer un groupe de sémaphores à partir d’une clé, le nombre de sémaphores que l’on
si valeur >= 0 feu vert, sinon feu rouge désire dans le groupe et les droits d’accès et les conditions de création associés au groupe de sémaphores. En
– Capteur de sortie = rôle de V retour, un identifiant de groupe de sémaphore est retourné.
+1 sur valeur semop permet à un ensemble d’opérations d’être réalisées atomiquement sur le groupe de sémaphores. Le
si valeur <= 0 feu vert pour une voiture premier paramètre spécifie l’identifiant du groupe de sémaphores ; ensuite, le tableau contenant les opérations
sinon rien (une place disponible de plus) à réaliser et le nombre d’opérations contenues dans le tableau sont spécifiés (Cf le transparent suivant).
– NB : si une voiture ne joue pas le jeu (elle ne passe pas devant les capteurs), elle met en péril le bon semctl permet de réaliser des opérations (en particulier celles vues sur le transparent « Constantes
fonctionnement du système : relatives aux IPC ») en précisant l’identifiant du groupe de sémaphores, le numéro du sémaphore dans le
– trop de voitures dans le parking groupe (si ceci est requis par l’opération), le type d’opération à réaliser et les paramètres associés à l’opération
– OU BIEN feu rouge alors que le parking contient des places libres (voir le manuel avec man semctl).

TELECOM SudParis — Denis Conan et Michel Simatic — Mai 2010 — module CSC4508/M2 91 TELECOM SudParis — Denis Conan et Michel Simatic — Mai 2010 — module CSC4508/M2 92
3 Communication à l’aide des IPC System V 3.6 Sémaphores IPC Communications inter-processus
' $
3.6.5 Utilisation des sémaphores IPC }
return EXIT_SUCCESS;

¥ Structure : Le programme exempleSem.c utilise ce sémaphore pour protéger l’accès au parking :


struct sembuf {
u_short sem_num ;
# 24 short sem_op ;
short sem_flg ;
}
¥ Opérations de base :
/****************/
¨ P ( s ) : { numSem, -1, 0 } /* exempleSem.c */
/****************/
¨ V ( s ) : { numSem, 1, 0 }
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>

& %
#include <sys/sem.h>
#include <unistd.h>

void P(int semId){


Les trois champs composant la structure struct sembuf représentent respectivement le numéro d’ordre struct sembuf op;
op.sem_num = 0;
du sémaphore dans le groupe de sémaphores, l’opération que l’on désire réaliser (en posant n la valeur de op.sem_op = -1;
semop, si n > 0 alors n ressources sont libérées, si n < 0 alors dès que n ressources sont disponibles on les op.sem_flg = 0;
if (semop(semId, &op, 1) == -1){
prend, si n = 0 alors l’opération bloque jusqu’à ce qu’il n’y ait plus de ressource disponibles), un drapeau perror("semop");
permettant par exemple de ne pas attendre si un nombre insuffisant de ressources est disponible. exit(2);
}
Deux fonctions sont traditionnellement réalisées lorsque l’on utilise les sémaphores. La première P(s) se }
traduit par « Puis-je ? », tandis que la seconde V(s) signifie « Vas-y ! ». Lorsque l’on désire prendre une void V(int semId){
ressource, on effectue un P(s) ; lorsque la ressource n’est plus nécessaire, on la relâche en effectuant un V(s). struct sembuf op = {0, 1, 0};
L’exemple suivant utilise un sémaphore pour réaliser un parking. Le programme exempleSemInit.c crée if (semop(semId, &op, 1) == -1){
ce sémaphore (dont la clé est basée sur un nom de fichier) et l’initialise : perror("semop");
exit(2);
}
/********************/ }
/* exempleSemInit.c */
/********************/ int main(int argc, char *argv[]){
int semParking;
#include <stdlib.h> key_t cle;
#include <stdio.h>
#include <sys/types.h> if (argc != 2) {
#include <sys/ipc.h> fputs("USAGE = exempleSem nomParking\n", stderr);
#include <sys/sem.h> exit(2);
}
int main(int argc, char *argv[]){ cle = ftok(argv[1], 1);
int semParking; if (cle == -1) {
key_t cle; perror("ftok");
exit(2);
if (argc != 3) { }
fputs("USAGE = exempleSemInit nomParking nbPlaces\n", stderr); semParking = semget(cle, 1, 0);
exit(2); if (semParking == -1) {
} perror("semget");
/* On se sert du nom de parking comme clé pour le sémaphore */ exit(2);
cle = ftok(argv[1], 1); }
if (cle == -1) { printf("Demande d’entree dans le parking \"%s\"...\n", argv[1]);
perror("ftok"); P(semParking);
exit(2); printf("...OK\n");
} sleep(5);
/* Création du sémaphore */ printf("Avertir de sortie du parking \"%s\"...\n", argv[1]);
semParking = semget(cle, 1, IPC_CREAT|0666); V(semParking);
if (semParking == -1) { printf("...OK\n");
perror("semget"); sleep(5);
exit(2); return EXIT_SUCCESS;
} }
/* Intialisation du sémaphore au nombre de places dans le parking */
if (semctl(semParking, 0, SETVAL, atoi(argv[2])) == -1) {
perror("semctl");
exit(2);
}
puts("Initialisation OK, vous pouvez executer exempleSem");

TELECOM SudParis — Denis Conan et Michel Simatic — Mai 2010 — module CSC4508/M2 93 TELECOM SudParis — Denis Conan et Michel Simatic — Mai 2010 — module CSC4508/M2 94
Communications inter-processus Communications inter-processus
' $
(a) essaye d’accéder à l’une des cases de ce tableau selon le paradigme de synchronisation Produc-
4 Comparaison des mécanismes de synchronisation
teur/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 information
est prête ;
– Chaque thread traiteur attend qu’une information soit prête dans ce tableau ;
Temps écoulé pour envoyer un message via différents media de communication :
– Quand c’est le cas, le thread traiteur :
1. Tube pour transférer 4096 octets : 3, 05 µs (a) recopie le contenu de la case prête dans une variable locale,
2. Tube pour transférer 416 octets en 2 écritures (longueur, puis donnée) : 1, 37 µs, (b) indique au thread distributeur (grâce au paradigme Producteur/Consommateur) que cette place
# 25 soit un gain de 55% est à nouveau disponible,
3. Tube pour transférer pointeur (suppose malloc et free) : 2, 85 µs, soit un gain de 7% (c) transmet l’adresse de cette variable locale à sa fonction de traitement ;
4. Tableau de zones mémoire géré par paradigme de synchronisation – Toutes les opérations de synchronisation sur ces tableaux sont réalisées à l’aide de sémaphores sem_t.
Producteur/Consommateur avec des sémaphores POSIX : 0, 76 µs, soit un gain de Le gain obtenu est dû au fait qu’on économise au maximum les transferts mémoire entre la mémoire
75% 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. Idem, mais avec des conditions POSIX : 1, 80 µs, soit un gain de 41%
5. Dans cette méthode, on utilise des conditions POSIX au lieu de sémaphores POSIX. Il apparaît que
6. File de messages (IPC et POSIX) : 0, 70 µs, soit un gain de 77% 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
Cette comparaison des mécanismes de synchronisation a été réalisée lors des TP notés de 2009 (première requête contenue dans ce message.
et deuxième session). Pour le code des expériences réalisées, se reporter au corrigé de ces TP notés. – La file de message est supprimée, une fois que le programme a fini son exécution.
Le principe de ces expériences est le suivant : un thread distributeur envoie 100.000 messages via un C’est le code le plus performant obtenu parmi les codes produits lors des TP notés de 2009. Les files
medium de communication à des threads traiteur. Les performances sont mesurées à l’aide de la commande de message IPC constituent donc un medium de communication très performant. NB : une expérience
time sur une machine HP Compaq dc 7700 munie d’un processeur double-cœur Pentium D cadencé à 3 GHz, réalisée (hors TP noté) avec les files de messages POSIX montrent que ces dernières ont un niveau de
2 Go de RAM et, du noyau Linux 2.6.30.10-105.2.23.fc11.i686.PAE. Noter que cette comparaison est réalisée performance équivalent au files de messages IPC. Les files de messages POSIX ont toutefois l’incon-
à l’aide de threads. De ce fait, les thread sont en mesure de partager l’espace mémoire. Par exemple, une zone vénient de nécessiter que les processus qui les utilisent soient root.
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 (34%) 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é 4% de temps par rapport à la première méthode de travail, on a perdu par rapport au code
de la deuxième méthode (45% d’augmentation). Les malloc/free sont ici beaucoup plus pénalisants
que l’écriture/lecture des octets du message sur le tube.
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 :

TELECOM SudParis — Denis Conan et Michel Simatic — Mai 2010 — module CSC4508/M2 95 TELECOM SudParis — Denis Conan et Michel Simatic — Mai 2010 — module CSC4508/M2 96
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 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
5 Mise en oeuvre dans un système d’exploitation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33

Synchronisation entre processus


& %

' $
1 Introduction
Dominique Bouillet et Michel Simatic

module CSC4508/M2 ¥ 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

& %

Mai 2010

97 TELECOM SudParis — Dominique Bouillet et Michel Simatic — Mai 2010 — module CSC4508/M2 98
Synchronisation entre processus 2 Sémaphore = Outil de base Synchronisation entre processus 2 Sémaphore = Outil de base
' $ ' $
1.1 Correspondance problèmes vie courante/informatique 2.1 Généralités

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 ¥ Sémaphore = objet composé :
compte. ¨ D’une variable (sa valeur)
Parking Problème de cohorte : un groupe de taille bornée est autorisé à offrir/utiliser un ¨ D’une file d’attente (les processus bloqués)
service. Cas, par exemple, d’un serveur de connexions Internet qui ne doit pas autoriser
¥ Primitives associées :
plus de N connexions en parallèle.
¨ Initialisation (avec une valeur positive ou nulle)
Roméo et Juliette Problème de passage de témoin : on divise le travail entre des processus.
#4 #6 ¨ Manipulation :
Cas, par exemple, de 2 processus qui doivent s’échanger des informations à un moment
donné de leur exécution avant de continuer.
◮ Prise (P ou Wait) = demande d’autorisation
Robert et Raymonde Problème de producteurs/consommateurs : un consommateur ne ◮ Validation (V ou Signal) = fin d’utilisation
peut consommer que si tous les producteurs ont fait leur travail. Cas, par exemple, d’un ¥ Principe : sémaphore associé à une ressource
processus chargé d’envoyer des tampons qui ont été remplis par d’autres processus. ¨ Prise = demande d’autorisation (Puis-je ?)
Train Problème de lecteurs/rédacteurs où l’on doit gérer, de manière cohérente, une si valeur > 0 accord, sinon blocage
compétition entre différentes catégories d’entités. Cas, par exemple, d’une tâche de fond ¨ Validation = restitution d’autorisation (Vas-y)
périodique de « nettoyage » qui ne peut se déclencher que quand les tâches principales si valeur < 0 déblocage d’un processus
sont inactives.

& % & %

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
2 Sémaphore = Outil de base

¥ Parking de N places contrôlé par un feu

2.1 Généralités . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 #7
#5
2.2 Analogie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 P V
3.0 Algorithmes P ET V . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 1111
0000
0000
1111 1111
0000
0000
1111

−1 Valeur du +1
sémaphore

& %
& %
– État initial : parking vide ==> valeur sémaphore = N
– Capteur d’entrée = rôle de P
−1 sur valeur
si valeur >= 0 feu vert, sinon feu rouge
– Capteur de sortie = rôle de V
Le sémaphore est l’outil de base pour résoudre les problèmes de synchronisation. +1 sur valeur

TELECOM SudParis — Dominique Bouillet et Michel Simatic — Mai 2010 — module CSC4508/M2 99 TELECOM SudParis — Dominique Bouillet et Michel Simatic — Mai 2010 — module CSC4508/M2 100
Synchronisation entre processus Synchronisation entre processus 3 Résolution de problèmes de synchronisation typiques

si valeur <= 0 feu vert pour une voiture Leur solution fournit des schémas de conception et de programmation de composants concurrents. Ils
sinon rien (une place disponible de plus) servent de briques de base à toute étude, analyse ou construction de systèmes ou d’applications coopératives.
' $
– 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 3.1 Exclusion mutuelle
– OU BIEN feu rouge alors que le parking contient des places libres
' $
2.3 Algorithmes P ET V ¥ Permet la gestion d’accès concurrent à des ressources partagées
¥ Principe :
¥ Initialisation(sémaphore,n) ¨ Sémaphore mutex initialisé à 1
valeur[sémaphore] = n ¨ Primitive P en début de section critique
¥ P(sémaphore) ¨ Primitive V en fin de section critique
valeur[sémaphore] = valeur[sémaphore] - 1 # 10 ¥ Exemple :
si (valeur[sémaphore] < 0) alors ¨ Sémaphore mutex initialisé à 1
étatProcessus = Bloqué ¨ Prog1 Prog2
#8 mettre processus en file d’attente P(mutex) P(mutex)
finSi x=lire (cpte) x=lire (cpte)
invoquer l’ordonnanceur
x = x + 10 x = x - 100
¥ V(sémaphore)
écrire (cpte=x) écrire (cpte=x)
valeur[sémaphore] = valeur[sémaphore] + 1
V(mutex) V(mutex)
si (valeur[sémaphore] <= 0) alors

& %
extraire processus de file d’attente
étatProcessus = Prêt
finSi
Quand Prog1 s’exécute sans que Prog2 soit activé, alors P(mutex) fait passer mutex à 0 et Prog1 entre
invoquer l’ordonnanceur
en section critique.
& % 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 Résolution de problèmes de synchronisation typiques 3.2 Cohorte

¥ Permet la coopération d’un groupe de taille maximum donnée


¥ Principe :
¨ Sémaphore parking initialisé à N
3.3 Exclusion mutuelle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 ¨ Primitive P en début de besoin
3.3 Cohorte. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .12 ¨ Primitive V en fin de besoin
#9
3.3 Passage de témoin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 # 11 ¥ Exemple :
3.4 Producteurs/Consommateurs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 ¨ Sémaphore parking initialisé à N
3.5 Lecteurs/rédacteurs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25 ¨ Prog vehicule
...
P(parking)
|...
V(parking)
...

& %
& %
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 Le (N + 1) véhicule qui cherche à entrer dans le parking se retrouve bloqué par son P(parking). Il n’est
e

des classes de problèmes qui sont fréquemment rencontrés et qui sont présents à tous les niveaux dans les débloqué que quand un autre véhicule sort en faisant V(parking).
systèmes et applications concurrentes. Noter que valeurSem[parking] = valeurInitialeSem[parking] − nb(P ) + nb(V )

TELECOM SudParis — Dominique Bouillet et Michel Simatic — Mai 2010 — module CSC4508/M2 101 TELECOM SudParis — Dominique Bouillet et Michel Simatic — Mai 2010 — module CSC4508/M2 102
3 Résolution de problèmes de synchronisation typiques 3.3 Passage de témoin 3 Résolution de problèmes de synchronisation typiques 3.3 Passage de témoin
' $ ' $
3.3 Passage de témoin 3.3.2 Rendez-vous entre deux processus

¥ Permet à deux processus d’établir un point de synchronisation


¥ Principe :
¥ Permet la coopération par division du travail entre processus ¨ Sémaphore romeo initialisé à 0
# 12 ¥ 3 types : # 14 ¨ Sémaphore juliette initialisé à 0
¨ Envoi de signal ¨ Prog1 Prog2
¨ Rendez-vous entre 2 processus ... ...
¨ Appel procédural entre processus V(juliette) V(romeo)
P(romeo) P(juliette)
... ...

& % & %

' $
3.3.1 Envoi de signal
Ce patron permet à Prog1 et Prog2 de s’échanger des informations à un moment précis de leur exécution
avant de continuer.
¥ Permet à un processus d’envoyer un « top » à un autre pour signaler la
présence/disponibilité d’une information
¥ Principe : Généralisation : principe d’un rendez-vous entre N processus (NB : cet algorithme ne fonctionne qu’une
¨ Sémaphore top initialisé à 0 seule fois)
¨ Primitive P pour attente « top »
# 13 ¨ Primitive V pour signal « top »
¥ Exemple : – Sémaphore rdv initialisé à 0
¨ Sémaphore top initialisé à 0 – Sémaphore mutex initialisé à 1
¨ Prog1 Prog2 – Entier nbAttente initialisé à 0
...
... ...
P(mutex)
calcul(info) P(top) si nbAttente < N - 1 alors
V(top) utilisation(info) 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
Ce mécanisme peut être utilisé par Prog1 pour donner à Prog2 le droit d’accès à une ressource quand finsi
Prog1 estime que cette ressource est prête. ...

TELECOM SudParis — Dominique Bouillet et Michel Simatic — Mai 2010 — module CSC4508/M2 103 TELECOM SudParis — Dominique Bouillet et Michel Simatic — Mai 2010 — module CSC4508/M2 104
3 Résolution de problèmes de synchronisation typiques 3.4 Producteurs/Consommateurs 3 Résolution de problèmes de synchronisation typiques 3.4 Producteurs/Consommateurs
' $ ' $
3.3.3 Appel procédural entre processus 3.4.1 Objectif

¥ Permet à un processus de faire un « appel de procédure », alors que le code de cette


procédure est localisé dans un autre processus ¥ Permettre le contrôle de flux entre un (ou des) producteur(s) et un (ou des)
¥ Principe : consommateur(s) dans le cas où ils communiquent via un tampon mémoire de
¨ Sémaphore appel initialisé à 0 N cases
¨ Sémaphore retour initialisé à 0 ¥ 1. Exécution Produc : il produit info0
# 15 ¨ Serveur Client # 17 info0
répéter ... 2. Exécution Produc : il produit info1
P(appel) preparationParamAppel() info0 info1
analyseParamAppel() V(appel) 3. Exécution Conso : il consomme info0
... P(retour) info1
préparationParamRetour() analyseParamRetour() 4. Exécution Produc : il produit info2
V(retour) ... info1 info2
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 Ce patron est une généralisation des fonctionnalités offertes par les tubes (transit d’information entre K
partagée. Avec V(appel), il prévient Serveur de la disponibilité de ces informations. processus producteurs et P processus consommateurs).
Serveur est alors réactivé. Il analyse les paramètres d’appel, effectue son traitement et stocke les
paramè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.
3.4.2 Principe
Client est réactivé, analyse les paramètres retour et poursuit son traitement.

' $
3.4 Producteurs/Consommateurs
¥ Sémaphore infoPrete initialisé à 0
¥ Sémaphore placeDispo initialisé à N
¥ Produc Conso
répéter répéter
# 18 ... P(infoPrete)
3.4.1 Objectif . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 calcul(info) extraire(info)
3.4.3 Principe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19 P(placeDispo) V(placeDispo)
3.4.3 Déposer et extraire . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19 déposer(info) utiliser(info)
# 16
3.4.5 Exemple . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 V(infoPrete) finRépéter
3.4.6 K producteurs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 ...
3.4.6 Exemple de problème avec 2 producteurs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 finRépéter
3.5.0 Solution complète . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25

& %

& % 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.

TELECOM SudParis — Dominique Bouillet et Michel Simatic — Mai 2010 — module CSC4508/M2 105 TELECOM SudParis — Dominique Bouillet et Michel Simatic — Mai 2010 — module CSC4508/M2 106
3 Résolution de problèmes de synchronisation typiques 3.4 Producteurs/Consommateurs 3 Résolution de problèmes de synchronisation typiques 3.4 Producteurs/Consommateurs
' $
3.4.3 Déposer et extraire

' $
¥ Le tampon ne peut s’étendre à l’infini → Tampon géré de façon circulaire, 3.4.5 K producteurs
c’est-à-dire que quand dernière case utilisée, retour à la première
¥ Il faut :
¨ un indice de dépot iDépot
# 19 ¨ un indice d’extraction iExtrait
¨ ajouter à l’initialisation iDépot = 0 et iExtrait = 0
¥ déposer(info)
tampon[iDépot] = info ¥ Ce modèle semble rester valable pour plusieurs producteurs (ou plusieurs
iDépot = (iDépot + 1) modulo N # 21 consommateurs) en termes de synchronisation. . .
¥ extraire(info) ¥ . . .mais en fait risque d’information perdue à cause des indices iDépot (pour
info = tampon[iExtrait] K producteurs) ou iExtrait (pour P consommateurs) qui sont des variables globales
iExtrait = (iExtrait + 1) modulo N (cf. exemple)

& %

' $

& %
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
¥ Tampon : info8 info9 ' $
¥ valeurSem[placeDispo] = 3 et valeurSem[inf oP rete] = 2 3.4.6 Exemple de problème avec 2 producteurs
# 20
¥ 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 ¥ soit un tampon avec N = 5 et deux dépots effectués sans extraction
¥ valeurSem[placeDispo] = 3 et valeurSem[inf oP rete] = 2 # 22 ¥ L’état courant est alors :
¨ Tampon : info0 info1
& %
¥ iDépot = 1 et iExtrait = 4
¨ valeurSem[placeDispo] = 3 et valeurSem[inf oP rete] = 2
NB : ¨ iDépot = 2 et iExtrait = 0
– 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
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 !

TELECOM SudParis — Dominique Bouillet et Michel Simatic — Mai 2010 — module CSC4508/M2 107 TELECOM SudParis — Dominique Bouillet et Michel Simatic — Mai 2010 — module CSC4508/M2 108
Synchronisation entre processus 3 Résolution de problèmes de synchronisation typiques 3 Résolution de problèmes de synchronisation typiques 3.5 Lecteurs/rédacteurs
' $ ' $
Exemple de problème avec 2 producteurs (suite) 3.5 Lecteurs/rédacteurs
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
2. Exécution complète de Produc2 : P(placeDispo) ;tampon[iDépot] = 3.5.2 Objectif . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
# 23 infoP2 ;iDépot=(iDépot+1) modulo N ;V(infoPrete) # 25 3.5.2 Solution de base . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
¥ Tampon : info0 info1 infoP2 3.5.4 Analyse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
¥ valeurSem[placeDispo] = 1 et valeurSem[inf oP rete] = 3 3.5.4 Solution avec priorités égales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
¥ 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
& % & %

Conclusion de l’exemple :
– Valeur des sémaphores et indices correcte : ' $
– 1 case libre : la dernière
– 4 infos disponibles à partir de 0 3.5.1 Objectif
– 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.

' $
3.4.7 Solution complète
¥ Permettre une compétition cohérente entre deux types de processus (les « lecteurs »
# 26 et les « rédacteurs ») :
¨ Plusieurs lecteurs peuvent accéder simultanément à la ressource
¥ Il faut une exclusion mutuelle sur déposer() et extraire() autour de iDépot et ¨ Les rédacteurs sont exclusifs entre eux pour leur exploitation de la ressource
iExtrait ¨ Un rédacteur est exclusif avec les lecteurs
¥ Pour K producteurs :
¨ Sémaphore mutex initialisé à 1
# 24 ¨ déposer(info)
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 Ce patron permet, par exemple, d’interdire à un processus d’écrire dans un fichier, si des processus sont
mutexC 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).

TELECOM SudParis — Dominique Bouillet et Michel Simatic — Mai 2010 — module CSC4508/M2 109 TELECOM SudParis — Dominique Bouillet et Michel Simatic — Mai 2010 — module CSC4508/M2 110
3 Résolution de problèmes de synchronisation typiques 3.5 Lecteurs/rédacteurs Synchronisation entre processus 4 Interblocage
' $ ' $
3.5.2 Solution de base 3.5.4 Solution avec priorités égales

¥ Sémaphore mutexG initialisé à 1 ¥ Sémaphore mutexG initialisé à 1


¥ Sémaphore mutexL initialisé à 1
¥ Sémaphore mutexL initialisé à 1
¥ Sémaphore fifo initialisé à 1
¥ Entier NL initialisé à 0 ¥ Entier NL initialisé à 0
¥ Lecteur Rédacteur ¥ Lecteur Rédacteur
P(mutexL) P(mutexG) P(fifo) P(fifo)
|NL = NL + 1 | ecrituresEtLectures() P(mutexL) P(mutexG)
|si NL == 1 alors V(mutexG) NL = NL + 1 V(fifo)
# 27 | P(mutexG) # 29 si NL == 1 alors ecrituresEtLectures()
P(mutexG) V(mutexG)
|finSi
finSi
V(mutexL) V(mutexL)
lectures() V(fifo)
P(mutexL) lectures()
|NL = NL - 1 P(mutexL)
NL = NL - 1
|si NL == 0 alors
si NL == 0 alors
| V(mutexG)
V(mutexG)
|finSi finSi

& % & %
V(mutexL) V(mutexL)

– Le « G » de mutexG signifie « Général ».


– Le « L » de mutexL signifie « Lecteur ».

' $ fifo signifie « First In First Out ».


3.5.3 Analyse 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
¥ La solution de base fonctionne, mais on constate qu’il y a une possibilité de famine
pour les rédacteurs.
# 28 ¥ Pour éviter cette famine, on ajoute les contraintes suivantes :
¨ Si la ressource est utilisée par un lecteur :
◮ Tout écrivain est mis en attente.
◮ Tout lecteur est accepté s’il n’y a pas d’écrivain en attente.
◮ Tout lecteur est mis en attente s’il y a un écrivain en attente.
# 30 4.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
5.0 Généralités . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33

& %

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.

TELECOM SudParis — Dominique Bouillet et Michel Simatic — Mai 2010 — module CSC4508/M2 111 TELECOM SudParis — Dominique Bouillet et Michel Simatic — Mai 2010 — module CSC4508/M2 112
Synchronisation entre processus Synchronisation entre processus
' $ ' $
4.1 Introduction 5 Mise en oeuvre dans un système d’exploitation
¥ 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émaphores
¨ Sémaphore mutex2 initialisé à 1 ¨ Sémaphores IPC : cf. chapitre « Communications inter-processus »
¨ Prog1 Prog2 ¨ Sémaphores Posix : cf. chapitre « Threads »
... ... ¥ Moniteurs (ou conditions ou variables-condition)
# 31 # 33
P(mutex1) P(mutex2) ¨ Le moniteur ajoute le type condition et une file d’attente associée à chaque
|accès à ressource 1 |accès à ressource 2 variable de type condition ainsi que des primitives wait x, signal x (x étant
|P(mutex2) |P(mutex1) une condition).
||accès à ressource 2 ||accès à ressource 1 ¨ Si besoin, un processus se bloque par wait et libère le moniteur
|V(mutex2) |V(mutex1) ¨ Un processus réveille un des processus en attente par signal
V(mutex1) V(mutex2) ¨ Il peut également réveiller tous les processus en attente par broadcast
... ...
¨ Les deux programmes se bloqueront mutuellement si Prog1 fait P(mutex1) alors

& % & %
que simultanément Prog2 fait P(mutex2)

– 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
– Des exemples de code en environnement Windows sont présentés
' $ dans l’article « OMG, Multi-Threading is easier than Networking »
4.2 Généralités (http://www.gamasutra.com/view/feature/4006/sponsored_feature_omg_.php)

¥ 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)
# 32 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’UV ASR4 « Algorithmes répartis ».

TELECOM SudParis — Dominique Bouillet et Michel Simatic — Mai 2010 — module CSC4508/M2 113 TELECOM SudParis — Dominique Bouillet et Michel Simatic — Mai 2010 — module CSC4508/M2 114
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 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
5 Implantation des threads . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
6 Autres fonctions de la bibliothèque POSIX threads . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37

Threads ou processus légers


& %

' $
1 Présentation
Éric Renault et Frédérique Silber-Chaussumier

module CSC4508/M2

#3 1.1 Bibliographie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.2 Threads . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.3 Détacher le flot d’exécution des ressources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7

& %

Mai 2010

115 TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Mai 2010 — module CSC4508/M2 116
Threads ou processus légers 1 Présentation 1 Présentation 1.3 Détacher le flot d’exécution des ressources
' $
CMP : Chip MultiProcessor
1.1 Bibliographie
' $
¥ « Computer Systems : A Programmer’s Perspective », R. E. Bryant et D. R.
1.3 Détacher le flot d’exécution des ressources
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. 1.3.1 Vision traditionnelle d’un processus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
#6
Nichols, D. Buttlar et J. P. Farrell, O’Reilly and Associates, 1996. 1.3.3 Autre vision d’un processus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
¨ « Threads Primer : A Guide to Multithreaded Programming », B. Lewis, D. Berg 1.3.3 Processus multi-thread . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
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 1.3.1 Vision traditionnelle d’un processus
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 de C. Blaess fournit beaucoup d’explications sur l’implantation des threads sous Linux même s’il ¥ Processus mono-thread
est déjà un peu ancien par exemple ce qui concerne la directive _REENTRANT est déjà obsolète. ¨ Contexte : contexte d’exécution + contexte du noyau
' $ ¨ Espace d’adressage : code, données et pile
1.2 Threads
Code, données et pile
#7 Contexte du processus
3 Go
Pile

Contexte d’exécution: SP
Registres généraux
¥ Traduction : fil d’exécution, processus légers Registre d’état

¥ Principe : détacher le flot d’exécution des ressources Pointeur de pile (SP)


Compteur ordinal (PC)
brk
Tas
¥ Introduits dans divers langages et systèmes : Contexte du noyau:
Données
¨ Programmation concurrente Structures MV
#5 Tables de descripteurs PC Code
¨ Utilisation des ressources simultanément : recouvrement du calcul et des Pointeur brk
0
entrées/sorties

& %
¨ Exploitation des architectures multiprocesseurs
◮ Architectures SMP ou CMP
◮ Plus généralement des architectures à mémoire partagée Le processus dispose d’un espace d’adressage s’étendant jusqu’à 3 Go. Sur le schéma de cette page, les
¨ Exploitation de la technologie hyper-threading 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
SMP : Symmetric MultiProcessor la mémoire, les tables de descripteurs pour accéder aux fichiers, aux ports de communication... et le pointeur

TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Mai 2010 — module CSC4508/M2 117 TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Mai 2010 — module CSC4508/M2 118
1 Présentation 1.3 Détacher le flot d’exécution des ressources Threads ou processus légers 2 Création/destruction de threads

vers la fin du segment de données, indiqué pointeur brk (voir le chapitre sur la mémoire). La pile d’un processus habituel n’est contrainte que par la limite de la zone nommée tas, dans
Les schémas sont tirés du cours de O’Hallaron et al. à l’université Carnegie Mellon. laquelle 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
1.3.2 Autre vision d’un processus dès la création des threads, ce qui impose des limites de taille puisqu’elles ne doivent pas se re-
joindre. Dans la bibliothèque POSIX, l’adresse et la taille maximum de la pile sont données par :
¥ Détacher le flot d’exécution des ressources _POSIX_THREAD_ATTR_STACKADDR et _POSIX_THREAD_ATTR_STACKSIZE. La taille min-
¥ Processus mono-thread imum de la pile est définie par PTHREAD_STACK_MIN correspondant à 16 Ko sous Linux. (Voir le
¨ Fil d’exécution ou thread : pile + contexte d’exécution “Blaess” p.286-287)
¨ Code, données et le contexte du noyau ' $
Thread Code et données 2 Création/destruction de threads

#8 Pile
SP
brk
Tas
Contexte du thread:
Données
Registres généraux
PC
Registre d’état Code
Pointeur de pile (SP)
Compteur ordinal (PC)

Contexte du noyau:
# 10 2.1 Pthread “Hello world” . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
Structures MV
Tables de descripteurs
2.2 Ensemble de threads pairs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
Pointeur brk 2.3 Threads POSIX : création/destruction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14

& %

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 2.1 Pthread “Hello world”
¨ Plusieurs fils d’exécution
¨ Code, données et contexte du noyau partagés : notamment partage des fichiers
et des ports de communication ¥ Interface POSIX Pthread
Thread 1 Code et données partagés Thread 2
¥ Fournir une fonction point d’entrée
thread principal

#9 Pile 1 Pile 2
appel pthread_create()
fin de pthread_create()
thread pair
Tas appel pthread_join()
Contexte du thread 1: Contexte du thread 2:
# 11 exécution de thread_function
Données printf()
Registres généraux Registres généraux
Registre d’état Code Registre d’état
pthread_exit()
SP 1 SP 2
PC 1 PC 2

Contexte du noyau:
Structures MV fin de pthread_join()
Tables de descripteurs
exit()
Pointeur brk

& %

& %
Dans le cas d’un processus multi-thread, les différents fils d’exécution caractérisés par leur pile, l’état des ¥ Visualisation des différents threads avec la commande ps
registres et le compteur ordinal se partagent le code, les données, le tas et le contexte du noyau notamment
les tables de descripteurs. Les fichiers et les ports de communication par exemple sont partagés.

TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Mai 2010 — module CSC4508/M2 119 TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Mai 2010 — module CSC4508/M2 120
Threads ou processus légers 2 Création/destruction de threads 2 Création/destruction de threads 2.3 Threads POSIX : création/destruction
' $
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) : 2.2 Ensemble de threads pairs
gcc helloworld.c -pthread -o helloworld

¥ Création d’un ensemble de threads pairs


#include <pthread.h>
#include <time.h> ¥ Pas d’arborescence parent/enfant comme pour les processus
#include <error.h>
#include <stdlib.h>
#include <stdio.h> T2
T1 T3
void *thread_function( void *arg )
{ # 12
struct timespec sleeptime;
Code et données partagées
printf("Hello world\n");
Contexte du noyau
/* ralentissement */
sleeptime.tv_sec = 10;
sleeptime.tv_nsec = 0;
T6 T4
nanosleep(&sleeptime, NULL);
T5

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");
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
}
return EXIT_SUCCESS;
thread, principal ou non, peut créer un autre thread qui rejoint alors l’ensemble de threads.

' $
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 2.3 Threads POSIX : création/destruction
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 à 2.3.1 Identification . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
# 13
pthread_join(), que tous les autres threads sont déjà terminés. 2.3.2 Utilisation. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .15
La commande ps -L (ou ps -m) selon les versions affiche les différents threads. Vérifiez qu’un seul iden- 2.3.3 Attributs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
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 — Mai 2010 — module CSC4508/M2 121 TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Mai 2010 — module CSC4508/M2 122
2 Création/destruction de threads 2.3 Threads POSIX : création/destruction 2 Création/destruction de threads 2.3 Threads POSIX : création/destruction
' $
de l’erreur sinon.
2.3.1 Identification
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
¥ Un équivalent du pid_t : pthread_t
le fil d’exécution associé à l’identifiant du premier paramètre se termine. La valeur de fin d’exécution du
# 14 ¥ Identifiant du thread courant : thread est alors copiée à l’emplacement pointé par le second paramètre.
¨ pthread_t pthread_self (void)
pthread_detach permet de « détacher » un fil d’exécution, c’est-à-dire qu’il n’est pas possible pour
¥ Comparaison de deux identifiants : un autre thread de se synchroniser avec lui avec la fonction pthread_join et que ses ressources seront
¨ int pthread_equal (pthread_t thread1, pthread_t thread2) 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

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 ¥ Création et destruction d’une structure d’attributs
identifiants de thread est effectué par l’intermédiaire de la fonction pthread_equal. ¨ int pthread_attr_init (pthread_attr_t *attr)
' $ ¨ int pthread_attr_destroy (pthread_attr_t *attr)
2.3.2 Utilisation # 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)
¥ Création ¨ → detachstate = PTHREAD_CREATE_[JOINABLE|DETACHED]
¨ 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 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
d’erreur non nul est renvoyé en cas de problème mais errno n’est pas
sont suffisants, on passe généralement un pointeur NULL. Un grand nombre de fonctions sont fournies afin
nécessairement positionné. 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
pthread_create permet de créer un nouveau fil d’exécution pour le processus courant. Pour cela, le
selon que l’on effectue une mise à jour ou une lecture.
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 L’attribut detachstate permet de spécifier s’il sera possible de se synchroniser sur le fil d’exécution une
deux derniers paramètres représentent respectivement l’adresse de la fonction devant être exécutée par le fois qu’il sera terminé. Deux valeurs sont possibles : PTHREAD_CREATE_JOINABLE pour autoriser la synchro-
nouveau thread et le paramètre fourni à cette fonction. La valeur retournée est 0 en cas de succès et le code nisation et PTHREAD_CREATE_DETACHED pour la décliner.

TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Mai 2010 — module CSC4508/M2 123 TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Mai 2010 — module CSC4508/M2 124
Threads ou processus légers Threads ou processus légers 3 Partage des données
' $ ' $
Attributs (2/2) 3 Partage des données
¥ 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
¨ Priorité d’ordonnancement
# 17
int pthread_attr_getschedparam(const pthread_attr_t *attr, const struct
# 19 3.2 Notion de variable partagée . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
sched_param *param) 3.2 Partage non-intentionnel des données . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
int pthread_attr_setschedparam(const pthread_attr_t *attr, struct 3.3 Code réentrant et thread-safe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
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)
3.1 Notion de variable partagée
int pthread_attr_setscope(const pthread_attr_t *attr, int scope)
→ scope = PTHREAD_SCOPE_[SYSTEM|PROCESS]
¥ Variable partagée : deux définitions possibles
¥ Adresse de la pile du thread
¨ Conceptuellement : variable utilisée par plusieurs threads
int pthread_attr_getstackaddr(const pthread_attr_t *attr, void *addr)
¨ Techniquement : pendant l’exécution une seule instance de la variable pour tous
int pthread_attr_setstackaddr(const pthread_attr_t *attr, void *addr)
les threads
¥ Taille de la pile du thread
◮ Variable globale : une seule instance “partagée”
int pthread_attr_getstacksize(const pthread_attr_t *attr, int size)
◮ Variable statique locale : une seule instance “partagée”
# 18 int pthread_attr_setstacksize(const pthread_attr_t *attr, int *size) # 20 ◮ Variable automatique locale : une instance dans chacune des piles des
threads 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,
Les attributs schedpolicy, schedparam, scope et inheritsched concernent l’ordonancement des fils on appelle variable partagée une variable pour laquelle durant l’exécution du programme, il n’existe qu’une
d’exécution. Ils ne sont disponibles que si la constante _POSIX_THREAD_PRIORITY_SCHEDULING est définie seule instance. En C, les variables globales et les variables locales statiques ne sont allouées qu’une seule
dans unistd.h. 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
En particulier, l’attribut schedpolicy correspond à la politique d’ordonnancement employée par le thread.
chaque thread appelant cette fonction. Donc il existe plusieurs instances de ces variables pendant l’exécution
Trois valeurs sont possibles : SCHED_OTHER pour l’ordonnancement classique ; SCHED_RR pour un séquence-
du programme.
ment temps-réel avec l’algorithme Round Robin ; SCHED_FIFO pour un ordonnancement temps-réel FIFO.
Attention : les threads partagent l’intégralité de l’espace d’adressage du processus ce qui signifie que
Pour plus de détails, cf. chapitre « Interaction système multi-tâche et processus ».
TOUTES les variables peuvent être partagées. MAIS généralement, on utilise des variables globales, visibles
Les attributs stackaddr et stacksize permettent de configurer la pile utilisée par le fil d’exécution. Ils dans l’ensemble des fonctions utiles, pour définir des variables partagées.
permettent respectivement de préciser l’adresse de départ et la taille maximale de la pile. Voir Briant et O’Hallaron.

TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Mai 2010 — module CSC4508/M2 125 TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Mai 2010 — module CSC4508/M2 126
Threads ou processus légers 3 Partage des données 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
3.2 Partage non-intentionnel des données
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
¥ Lorsqu’une même instance de variable est utilisée par plusieurs threads alors que simultanément. En effet, sur une machine multiprocesseur, il est possible que pendant l’exécution du code
# 21 conceptuellement ce n’est pas une variable partagée par un processeur, un autre processeur commence l’exécution de ce même code. Il faut donc vérifier que
¨ Peut provoquer des situations de compétition (race condition) dans ce cas, les exécutions se déroulent correctement. Sur une machine monoprocesseur, le problème se
◮ Le résultat varie selon les conditions d’exécution 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
¨ Erreurs difficiles à détecter !
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


* race.c: code faux (poly Briant et Hallaron)
*
plusieurs threads
* sans temporisation, semble s’executer correctement! ¨ Ne pas maintenir d’état persistant entre les appels
* en ajoutant une temporisation, on voit l’erreur.
*/ ◮ Contre-exemple : strtok, rand
¨ Ne pas retourner de pointeur sur une variable statique
#include
#include
<pthread.h>
<error.h>
# 22
#include <stdlib.h>
◮ Contre-exemple : ctime
#include <stdio.h> ¥ Code thread-safe : résultats corrects lors d’exécutions simultanées par plusieurs
#define NTHREADS 100 threads
void *func(void *arg) ¨ Protéger les accès aux données partagées dans les fonctions
{
int me; ◮ Contre-exemple : variable globale externe errno
me = *((int *)arg);
◮ Redéfinition de errno : chaque thread a son propre errno
printf("Hello from %d\n", me); ¨ Pas d’appel à du code non thread-safe
pthread_exit(NULL);

& %
}

int main (int argc, char **argv)


{
int i; On trouve dans la littérature deux termes différents : réentrant et thread-safe. Pour certains, ils sont
pthread_t threads[NTHREADS];
équivalents. Pour d’autres, bien que ces notions soient liées à la façon de gérer les ressources , ce sont deux
for (i = 0 ; i < NTHREADS; i++) notions différentes. Une fonction thread-safe produit un résultat correct lors d’une exécution multithread.
{
int rc; Une fonction réentrante n’utilise pas de variables partagées lors d’une exécution multithread (voir le Briant
/* OOPS !!!! FAUX: i est partagé par tous les threads! */ et O’Hallaron). Une fonction peut être soit réentrante, soit thread-safe, soit les deux, soit ni l’un ni l’autre.
rc = pthread_create(&threads[i], NULL, func, &i);
if(rc) Une fonction réentrante ne conserve pas d’état entre deux appels successifs ; elle ne retourne pas non
error(EXIT_FAILURE, rc, "pthread_create");
}
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.
for (i = 0 ; i < NTHREADS; i++)
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
int rc;
fonctions, néanmoins, échappent à cette spécification parmi lesquelles dirname, getenv, gethostbyname,
rc = pthread_join(threads[i], NULL); gethostbyadddr, rand, readdir, setenv, putenv, strerror, strtok... (Voir le Robbins et Robbins p.432)
if(rc)
error(EXIT_FAILURE, rc, "pthread_join");
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
exit(EXIT_SUCCESS);
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 — Mai 2010 — module CSC4508/M2 127 TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Mai 2010 — module CSC4508/M2 128
Threads ou processus légers 3 Partage des données 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 if (rc)
de ces primitives, par exemple strtok_r. error(EXIT_FAILURE, rc, "pthread_join");

Une fonction thread-safe protège les données partagées (variables mais aussi écriture dans des fichiers par element = strtok(NULL, delim);
printf("0) element 1 = %s: OUOUOUPS!\n", element);
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. return EXIT_SUCCESS;
}
Une fonction thread-safe non-réentrante est souvent moins performante qu’une fonction réentrante
puisqu’il a été nécessaire d’ajouter des synchronisations pour la rendre thread-safe. Dans les programmes fournis, deux threads, le thread principal et le thread pair, analysent chacun une
Dans une application multi-thread, toutes les fonctions appelées par plusieurs threads, doivent être thread- chaîne de caractères différente pour la décomposer en élément.
safe. Il faut remarquer aussi que la plupart du temps, les fonctions non-réentrantes ne sont pas thread-safe Le programme str.c utilise la fonction strtok(). Cette fonction découpe une chaîne de caractères
mais que les rendre réentrantes les rend aussi thread-safe. en plusieurs éléments, retournés successivement par différents appels à strtok(). Entre chaque appel à
Les bibliothèques réentrantes et thread-safe sont utiles dans tous les environnements de programmation strtok(), un pointeur vers l’élément suivant de la chaîne est maintenu par l’intermédiaire d’une variable
parallèles et asynchrones et pas seulement pour des threads. statique à l’intérieur de la fonction strtok().
errno est par défaut une variable globale externe. Cette implantation ne peut pas fonctionner avec des Le résultat du programme dépend donc de l’ordre des appels à strtok(). Dans notre cas, le premier
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 appel est effectué par le thread principal. Ensuite le thread pair est créé. Si le thread pair effectue alors un
fonction _errno_location() (voir bits/errno.h) pour fournir une adresse propre à chaque thread. Par appel à strtok(), à l’appel suivant, le thread principal renverra un résultat correspondant à la chaîne du
défaut, pour un processus monothread, _errno_location() renvoie l’adresse de la variable globale errno. 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
* Exemple de code non-réentrant
* strtok() maintient un état persistant entre les appels 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).
#include <stdio.h>
#include <stdlib.h> /*
#include <string.h> * Attention le buffer en entree, phrase, est modifie
#include <error.h> * par l’appel a strotk donc il ne sert a rien d’appeler
#include <pthread.h> * plusieurs fois strtok sur le meme buffer!
#define MAXSIZE 256 *
* appel a strtok_r
*
void *start_function(void *arg) */
{ #include <stdio.h>
char *phrase; #include <stdlib.h>
char *element; #include <string.h>
char *delim = " "; #include <error.h>
#include <pthread.h>
phrase = malloc(MAXSIZE * sizeof(char)); #define MAXSIZE 256
if (phrase == NULL)
{
perror("malloc"); void *start_function(void *arg)
exit(EXIT_FAILURE); {
} char *phrase;
char *element;
strcpy(phrase, "11 12 13"); char *delim = " ";
char *ptr;
element = strtok(phrase, delim);
printf("1) element 0 = %s\n", element); phrase = malloc(MAXSIZE * sizeof(char));
if (phrase == NULL)
element = strtok(NULL, delim); {
printf("1) element 1 = %s\n", element); perror("malloc phrase");
exit(EXIT_FAILURE);
pthread_exit(NULL); }
}
strcpy(phrase, "11 12 13");
int main(int argc, char **argv)
{ element = strtok_r(phrase, delim, &ptr);
int rc; printf("1) element 0 = %s\n", element);
pthread_t thread;
char *element; element = strtok_r(NULL, delim, &ptr);
char *delim = " "; printf("1) element 1 = %s\n", element);
char *phrase;
pthread_exit(NULL);
phrase = malloc(MAXSIZE * sizeof(char)); }
if (phrase == NULL)
{ int main(int argc, char **argv)
perror("malloc"); {
exit(EXIT_FAILURE); int rc;
} pthread_t thread;
char *element;
strcpy(phrase, "01 02 03"); char *delim = " ";
char *phrase;
element = strtok(phrase, delim); char *ptr;
printf("0) element 0 = %s\n", element); phrase = malloc(MAXSIZE * sizeof(char));
rc = pthread_create(&thread, NULL, start_function, NULL); if (phrase == NULL)
if (rc) {
error(EXIT_FAILURE, rc, "pthread_create"); perror("malloc");
exit(EXIT_FAILURE);
pthread_join(thread, NULL); }

TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Mai 2010 — module CSC4508/M2 129 TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Mai 2010 — module CSC4508/M2 130
Threads ou processus légers 4 Synchronisation Threads ou processus légers 4 Synchronisation
' $
strcpy(phrase, "01 02 03"); 4.1 Synchronisation
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) ¥ Garantir la consistance des données
error(EXIT_FAILURE, rc, "pthread_join"); ¨ Accès simultanés à une donnée partagée en lecture/écriture
printf("0) element 1 = %s: OK!\n", element); ◮ Séquencement des instructions load, update, store
# 24
return EXIT_SUCCESS; ¨ 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
Interactivement en cours : Exemple avec un code utilisant strtok(). 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).
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

4.1 Synchronisation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
# 23 4.2 Exclusions mutuelles. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .25
4.3 Sémaphores POSIX . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
¥ Type : pthread_mutex_t
4.4 Attente de conditions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
¥ Création
# 25 ¨ 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)

& %

& %

TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Mai 2010 — module CSC4508/M2 131 TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Mai 2010 — module CSC4508/M2 132
Threads ou processus légers 4 Synchronisation Threads ou processus légers 4 Synchronisation

Les variables globales et les descripteurs de fichiers (et d’autres choses encore) étant partagés par l’ensem-
ble des threads d’un processus, un mécanisme dit d’« exclusion mutuelle » est fourni afin d’assurer que cer- for (i = 0; i < NBITER; i++)
{
taines opérations seront synchronisées. Pour cela, on utilise un « mutex » dont le type est pthread_mutex_t. rc = pthread_mutex_lock(&mutex);
if(rc)
L’initialisation d’un mutex peut s’effectuer soit de manière statique (en assignant la valeur error(EXIT_FAILURE, rc, "pthread_mutex_lock");
PTHREAD_MUTEX_INITIALIZER au mutex), soit de manière dynamique (à l’aide de la fonction
compteur ++;
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 at- rc = pthread_mutex_unlock(&mutex);
if(rc)
tributs par défaut). error(EXIT_FAILURE, rc, "pthread_mutex_unlock");
pthread_mutex_destroy permet de détruire un mutex (non verrouillé) en précisant l’adresse du mutex. }

' $
pthread_exit(NULL);
}

Exclusions mutuelles (2/2) 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");

¥ Utilisation rc = pthread_create(&thread2, NULL, start_routine, NULL);


if(rc)
¨ int pthread_mutex_lock (pthread_mutex_t *mutex) error(EXIT_FAILURE, rc, "pthread_create");
¨ int pthread_mutex_unlock (pthread_mutex_t *mutex)
# 26 rc = pthread_join(thread1, NULL);
¨ int pthread_mutex_trylock (pthread_mutex_t *mutex) if(rc)
error(EXIT_FAILURE, rc, "pthread_join");
¥ Attributs
rc = pthread_join(thread2, NULL);
¨ Les attributs associés aux MUTEX ne sont pas portables if(rc)
error(EXIT_FAILURE, rc, "pthread_join");
¨ Il convient de ne pas trop les utiliser
¨ Ou de se référer à la documentation en ligne 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);
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.
4.3 Sémaphores POSIX
Il est à noter qu’à ce jour la norme POSIX ne prévoit aucun attribut pour les mutex (des attributs
existent dans certaines implantations, en particulier les mutex récursifs sous Linux, voir les fonctions
pthread_mutexattr_init, pthread_mutexattr_destroy...).
/*
* compteurMutex.c
* ¥ Création et destruction
* Acces au compteur protege par mutex
* mutex_init ne renvoie pas de code d’erreur ¨ int sem_init ( sem_t *sem, int pshared, u_int value)
*
*/ ¨ int sem_destroy ( sem_t *sem)
# 27 ¥ Utilisation
#include <unistd.h>
#include <error.h> ¨ int sem_wait ( sem_t *sem)
#include <stdlib.h>
#include <stdio.h> ¨ int sem_post ( sem_t *sem)
#include <pthread.h>
¨ int sem_trywait ( sem_t *sem)
/* INT_MAX / 2 */
#define NBITER 1000000000 ¨ int sem_getvalue ( sem_t *sem, int *sval)
int compteur = 0;
¥ Toutes ces fonctions renvoient -1 en cas de problème et positionnent errno.

pthread_mutex_t mutex;

void *start_routine(void *arg)

& %
{
int i, rc;

TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Mai 2010 — module CSC4508/M2 133 TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Mai 2010 — module CSC4508/M2 134
Threads ou processus légers 4 Synchronisation Threads ou processus légers 4 Synchronisation

Les sémaphores POSIX font parti de la bibliothèque des threads Linux si la constante _POSIX_SEMAPHORES rc = sem_wait(&infoPrete);
est définie dans unistd.h ; les fonctionnalités sont alors définies dans semaphore.h. Ils sont sensiblement if (rc)
{
différents des sémaphores IPC System V. Le type associé aux sémaphores POSIX est sem_t. perror("sem_wait");
sem_init permet d’initialiser le sémaphore dont l’adresse constitue le premier paramètre et la valeur le }
exit(EXIT_FAILURE);

dernier. Le deuxième paramètre indique si le sémaphore peut être partagé par plusieurs processus (ce qui
n’est pas possible sous Linux — la valeur est donc toujours nulle). consommation( buffer );

sem_destroy détruit le sémaphore passé en argument. /* V(placeDispo) */


rc = sem_post(&placeDispo);
sem_wait attend que le sémaphore passé en argument soit libre pour le prendre. if (rc)
sem_post libère le sémaphore passé en argument. {
perror("sem_post");
sem_trywait renvoie -1 (et place la valeur EAGAIN dans errno) si le sémaphore est déjà pris ; elle renvoie exit(EXIT_FAILURE);
0 dans le cas contraire. }
sem_getvalue place la valeur courante du sémaphore à l’adresse passé en second argument.
pthread_exit(NULL);
/** }
** semaphore.c
** int main()
** synchronisation de deux threads: utilisation d’un semaphore {
** int rc;
** exemple d’un schéma producteur/consommateur pthread_t lecteur;
**
** Le thread principal écrit un caractère dans une zone de mémoire commune. sem_init(&placeDispo, 0, 1);
** Un deuxième thread le lit. sem_init(&infoPrete, 0, 0);
**
** sem_init, sem_wait, sem_post et sem_destroy renvoient -1 en cas de rc = pthread_create( &lecteur, NULL, &lire, NULL);
** probleme et positionnent errno. if(rc)
** error(EXIT_FAILURE, rc, "pthread_create");
**/
ecrire(NULL);
#include <pthread.h>
#include <semaphore.h> rc = pthread_join(lecteur, NULL);
#include <error.h> if(rc)
#include <stdlib.h> error(EXIT_FAILURE, rc, "pthread_join");
#include <stdio.h>
rc = sem_destroy(&placeDispo);
char buffer; if (rc)
sem_t placeDispo; {
sem_t infoPrete; perror("sem_destroy");
exit(EXIT_FAILURE);
void production() }
{ rc = sem_destroy(&infoPrete);
buffer = ’a’; if (rc)
{
printf("producteur: <0x%x> item = %c \n", (unsigned int) pthread_self(), buffer); perror("sem_destroy");
} exit(EXIT_FAILURE);
}
void consommation()
{ return EXIT_SUCCESS;
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) */

TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Mai 2010 — module CSC4508/M2 135 TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Mai 2010 — module CSC4508/M2 136
Threads ou processus légers 4 Synchronisation Threads ou processus légers 4 Synchronisation
' $
pthread_cond_signal réalise la condition passé en paramètre.
4.4 Attente de conditions
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). 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) à
¥ Type : pthread_cond_t partir de laquelle le thread sera réveillé, même si la condition n’a pas été réalisée.
¥ Création pthread_cond_broadcast réalise la condition et réveille tous les fils d’exécution en attente de sa réali-
¨ int pthread_cond_init (pthread_cond_t *cond, const pthread_condattr_t sation.
# 28 Pour utiliser correctement ces fonctions, il faut toujours prendre le mutex associé à la condition avant et
*cond_attr) 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
¨ pthread_cond_init retourne toujours 0. attente de la condition. Une fois que la condition est réalisée, la fonction bloque à nouveau le mutex avant
¥ Destruction de sortir.
¨ int pthread_cond_destroy ( pthread_cond_t *cond) /**
** conditionUn.c
¥ Note : il n’existe pas d’attribut pour les conditions **
** 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
**
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
#include <pthread.h>
pthread_cond_t. Le principe est le suivant : un premier thread se met en attente d’une condition devant #include <error.h>
être réalisée ; lorsqu’un second thread réalise la condition, il émet un signal à destination de la condition qui #include <unistd.h>
#include <stdlib.h>
réveille le thread en attente. Si aucun thread n’est en attente, rien ne se passe ; si plus d’un thread est en #include <stdio.h>
attente, l’un d’eux est réveillé, mais on ne peut déterminer lequel.
char buffer = ’a’;
Une condition peut être initialisée soit statiquement en assignant la valeur définie par la macro pthread_mutex_t mutex;
PTHREAD_COND_INITIALIZER, soit dynamiquement en réalisant un appel à pthread_cond_init. Cette fonc-
pthread_cond_t condPlaceDispo;
tion nécessite deux paramètres : le premier est un pointeur sur la condition à initialiser ; le second est pthread_cond_t condInfoPrete;
un pointeur les attributs associés à la condition (ou la valeur nulle si on désire les attributs par défaut). int infoPrete = 0;

pthread_cond_destroy détruit la condition sur laquelle pointe le paramètre. Il est à noter que la biblio- void production()
thèque de threads Linux n’implémente aucun attribut pour les conditions. {

' $
printf("producteur: <0x%x> buffer = %c \n", (unsigned int) pthread_self(), buffer);
}

Attente de conditions (2/2) void comsommation()


{
printf("consommateur: <0x%x> buffer = %c \n", (unsigned int) pthread_self(), buffer);
}

void *ecrire(void *null)


{
int rc;

/* P(placeDispo) */
¥ Utilisation
rc = pthread_mutex_lock(&mutex);
¨ int pthread_cond_signal ( pthread_cond_t * cond) if(rc)
error(EXIT_FAILURE, rc, "pthread_mutex_lock");
¨ int pthread_cond_wait (pthread_cond_t *cond, pthread_mutex_t *mutex)
# 29
/* while pallie les cas où le thread se reveille alors que le
¨ int pthread_cond_timedwait (pthread_cond_t *cond, pthread_mutex_t predicat n’est pas rempli */
*mutex, const struct timespec *abstime) while (infoPrete)
pthread_cond_wait(&condPlaceDispo, &mutex);
¨ int pthread_cond_broadcast ( pthread_cond_t * cond)
rc = pthread_mutex_unlock(&mutex);
¨ Les fonctions pthread_cond_signal, pthread_cond_wait et if(rc)
error(EXIT_FAILURE, rc, "pthread_mutex_unlock");
pthread_cond_broadcast renvoient toujours 0.
production();

/* V(infoPrete) */
rc = pthread_mutex_lock(&mutex);
if(rc)

& %
error(EXIT_FAILURE, rc, "pthread_mutex_lock");

TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Mai 2010 — module CSC4508/M2 137 TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Mai 2010 — module CSC4508/M2 138
Threads ou processus légers 4 Synchronisation Threads ou processus légers 4 Synchronisation

infoPrete = 1; **
pthread_cond_signal(&condInfoPrete); ** Le thread principal écrit caractère a caractere dans une zone de mémoire commune.
rc = pthread_mutex_unlock(&mutex); ** Un deuxième thread lit caractere a caractere.
if(rc) **
error(EXIT_FAILURE, rc, "pthread_mutex_unlock"); ** mutex_init, cond_init, cond_signal, cond_broadcast et cond_wait ne
** renvoient pas de code d’erreur
return NULL; **
} **/

void *lire(void * null) #include <pthread.h>


{ #include <error.h>
int rc; #include <unistd.h>
#include <stdlib.h>
/* P(infoPrete) */ #include <stdio.h>

rc = pthread_mutex_lock(&mutex); #define NBCHARS 26


if(rc) char buffer = ’a’ - 1;
error(EXIT_FAILURE, rc, "pthread_mutex_lock"); pthread_mutex_t mutex;

/* while pallie les cas où le thread se reveille alors que le pthread_cond_t condPlaceDispo;
predicat n’est pas rempli */ pthread_cond_t condInfoPrete;
while (!infoPrete) int infoPrete = 0;
pthread_cond_wait(&condInfoPrete, &mutex);
rc = pthread_mutex_unlock(&mutex); void production()
if(rc) {
error(EXIT_FAILURE, rc, "pthread_mutex_unlock"); buffer++;

comsommation( buffer ); printf("producteur: <0x%x> buffer = %c \n", (unsigned int) pthread_self(), buffer);
}
/* V(placeDispo) */
rc = pthread_mutex_lock( &mutex); void comsommation()
if(rc) {
error(EXIT_FAILURE, rc, "pthread_mutex_lock"); printf("consommateur: <0x%x> buffer = %c \n", (unsigned int) pthread_self(), buffer);
}
infoPrete = 0;
pthread_cond_signal(&condPlaceDispo); void *ecrire(void *null)
{
rc = pthread_mutex_unlock(&mutex);
if(rc) int i;
error(EXIT_FAILURE, rc, "pthread_mutex_unlock"); for (i = 0; i < NBCHARS; i++)
{
pthread_exit(NULL); int rc;
} /* P(placeDispo) */

int main() rc = pthread_mutex_lock(&mutex);


{ if(rc)
int rc; error(EXIT_FAILURE, rc, "pthread_mutex_lock");
pthread_t lecteur;
/* while pallie les cas où le thread se reveille alors que le
pthread_mutex_init(&mutex, NULL); predicat n’est pas rempli */
while (infoPrete)
pthread_cond_init(&condPlaceDispo, NULL); rc = pthread_cond_wait(&condPlaceDispo, &mutex);
pthread_cond_init(&condInfoPrete, NULL);
rc = pthread_mutex_unlock(&mutex);
rc = pthread_create( &lecteur, NULL, &lire, NULL); if(rc)
if(rc) error(EXIT_FAILURE, rc, "pthread_mutex_unlock");
error(EXIT_FAILURE, rc, "pthread_create");
production();
ecrire(NULL);
/* V(infoPrete) */
rc = pthread_join(lecteur, NULL); rc = pthread_mutex_lock(&mutex);
if(rc) if(rc)
error(EXIT_FAILURE, rc, "pthread_join"); error(EXIT_FAILURE, rc, "pthread_mutex_lock");

rc = pthread_mutex_destroy(&mutex); infoPrete = 1;
if(rc) pthread_cond_signal(&condInfoPrete);
error(EXIT_FAILURE, rc, "pthread_mutex_destroy");
rc = pthread_cond_destroy(&condPlaceDispo); rc = pthread_mutex_unlock(&mutex);
if(rc) if(rc)
error(EXIT_FAILURE, rc, "pthread_cond_destroy"); error(EXIT_FAILURE, rc, "pthread_mutex_unlock");
rc = pthread_cond_destroy(&condInfoPrete);
if(rc) }
error(EXIT_FAILURE, rc, "pthread_cond_destroy");
return NULL;
return EXIT_SUCCESS; }
}
void *lire(void * null)
{
int i;
/** for (i = 0; i < NBCHARS; i++)
** conditionMult.c {
** int rc;
** synchronisation de deux threads: utilisation d’une variable condition
** rc = pthread_mutex_lock( &mutex);
** exemple d’un schéma producteur/consommateur if(rc)

TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Mai 2010 — module CSC4508/M2 139 TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Mai 2010 — module CSC4508/M2 140
Threads ou processus légers Threads ou processus légers 5 Implantation des threads
' $
error(EXIT_FAILURE, rc, "pthread_mutex_lock");
5 Implantation des threads
/* 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) */ 5.1 Threads utilisateurs et threads noyaux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31


pthread_cond_signal(&condPlaceDispo);
# 30 5.3 Linux threads . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
rc = pthread_mutex_unlock(&mutex);
if(rc) 5.3 Exploitation des architectures multi-processeurs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
error(EXIT_FAILURE, rc, "pthread_mutex_unlock");
5.4 Limitations des threads . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
}

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) 5.1 Threads utilisateurs et threads noyaux
error(EXIT_FAILURE, rc, "pthread_create");

ecrire(NULL);

rc = pthread_join(lecteur, NULL);
if(rc)
¥ Threads utilisateurs : gérés par une bibliothèque utilisateur
error(EXIT_FAILURE, rc, "pthread_join"); ¨ Pas d’intervention du noyau
rc = pthread_mutex_destroy(&mutex); ¨ Rapide à la création et à la commutation
if(rc)
error(EXIT_FAILURE, rc, "pthread_mutex_destroy"); ¨ Portable
rc = pthread_cond_destroy(&condPlaceDispo);
¨ Ordonnancement limité à celui du processus d’accueil donc limité à une
if(rc) # 31
error(EXIT_FAILURE, rc, "pthread_cond_destroy"); architecture monoprocesseur
rc = pthread_cond_destroy(&condInfoPrete);
if(rc) ¥ Threads noyaux : supportés par le noyau (parfois appelés LightWeight Process)
error(EXIT_FAILURE, rc, "pthread_cond_destroy");
¨ Ordonnancement indépendant des différents threads donc exploitation possible
return EXIT_SUCCESS;
} des architectures multi-processeurs, hyper-threading
¨ Nécessité d’appels systèmes spécifiques
¨ Commutation par le noyau avec changement de contexte
¥ Différents modèles d’implantation : M :1, M :N, 1 :1

& %

On distingue deux catégories de threads : les threads utilisateurs et les threads noyaux. Les threads util-
isateurs sont implantés par une bibliothèque utilisateur au-dessus du noyau. Cette bibliothèque fournit la
création, l’ordonnancement et la gestion des threads sans intervention du noyau. Comme le noyau n’a pas
connaissance des threads, chaque thread est créé et ordonnancé au niveau de l’utilisateur. C’est pourquoi les
threads utilisateurs sont généralement rapides à créer et à gérer. Néanmoins, si un thread utilisateur bloque
sur un appel système, le processus entier sera bloqué même si les autres threads sont prêts à s’exécuter.
Pour éviter cela, la bibliothèque utilisateur doit réordonnancer les threads en cas d’appel système. Enfin les
threads ne pourront s’exécuter que sur un seul processeur puisqu’ils sont contenus dans un seul processus.
Les threads noyaux sont supportés directement par le système d’exploitation. Le noyau effectue la création,
l’ordonnancement et la gestion des threads directement dans l’espace noyau. Comme ils sont gérés par le
système d’exploitation, les threads noyaux sont souvent plus lents à la création et pour la gestion que les

TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Mai 2010 — module CSC4508/M2 141 TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Mai 2010 — module CSC4508/M2 142
Threads ou processus légers 5 Implantation des threads 5 Implantation des threads 5.3 Exploitation des architectures multi-processeurs
' $
threads utilisateurs. Cependant, comme le noyau gère les threads, l’ordonnancement des différents threads
5.3 Exploitation des architectures multi-processeurs
se fait indépendamment les uns des autres. Donc si un thread effectue un appel système bloquant, le noyau
peut ordonnancer un autre thread de l’application pour l’exécution.
De plus, dans le cas d’une architecture multi-processeurs, le noyau peut ordonnancer les threads sur ¥ Exploitation naturelle d’architectures multi-processeurs à mémoire partagée
plusieurs processeurs simultanément. ¨ Calculateurs à mémoire partagée
¨ Architectures SMP (Symmetric MultiProcessor)
Dnas le modèle M :1 (Many to One), plusieurs threads utilisateurs sont gérés par un seul thread noyau.
Ce modèle est utile pour les architectures ne supportant pas les threads noyaux. Solaris Green Thread est ◮ Plusieurs processeurs sur une carte mère
un exemple de ce modèle M :1. ¨ Processeurs multi-cœurs (CMP : Chip MultiProcessor)
◮ Plusieurs processeurs sur une même puce
Le modèle 1 :1 (One to One) fait correspondre un thread utilisateur à un thread noyau. Cette implantation # 33
va permettre une exécution concurrente des threads ainsi que l’exploitation d’architectures multi-processeurs. ¥ Exploitation de la technologie hyper-threading
Néanmoins, le surcoût de création des threads noyaux peut affecter les applications. Dans la plupart des cas, ¨ Un processeur : deux processeurs virtuels vus par le système d’exploitation
les implantations de ce modèle limitent alors le nombre de threads que l’on peut créer. ¨ Ressources du processeur inexploitées par un thread utilisées par un autre thread
¥ Exécution de calculs en parallèle sur plusieurs processeurs (éventuellement virtuels)
Le modèle M :N (Many to Many) fait correspondre plusieurs threads utilisateurs à un nombre inférieur
ou égal de threads noyaux. Le nombre de threads noyaux peut dépendre de l’application ou du système ¨ Programme de test : décrémentation de 2 compteurs
d’exploitation. Ce modèle permet l’exécution concurrente de plusieurs threads, l’exploitation de machines ◮ Exécution séquentielle avec 1 seul thread
multi-processeurs et les développeurs peuvent créer autant de threads utilisateurs qu’ils le souhaitent. Un ◮ Exécution parallèle avec 1 thread par compteur
exemple d’implantation du modèle M :N sont les threads Solaris 2 de la bibliothèque UI threads. ¨ Tests sur une machine monoprocesseur et multiprocesseur

& %
' $
5.2 Linux threads Documentation en ligne : Hyper-threading : http ://www.intel.com : Technology and Research :
Computing : Architectural Innovation : Multi-core : Intel multi-core demos : Intel Dual-Core Processor Demo
¥ threads → tâche (tasks)
¥ création d’un thread : appel système clone() http ://multicore.amd.com : What is Multi-core ?
¨ clone() permet à une tâche enfant (thread) de partager l’espace d’adressage de la
tâche parent (processus) Interactivement pendant le cours : Exécution de programmes en parallèle.
¥ Bibliothèques implantant la norme Pthread Le programme, compteur1.c, est mono-thread. Il décrémente successivement deux compteurs compteur1
¨ À partir de 1996 LinuxThread et compteur2. Le programme, compteur2procs.c, peut s’exécuter en parallèle sur une machine multipro-
◮ Modèle 1 :1 cesseur. Il utilise 2 processus mono-thread, chaque processus décrémentant un compteur. La valeur du comp-
# 32 teur calculé par le processus enfant est retourné au processus parent par l’appel à exit. Il faut noter que
◮ Créée par X. Leroy
cette valeur est alors limitée à 8 bits. Le programme, compteur2threads.c, peut s’exécuter en parallèle sur
◮ Pas entièrement conforme à POSIX (gestion des signaux, synchronisation)
une machine multiprocesseur. Il utilise 1 processus multi-thread avec un thread principal et un thread pair.
¨ Projet IBM Next Generation POSIX Threads : abandonné
Chaque thread décrémente un compteur. La valeur du compteur calculé par le thread pair est retourné au
◮ Modèle M :N
processus parent par l’appel à pthread_exit. Il aurait, bien sûr, été possible d’utiliser la mémoire partagée.
¨ Maintenant Native POSIX Thread Library Pour chacun des programmes :
◮ Modèle 1 :1 – mesurer son temps d’exécution sur une machine monoprocesseur et sur une machine biprocesseur,
◮ Créée par U. Drepper – visualiser les informations du ou des processus avec ps,top et/ou xosview, les comparer,
◮ Totalement conforme à POSIX – comparer les temps d’exécution ; donner une première explication.
◮ Bénéficie des fonctionnalités du noyau Linux 2.6
& % Exécution sur une machine bi-processeur en 2006 : Sur anaconda, il manque le package procps
pour visualiser les différents threads avec top et ps. En revanche xosview semble fonctionner.

Il existe plusieurs bibliothèques de threads sous Linux. Les plus couramment utilisées sont LinuxThread Exécution sur une machine bi-processeur en 2005 : L’exécution sur une machine biprocesseur,
et plus récemment NPTL. Pour connaître celle utilisée : exécuter dans notre cas elaphe, montre que les threads sont indépendamment ordonnancés sur un processeur ou
l’autre. La commande top, (avec les options u et H interactivement) peut montrer la situation suivante :

PID USER PRI NI SIZE RSS SHARE STAT %CPU %MEM TIME CPU COMMAND
12601 silber 25 0 320 320 264 R 19,6 0,0 0:01 0 compteur2thread
/lib/libc.so.6 12600 silber 22 0 320 320 264 R 11,7 0,0 0:01 1 compteur2thread

Remarquez que les deux threads s’exécutent sur des processeurs différents. Remarquez enfin que sur
elaphe, les PID apparaissent différents. Néanmoins des appels à getpid par chaque thread renvoient bien
une valeur unique !

TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Mai 2010 — module CSC4508/M2 143 TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Mai 2010 — module CSC4508/M2 144
5 Implantation des threads 5.3 Exploitation des architectures multi-processeurs Threads ou processus légers 5 Implantation des threads
' $
mande à une entité esclave d’effectuer l’impression sur une imprimante donnée. Chaque esclave peut effectuer
5.3.1 Utilisation des threads
une impression à un instant donné sur une imprimante. Le gestionnaire est chargé de traiter les suppressions
¥ Pourquoi utiliser les threads ? des requêtes d’impressions.
¨ Améliorer la réactivité des applications
¨ Partager des ressources Dans le modèle diviser pour régner, les entités effectuent les tâches en parallèle, indépendamment les
¨ Économiser du temps et de la place mémoire unes des autres. Il n’y a pas d’entité maître.
¨ Exploiter des architectures multi-processeurs
¥ Exemples d’architectures logicielles Un exemple de modèle diviser pour régner serait d’exécuter une commande grep parallèle. La commande
¨ Maître/esclave grep établit tout d’abord un ensemble de fichiers à examiner. Elle crée ensuite un ensemble d’entités. Chaque
# 34 entité traite un fichier et effectue la recherche du schéma envoyant le résultat sur une sortie commune. Quand
¨ Diviser pour régner
une entité termine sa recherche dans le fichier, elle traite un autre fichier ou s’arrête.
¨ Producteur/consommateur
¥ Exemples d’applications
¨ Traitement de texte Le modèle producteur/consommateur représente typiquement une chaîne de production (modèle
¨ Navigateur web pipeliné).
¨ Serveur web
¥ Pour mon application ? Voilà quelques exemples d’application pouvant tirer profit d’une implantation multi-thread. Un navigateur
¨ Identifier des opérations coûteuses 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
& %
¨ Déterminer la taille des données à partager (si possible petite)
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.
¨ Identifier les (nombreux ?) problèmes de verrous
¨ Répartir les tâches indépendantes 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.

# 35 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
Programmer une application interactive avec des threads peut permettre à un programme de continuer à P(s) P(t)
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. P(t) P(s)
Cela permet donc d’accroître l’interactivité de l’application. V(s) V(t)
Les architectures maître/esclave, diviser pour régner et producteurs/consommateurs sont des exemples V(t) V(s)
d’architectures pouvant être facilement implantées avec des threads. Ces architectures mènent toutes à des ou avec s = 1 et t = 0 :
programmes modulaires efficacement implantés par des threads. Thread 1 Thread 2
Dans le cas d’une architecture maître/esclave, une entité maître reçoit la ou les requêtes et crée les P(s) P(s)
entités esclaves pour les exécuter. Le maître contrôle, par exemple, le nombre d’esclaves existants et ce que V(s) V(s)
fait chaque esclave. Un esclave s’exécute indépendamment des autres esclaves. P(t) P(t)
Un gestionnaire d’impressions est un exemple d’architecture maître/esclaves. Un gestionnaire d’impres- V(t) V(t)
sions gère plusieurs imprimantes, s’assure que toutes les requêtes d’impression reçues sont effectuées en un – Les tâches peuvent-elles être réparties en plusieurs ? par exemple un thread pour la gestion des signaux,
temps raisonnable. Quand le gestionnaire reçoit une requête, l’entité maître choisit une imprimante et de- un autre pour l’interface graphique, ...

TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Mai 2010 — module CSC4508/M2 145 TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Mai 2010 — module CSC4508/M2 146
Threads ou processus légers Threads ou processus légers 6 Autres fonctions de la bibliothèque POSIX threads
' $ ' $
5.4 Limitations des threads 6 Autres fonctions de la bibliothèque POSIX 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
6.1 Annulation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
# 36 ¥ Mort d’un thread → mort de l’application entière # 37 6.2 Nettoyage des ressources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
¥ Problématiques
6.4 Initialisation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
¨ Exécution d’un fork
◮ Duplication de tous les threads ou nouveau processus mono-thread 6.4 Données privées . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
¨ Gestion de signaux : à qui envoyer le signal ?
◮ Au thread auquel il s’applique ?
◮ À tous les threads du processus ?
◮ À certains threads ?
◮ À un thread spécifique qui se chargera de le gérer correctement ?
¨ Éviter de communiquer par signaux dans une application multi-thread !
& % & %

' $
6.1 Annulation

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, ¥ Envoi d’une requête d’annulation
les identifiants d’utilisateurs et de groupe et les permissions qui leur sont associées sont gérés au niveau du ¨ int pthread_cancel (pthread_t thread)
processus. Les programmes qui ont besoin d’affecter un utilisateur différent aux différentes entités de leur ¥ Définition du comportement
programme utiliseront plusieurs processus plutôt qu’un seul processus possédant plusieurs threads. D’autre # 38
¨ int pthread_setcancelstate (int state, int * oldstate)
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 → state = PTHREAD_CANCEL_[ENABLE|DISABLE]
application ayant besoin de gérer ces attributs de manière indépendante utilisera plusieurs processus. Dans ¥ Définition du type d’annulation (si PTHREAD_CANCEL_ENABLE)
un programme multi-thread, les sémantiques de fork() et de exec() peuvent être modifiées. Si un thread ¨ int pthread_setcanceltype (int type, int * oldtype)
appelle fork() à l’intérieur d’un programme, ce nouveau processus doit-il dupliquer tous les threads ? Ce → type = PTHREAD_CANCEL_[DEFERRED|ASYNCHRONOUS]
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 Un thread peut vouloir annuler un autre thread. Il envoie alors une demande d’annulation par l’inter-
Blaess p. 291) 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 traitement d’un signal dépend du type de signal. Un signal synchrone (accès illégal à la mémoire, Le thread récepteur peut accepter la requête, la refuser ou la repousser jusqu’à atteindre un « point
division par zéro) doit être envoyé au thread concerné et non aux autres. Pour les signaux asynchrones, la d’annulation ». Ceci est particulièrement intéressant si le code exécuté par le thread est sensible (comme la
situation n’est pas claire. Certains signaux asynchrones comme Control-C doivent être envoyés à tous les manipulation des sémaphores par exemple).
threads du processus. Certaines implantations d’UNIX permettent à un thread de spécifier quel signal il pthread_setcancelstate permet de préciser si la prochaine requête d’annulation sera prise en compte
recevra et quel signal il bloquera. Cependant, les signaux ne doivent être gérés qu’une seule et unique fois. (PTHREAD_CANCEL_ENABLE) ou non (PTHREAD_CANCEL_DISABLE). Le second paramètre permet de récupérer
C’est pourquoi, le signal n’est souvent envoyé qu’au premier thread qui ne bloque pas le signal. Solaris 2 l’état précédent.
implante la quatrième solution, un thread spécifique gère tous les signaux. 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 — Mai 2010 — module CSC4508/M2 147 TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Mai 2010 — module CSC4508/M2 148
Threads ou processus légers 6 Autres fonctions de la bibliothèque POSIX threads 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 ; Un point d’annulation pouvant intervenir à n’importe quel moment et les ressources associées aux threads
PTHREAD_CANCEL_ASYNCHRONOUS termine le thread dès réception de la requête. Le second paramètre permet 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
de récupérer l’état précédent. place afin de libérer ses ressources avant qu’il se termine vraiment.
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 Pour une ressource que l’on vient d’allouer à un thread, pthread_cleanup_push permet de préciser la
autorisées, les demandes arrivant pendant les périodes de refus seraient systématiquement perdues. 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.
Annulation (2/2)
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
macros dont la première ouvre un bloc lexical que ferme la seconde. Les deux appels doivent appartenir
au même bloc d’instructions.

¥ Test d’annulation explicite ' $


# 39 ¨ void pthread_testcancel (void)
6.3 Initialisation
¥ Test d’annulation implicite
¨ pthread_cond_wait, pthread_cond_timedwait
¨ pthread_join, sem_wait, sigwait

¥ À l’entrée d’une fonction


¨ Type : pthread_once_t
¨ Valeur : PTHREAD_ONCE_INIT
& %
# 41
¨ int pthread_once ( pthread_once_t *once_control, void (*init_routine) (
) )
Il existe deux types de point d’annulation. Les points d’annulation explicites sont précisés par le program-
¥ Lors d’un fork
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- ¨ int pthread_atfork (void (*prepare) ( ), void (*parent) ( ), void
tèmes (et surtout de leur implantation !). Se reporter à la documentation (GNU en particulier) pour plus (*child) ( ))
d’information.
' $
6.2 Nettoyage des ressources
& %

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
¥ Deux routines plusieurs threads l’appellent simultanément.
# 40 ¨ void pthread_cleanup_push (void (* routine) (void *), void *arg)
Le type pthread_once_t est opaque. Pour être utile, une variable de ce type doit être déclarée de
¨ void pthread_cleanup_pop (int execute) manière statique ou globale (afin de ne pas avoir une copie de la variable pour chaque appel de fonction).
¥ Attention L’initialisation s’effectue à l’aide de la valeur prédéfinie PTHREAD_ONCE_INIT. La fonction précisée en second
¨ Les deux appels doivent appartenir au même bloc d’instructions 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’ensemble des ressources (en particuliers les piles d’exécutions et les segments de mémoire alloués dynamique-
ment 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 respective-
& %
ment 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 — Mai 2010 — module CSC4508/M2 149 TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Mai 2010 — module CSC4508/M2 150
Threads ou processus légers 6 Autres fonctions de la bibliothèque POSIX threads Threads ou processus légers
' $
6.4 Données privées

¥ Créer des variables globales spécifiques à un thread


¥ Création
¨ int pthread_key_create (pthread_key_t *cle, void (*destr_fonction ) (
void * )
# 42
¥ 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.

TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Mai 2010 — module CSC4508/M2 151 TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Mai 2010 — module CSC4508/M2 152
Éléments d’architecture client-serveur 1 Introduction
' $
Plan du document

1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
#2 2 Serveur mono-tâche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
3 Serveur avec autant de tâches que de clients . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
4 Serveur avec N tâches gérant tous les clients . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16

Éléments d’architecture
client-serveur
& %

' $
1 Introduction

Michel Simatic

module CSC4508/M2

#3 1.1 Définition d’une architecture client/serveur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4


1.3 Objectif de cette présentation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.3 À propos des communications . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6

& %

Mai 2010

153 TELECOM SudParis — Michel Simatic — Mai 2010 — module CSC4508/M2 154
Éléments d’architecture client-serveur 1 Introduction Éléments d’architecture client-serveur 2 Serveur mono-tâche
' $ ' $
1.1 Définition d’une architecture client/serveur 1.3 À propos des communications
¥ Une application fonctionne selon une architecture client-serveur quand : les machines
« clientes » contactent une machine « serveur » afin que ce serveur leur fournisse un
¥ Tout processus (client ou serveur) peut recevoir des messages via un (ou plusieurs)
service (via l’exécution d’un programme)
« point(s) d’accès » qui lui est (sont) propre(s)
¥ Les clients envoient des requêtes