Académique Documents
Professionnel Documents
Culture Documents
module CSC4508/M2
Avril 2016
Concepts des systèmes d’exploitation et mise en oeuvre sous Unix
Contents
Licence ix
Présentation du cours 1
Plan du document 2
1 Objectifs du cours 2
3 Déroulement 4
Plan du document 6
1 Systèmes informatiques 6
2 Machine virtuelle 6
Plan du document 12
Gestion de la mémoire 27
Plan du document 28
Bibliographie du chapitre 54
Plan du document 56
2 Bibliothèque C d’entrées-sorties 66
2.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
2.2 Ouverture/fermeture de fichier . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
2.3 Lecture de fichier . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
Lecture de fichier (suite) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
2.4 Écriture de fichier . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
Écriture de fichier (suite) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
2.5 Contrôle du tampon . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
2.6 Divers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
6 Limitations de NFS 75
Bibliographie du chapitre 76
Communications inter-processus 77
Plan du document 78
1 Introduction 106
1.1 Correspondance problèmes vie courante/informatique . . . . . . . . . . . . . . . . . . . . . 106
TELECOM SudParis — Coordonnateur : François Trahay — Avril 2016 — module CSC4508/M2 iii
Concepts des systèmes d’exploitation et mise en oeuvre sous Unix
4 Interblocage 122
4.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122
4.2 Généralités . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122
1 Présentation 126
1.1 Bibliographie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126
1.2 Threads . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127
1.3 Détacher le flot d’exécution des ressources . . . . . . . . . . . . . . . . . . . . . . . . . . . . 128
1.3.1 Vision traditionnelle d’un processus . . . . . . . . . . . . . . . . . . . . . . . . . . . 128
1.3.2 Autre vision d’un processus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129
1.3.3 Processus multi-thread . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129
4 Synchronisation 143
4.1 Synchronisation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143
4.2 Exclusions mutuelles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144
Exclusions mutuelles (2/2) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144
4.3 Sémaphores POSIX (rappel) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146
4.4 Attente de conditions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148
Attente de conditions (2/2) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148
Architecture 161
1 Introduction 162
1.1 Loi de Moore . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 162
3 Pipeline 163
3.1 Micro architecture d’un pipeline . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 164
3.2 Processeurs superscalaires . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 164
3.3 Processeurs superscalaires . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165
3.4 Dépendance entre instructions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165
3.5 Gestion des branchements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166
3.6 Prédiction de branchement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166
3.7 Instructions vectorielles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 167
1 Introduction 178
1.1 Définition d’une architecture client/serveur . . . . . . . . . . . . . . . . . . . . . . . . . . . 178
1.2 Objectif de cette présentation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 179
1.3 À propos des communications . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 180
6 Conclusion 196
2 Debugging 198
4 IPC 199
Bibliographie 201
5 Divers 204
6 Bibliographie 205
Index 209
TELECOM SudParis — Coordonnateur : François Trahay — Avril 2016 — module CSC4508/M2 vii
Concepts des systèmes d’exploitation et mise en oeuvre sous Unix
TELECOM SudParis — Coordonnateur : François Trahay — Avril 2016 — module CSC4508/M2 viii
Concepts des systèmes d’exploitation et mise en oeuvre sous Unix
' $
Licence
Ce document est une documentation libre, placée sous la Licence de Documentation Libre GNU (GNU
Free Documentation License).
Copyright (c) 2003-2016 Frédérique Silber-Chaussumier, Michel Simatic et François
Trahay
Permission est accordée de copier, distribuer et/ou modifier ce document selon les
termes de la Licence de Documentation Libre GNU (GNU Free Documentation License),
version 1.2 ou toute version ultérieure publiée par la Free Software Foundation; avec
#1 les Sections Invariables qui sont ‘Licence’ ; avec les Textes de Première de Couverture
qui sont ‘Concepts des systèmes d’exploitation et mise en oeuvre sous Unix’
et avec les Textes de Quatrième de Couverture qui sont ‘Help’.
Une copie de la présente Licence peut être trouvée à l’adresse suivante :
http://www.gnu.org/copyleft/fdl.html.
Remarque : La licence comporte notamment les sections suivantes : 2. COPIES VERBATIM, 3. COPIES
EN QUANTITÉ, 4. MODIFICATIONS, 5. MÉLANGE DE DOCUMENTS, 6. RECUEILS DE
DOCUMENTS, 7. AGRÉGATION AVEC DES TRAVAUX INDÉPENDANTS et 8. TRADUCTION.
& %
Ce document est préparé avec des logiciels libres :
• LATEX : les textes sources sont écrits en LATEX (http://www.latex-project.org/, le site du Groupe
francophone des Utilisateurs de TEX/LATEX est http://www.gutenberg.eu.org). Une nouvelle classe
et une nouvelle feuille de style basées sur la classe seminar ont été tout spécialement dévélop-
pées: newslide (projet picoforge newslide, http://picoforge.int-evry.fr/projects/slideint)
et slideint (projet picoforge slideint, http://picoforge.int-evry.fr/projects/slideint);
• emacs: tous les textes sont édités avec l’éditeur GNU emacs (http://www.gnu.org/software/emacs);
• dvips: les versions PostScript (PostScript est une marque déposée de la société Adobe Systems Incor-
porated) des transparents et des polycopiés à destination des élèves ou des enseignants sont obtenues
à partir des fichiers DVI (« DeVice Independent ») générés à partir de LaTeX par l’utilitaire dvips
(http://www.ctan.org/tex-archive/dviware/dvips);
• ps2pdf et dvipdfm: les versions PDF (PDF est une marque déposée de la société Adobe Sys-
tems Incorporated) sont obtenues à partir des fichiers Postscript par l’utilitaire ps2pdf (ps2pdf
étant un shell-script lançant Ghostscript, voyez le site de GNU Ghostscript http://www.gnu.org/-
software/ghostscript/) ou à partir des fichiers DVI par l’utilitaire dvipfm;
• makeindex: les index et glossaire sont générés à l’aide de l’utilitaire Unix makeindex
(http://www.ctan.org/tex-archive/indexing/makeindex);
• TeX4ht: les pages HTML sont générées à partir de LaTeX par TeX4ht (http://www.cis.ohio-
-state.edu/~gurari/TeX4ht/mn.html);
• Xfig: les figures sont dessinées dans l’utilitaire X11 de Fig xfig (http://www.xfig.org);
• fig2dev: les figures sont exportées dans les formats EPS (« Encapsulated PostScript ») et PNG
(« Portable Network Graphics ») grâce à l’utilitaire fig2dev (http://www.xfig.org/userman/-
installation.html);
• convert: certaines figures sont converties d’un format vers un autre par l’utilitaire convert
(http://www.imagemagick.org/www/utilities.html) de ImageMagick Studio;
• HTML TIDY: les sources HTML générés par TeX4ht sont « beautifiés » à l’aide de HTML TIDY
(http://tidy.sourceforge.net) ; vous pouvez donc les lire dans le source;
Nous espérons que vous regardez cette page avec un navigateur libre: Mozilla ou Firefox par exemple. Comme
l’indique le choix de la licence GNU/FDL, tous les éléments permettant d’obtenir ces supports sont libres.
Avant-propos
Ce cours est préparé et dispensé depuis de nombreuses années par les enseignants de l’Institut Na-
tional des Télécommunications pour le module de deuxième année ASR3 de Télécom INT. Que soient
remerciées ici toutes les personnes qui ont contribuées à ce cours : Christian Bac, Djamel Belaïd, Olivier
Berger, Guy Bernard, Dominique Bouillet, Denis Conan, Daniel Millot, Christian Schüller, Frédérique
Silber-Chaussumier, Michel Simatic et Eric Renault.
Certaines parties de ce cours ne seraient pas sans les nombreux échanges avec Catherine Coquery et
Claude Kaiser du Département Informatique du CNAM : qu’ils en soient ici également remerciés.
Et puis un grand merci aux personnes qui ont permis au logo CSC4508 d’exister : George W. Hart
(http://www.georgehart.com) qui a autorisé l’utilisation de photos de sa sculpture Knot structured pour
ce logo et Steeve Jouannet et Marie-Christine Monget qui ont travaillé à l’intégration de ces photos dans le
logo.
Les transparents et le polycopié ont été réalisés grâce à LaTeX et aux travaux de Philippe Lalevée
et Denis Conan.
TELECOM SudParis — Coordonnateur : François Trahay — Avril 2016 — module CSC4508/M2 xii
Présentation du cours
Michel Simatic
module CSC4508/M2
Avril 2016
1
Présentation du cours
' $
Plan du document
#2 1 Objectifs du cours . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
2 Public visé et pré-requis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
3 Déroulement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
& %
' $
1 Objectifs du cours
« Ceux qui sont férus de pratique sans posséder la science sont comme le pilote qui
s’embarquerait sans timon, ni boussole et ne saurait jamais où il va » (Léonard de
Vinci)
Objectifs
(Re)découvrir les concepts de base d’un système d’exploitation (processus,
#3
gestion de la mémoire, fichiers, threads)
I Prendre conscience des hypothèses choisies par les concepteurs/développeurs
de système d’exploitation
I Comprendre, en expérimentant sous Linux, les impacts de ces hypothèses
Étudier les mécanismes de communication entre processus d’une même machine
Comprendre les problèmes de synchronisation de processus et les patrons de
conception associés
& %
À la place de patron de conception, certains auteurs parlent de paradigme, c’est-à-dire un modèle théorique
de pensée qui oriente la recherche et la réflexion scientifique.
Ce cours fait la transition entre la première et la troisième année :
• 1ère année :
• 2è année :
– CSC4508 (partie interaction entre les programmes et le système d’exploitation) : Interactions :
' $
2 Public visé et pré-requis
Public visé
Futurs ingénieurs (développeurs, spécifieurs, architectes. . .) dont le cœur de
métier (Télécoms, spatial. . .) utilise l’outil informatique
Futurs chercheurs en systèmes (répartis)
#4
Pré-requis
Algorithmie (notions)
Architectures matérielles (notions)
Langage C (bonne pratique)
Unix utilisateur (bonne pratique)
& %
Si un participant veut parfaire ses connaissances liées aux pré-requis, il pourra se reporter avec profit
vers les sites suivants :
• Algorithmie : cours Algorithmique et programmation (CSC3002, http://cours.it-sudparis.eu/
moodle)
• Architectures matérielles : cours Architecture matérielle et logicielle d’un ordinateur (CSC3501, http:
//cours.it-sudparis.eu/moodle)
• Langage C :
– Cours C (http://picolibre.int-evry.fr/projects/coursc/)
– Cours Algorithmique, Langage C et Structures de Données (CSC3002, http://cours.
it-sudparis.eu/moodle)
• Unix utilisateur : cours Initiation à Unix (http://www-inf.it-sudparis.eu/cours/UNIX/CSC3001.
html)
• papi, papi-devel
• valgrind
Michel Simatic
module CSC4508/M2
Avril 2016
5
Introduction : rôle d’un système d’exploitation
' $
Plan du document
2 Systèmes informatiques . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
#2 2 Machine virtuelle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
4 Objectifs d’un système d’exploitation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
4 Différents types de systèmes d’exploitation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
& %
' $
1 Systèmes informatiques
& %
#4
& %
' $
3 Objectifs d’un système d’exploitation
& %
' $
4.1 Temps réel souple versus temps réel strict
Contraintes
Réagir impérativement dans un laps de temps déterminé (durée fonction du
domaine)
Sûreté de fonctionnement : il s’agit d’assurer un service permanent fiable car un
arrêt (partiel ou total) aurait des conséquences désastreuses.
#8
Axiomes de base lors de la spécification/conception
Choix de solutions sans aucun risque (par exemple en termes de blocages)
Service minimum pour les opérations critiques
Redondance : matérielle (doublement des organes vitaux, dont l’unité centrale)
et logicielle (procédures de contrôle, reprise. . .)
I Mode maître/esclave
I Mode partage de charge
& %
' $
4.3 Transactionnel
Caractéristiques
Gestion d’informations en grande quantité
Exécution simultanée d’opérations prédéfinies
Accès au service de façon interactive
Grand nombre de terminaux raccordés
#9 Garantie au niveau performance (temps de réponse, sécurité, fiabilité. . .).
Solutions
Ajouter la gestion des communications à une application existante (vente par
correspondance)
Développer une application intégrant les communications (réservation de place)
Moniteurs transactionnels d’origine constructeurs ou tierce-partie comme Tuxedo
(société BEA) : optimiser la charge, faciliter la programmation, prise en compte
des aspects session et communication par le moniteur, fiabilité. . .
& %
Michel Simatic
module CSC4508/M2
Avril 2016
11
Interactions entre système multi-tâche et processus 1 Point de vue processus
' $
Plan du document
& %
' $
1 Point de vue processus
& %
#4
& %
' $
1.2 La programmation système
& %
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. . .
& %
' $
1.4 Utilisation des appels systèmes
& %
Retournent une valeur de type divers (entier, caractère, pointeur. . .). Voir le manuel
de référence pour chacune d’entre elles
& %
' $
1.6 Test du retour des appels système et des fonctions
& %
Le fichier errno.h associe des mnémoniques à chaque erreur « standard ».
Le man de chaque appel système et de chaque fonction explique, dans la section ERRORS, les différents
codes d’erreur qui peuvent être renvoyés.
Un exemple de « presque toujours » est la fonction printf(3). Pourquoi ?
Quelques questions pour s’entraîner sur les appels systèmes et les fonctions :
• Voyez la page du manuel de l’appel système stat(2)
– Que fait cet appel système ?
– Pourquoi faut-il lui passer un pointeur sur une structure stat en paramètre ?
– Avant d’appeler stat, ce pointeur doit être initialisé pour pointer sur une zone mémoire, pourquoi ?
Sinon que se passe-t-il ?
Témoignage d’un ancien ASR : « Sans insistance de votre [NDLR : Michel Simatic] part, cela ne nous
aurait pas sauté si vite aux yeux que les problèmes (en début de la coupe robotique) venait d’un manque de
gestion des erreurs sur un code qui n’avait pas été relu par suffisamment de monde. »
' $
1.7 Que faire en cas d’erreur système ?
La fonction strerror() permet d’obtenir la chaîne de caractères affichée par perror() sans pour autant
provoquer d’affichage.
__FILE__, __LINE__ et __func__ sont des macros définies par le standard C99 et sont donc valides pour
tout compilateur.
La fonction abort() termine de manière anormale le processus en cours. Un core dump est généré
(analysable ultérieurement par un débuggueur), si la limite fixée par le shell l’autorise. Par exemple sous bash,
ulimit -c permet de voir la taille limite du core dump (il faut taper la commande ulimit -c unlimited
pour n’avoir aucune limite).
Voici un exemple d’utilisation de la fonction assert :
/*******************/
/* exempleAssert.c */
/*******************/
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <assert.h>
int main(){
struct stat buf;
int rc;
rc = stat("unFichierQuiNExistePas", &buf);
assert(rc >= 0);
return EXIT_SUCCESS;
}
assert permet de détecter le problème, mais il faut passer sous debugger pour mieux comprendre l’origine
de l’erreur lors de l’appel système (en affichant le contenu de la valeur errno). Notez qu’il faut aussi que le
programme ait été compilé avec l’option -g, que ulimit -c valait unlimited au moment de l’erreur, et que
la variable errno soit accessible au debugger.
De ce fait, un programmeur, utilisant le compilateur gcc, peut préférer error ou error_at_line dont
voici un exemple :
/**************************/
/* exempleErroc_at_line.c */
/**************************/
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
#include <error.h>
int main(){
struct stat buf;
int rc;
rc = stat(NOM_FICHIER, &buf);
if (rc < 0){
error_at_line(EXIT_FAILURE, errno, __FILE__, __LINE__,
"Pb au moment de l’appel a stat sur fichier \"%s\"",
NOM_FICHIER);
}
return EXIT_SUCCESS;
}
Le programmeur dispose ainsi d’un affichage plus clair quant à l’origine de l’erreur. Toutefois, error et
error_at_line présentent 2 inconvénients. Tout d’abord, elles sont spécifiques à gcc : elles ne sont donc pas
exploitables avec d’autres compilateurs. Surtout, elles ne déclenchent pas la création d’un core. De ce fait,
il n’est pas possible d’analyser le core généré au moment de l’erreur, de manière à comprendre comment le
programme est arrivé au niveau de cette erreur.
C’est pourquoi nous recommandons l’utilisation de la macro ERROR_AT_LINE dont voici un exemple :
/**************************/
/* exempleERROR_AT_LINE.c */
/**************************/
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#define ERROR_AT_LINE(status,errnum,filename,linenum,...) { \
fprintf(stderr,"%s:%d:", filename,linenum); \
fprintf(stderr,__VA_ARGS__); \
fprintf(stderr,":%s\n", strerror(errnum));\
abort(); \
}
int main(){
struct stat buf;
int rc;
rc = stat(NOM_FICHIER, &buf);
if (rc < 0){
ERROR_AT_LINE(EXIT_FAILURE, errno, __FILE__, __LINE__,
"Pb au moment de l’appel a stat sur fichier \"%s\"",
NOM_FICHIER);
}
return EXIT_SUCCESS;
}
exempleERROR_AT_LINE.c:32:Pb au moment de l’appel a stat sur fichier "unFichierQuiNExistePas":No such file or directory
Abandon (core dumped)
Cette macro pallie les inconvénients identifiés précédemment. De plus, le programmeur dispose d’un core
pour une analyse fine de l’erreur.
Notez que, dans le cas d’une erreur de certaines fonctions de la librairie C (malloc par exemple), la
variable errno n’est pas positionnée. Donc, si votre programme peut utiliser la macro assert() (comme
pour les appels système), l’utilisation de la macro ERROR_AT_LINE a moins de sens (puisque la variable errno
n’est pas positionnée). Cela peut amener le programmeur à définir une autre macro qui ne prend pas en
compte le errno.
' $
2 Point de vue système
& %
L’exécution des tâches système s’effectue en général sur le compte des différents
processus hébergés par le système
# 12
Le système ne se déroule pour son propre compte que dans très peu de cas
On distingue deux types d’actions
Le traitement des appels système
La prise en compte des interruptions
& %
' $
2.2 Traitement des appels système
# 13
& %
1. Préparation
– Algorithme de base
∗ Vérifier que la requête est valide
∗ Commuter la pile
∗ Sauter à la fonction rendant le service. Cette fonction récupère les arguments et les met
dans une structure de travail.
– Si la requête ne concerne pas une Entrée/Sortie, le processus déroule le code du noyau sans
blocage et termine l’appel système
– En ce qui concerne les requêtes d’E/S, on distingue :
∗ Les E/S interruptibles (elles peuvent être avortées par un signal)
∗ Les E/S ininterruptibles (elles ne peuvent pas être avortées)
– Pour ces deux types, le processus
∗ Prépare un environnement pour se mettre en attente de la réalisation
∗ Demande au gérant de périphériques de réaliser l’E/S.
∗ S’endort
2. Terminaison
– L’E/S est acquittée par un autre processus qui indique que le processus endormi est désormais
prêt pour exécution
– Le système donne la main au processus prêt pour exécution selon l’algorithme d’ordonnance-
ment
– Un processus réalisant une E/S interruptible peut sortir de l’attente lors de la réception d’un
signal
– Il défait alors sa demande au niveau noyau et retourne en mode utilisateur avec un compte-
rendu d’erreur
3. Retour au mode utilisateur
– Les appels système retournent un code
– En cas d’erreur, la variable errno est modifiée pour expliciter l’erreurg
∗ Les valeurs de errno sont décrites dans le fichier <errno.h>.
∗ Une courte phrase peut être obtenue par la fonction char *strerror (int errnum).
Cette phrase peut être affichée par void perror (const char *s) .
' $
2.3 Prise en compte des interruptions
& %
& %
• La priorité statique,
• La politique d’ordonnancement,
• La priorité d’ordonnancement,
• La priorité dynamique.
NB : Sous Linux, on n’a aucune garantie sur le temps d’exécution d’une tâche (axiome de base du
temps-réel « strict »).
# 16 permet d’affecter à un processus une priorité statique (entre 0 et 99) stipulant la file
d’attente de l’ordonnanceur (scheduler ) où il doit être placé.
Pour décider du processus à exécuter, l’ordonnanceur prend le premier processus
prêt dans la file de plus haute priorité
NB : Valeurs fortes de priorité statique = Fortes priorités
& %
' $
3.2 Politique d’ordonnancement
int sched_setscheduler(pid_t pid, int policy, const struct
sched_param *p);
permet d’affecter à un processus une politique d’ordonnancement
SCHED_FIFO : Le processus n’arrête son exécution que si
I Il fait une E/S
I Il fait un appel à int sched_yield(void) (il laisse alors passer les autres
processus prêts de même priorité)
# 17
I Il change sa politique d’ordonnancement
I Il est préempté par des processus plus prioritaires (il reprend ensuite son
exécution)
SCHED_RR : une condition d’arrêt supplémentaire est définie
I La durée d’exécution atteint un quantum de temps (déterminable via la
fonction int sched_rr_get_interval(pid_t pid, struct timespec
*tp))
SCHED_OTHER : ordonnancement en temps partagé (réservé aux processus de
priorité statique 0)
& %
Le quantum de temps pour un processus SCHED_RR est identique quelle que soit la priorité statique du
processus.
& %
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.
# 19
& %
' $
3.3.2 Inversion de priorité : exemple de solution
# 20
& %
Ce transparent présente un exemple de solution qui ne peut être qu’implémenté manuellement sous
Linux. En effet, Linux ne fournit aucun mécanisme pour détecter et corriger automatiquement le problème
d’inversion de priorité.
Michel Simatic
module CSC4508/M2
Avril 2016
27
Gestion de la mémoire 1 Point de vue système
' $
Plan du document
& %
' $
1 Point de vue système
1.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.2 Besoins des processus. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .5
#3 1.3 Pagination . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.4 Segmentation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
1.5 Pagination versus Segmentation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
1.6 Algorithmes pour la gestion des pages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
& %
& %
À propos du principe d’inclusion, dans une architecture Intel, on a le cache L1 (Level 1) qui est inclus
dans le cache L2 (Level 2), lui-même inclus dans la RAM, elle-même incluse dans le swap (disque).
Voici les temps d’accès typiques à des données situées dans les différents types de mémoire sur une
machine “classique” en 2014 :
• donnée dans la RAM : 100 ns (200 fois plus lent que L1)
• donnée sur un disque SSD : 150 µs (150 000 ns : 300 000 fois plus lent que L1)
• donnée sur un disque dur : 10 ms (10 000 000 ns : 20 millions de fois plus lent que L1)
Le tableau suivant montre les différences de coût entre les types de mémoire (et l’évolution avec les
années) :
Les systèmes multi-utilisateurs et multi-tâches actuels tels Unix, Windows-NT (et ses successeurs
Windows-2000, Windows-XP, etc.). . . ont ces 4 besoins. Comme la gestion mémoire à base de pagination
et de segmentation y répond pleinement, ces systèmes d’exploitation utilisent ce type de gestion mémoire.
C’est pourquoi nous les détaillons dans la suite de ce cours.
Toutefois des systèmes d’exploitation (notamment ceux animant certains super-calculateurs) n’ont pas
certains de ces besoins (par exemple, la protection ou bien l’adressage). Ils utilisent donc une gestion mémoire
plus frustre, mais beaucoup moins gourmande en ressources CPU. Les problèmes spécifiques de ce type de
gestion mémoire (allocation d’une zone libre, libération de zone en évitant de créer une fragmentation. . .)
sont similaires à ceux posés par malloc/free (qui seront étudiés dans la suite de ce cours). Les algorithmes
sont donc très proches.
' $
1.3 Pagination
1.3.1 Généralités . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
#6 1.3.2 Adresse logique . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
1.3.4 Table des pages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
1.3.4 Accélérateurs (caches d’adresses) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
1.4.0 Écroulement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
& %
#7
' $
I Inexistant en mémoire (pages inactives jamais écrites)
I En mémoire secondaire (pages inactives qui ont déjà été écrites)
Le dispositif de pagination
Effectue la correspondance d’adresse
Charge les pages nécessaires (déroutement par défaut de page)
(Éventuellement) décharge des pages actives en mémoire secondaire
Donc, le programme n’est plus présent intégralement en mémoire centrale et il n’y a plus
de continuité physique
#8
& %
Sous Linux, les cadres de page ont une taille de 4 Ko (taille définie par les constantes PAGE_SIZE et
PAGE_SHIFT dans le fichier page.h).
' $
1.3.3 Table des pages
La correspondance entre adresse logique et adresse physique se fait avec une table
des pages contenant
Numéro de cadre de page
Bits d’information (présence, accès, référence, écriture, date de chargement. . .)
# 10
& %
& %
Dans une architecture Intel, on dispose d’un Translation Look-aside Buffer (TLB) muni de 32, 64, voire
256 entrées. NB : on parle également de cache de traduction d’adresse.
Pour anecdote, en décembre 2007, AMD a constaté un bug dans ses processeurs quadri-cœurs Opteron
and Phenom. Ce bug était lié à la TLB et au cache de niveau 3. Un patch a été proposé : il entraînait une
dégradation de performances de 10 à 40% (http://techreport.com/articles.x/13741).
' $
1.3.5 Écroulement
Effet secondaire de la pagination : écroulement (thrashing) dû à un taux de défauts de page trop
important
# 12
Solutions
Augmentation de la capacité mémoire
Ordre des modules utilisés
Prise en compte lors du développement
Régulation de la charge (load levelling) : limitation du nombre de processus
Définition d’un espace vital (working set) nécessaire
Gestion du taux de défauts de page : moduler le nombre de pages allouées
& %
# 13 1.4.2 Généralités . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
1.4.2 Descripteur de segments . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
& %
' $
1.4.1 Généralités
Objectif : refléter en mémoire centrale la structure des programmes (code, données, pile)
Programme = ensemble de segments ayant une taille et des attributs propres (lecture
seule, écriture, partage)
# 15
' $
1.5 Pagination versus Segmentation
& %
Les commandes Unix suivantes donnent des informations sur la pagination et la segmentation des pro-
cessus de la machine :
• Pagination
– La commande ps -u affiche
∗ La taille mémoire virtuelle utilisée par le processus (VSZ)
∗ La taille mémoire résidant en mémoire centrale (RSS)
– La commande top affiche
∗ la mémoire centrale totale/utilisée/disponible
∗ le swap total/utilisé/disponible
– top a un affichage complexe qui peut requérir trop de temps quand le processeur est très utilisé.
Dans ce cas, la commande vmstat -n 1 est préférable : elle affiche également d’autres indications
sur la charge de la machine
• Segmentation
' $
1.6 Algorithmes pour la gestion des pages
# 17 1.6.2 Chargement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
1.6.2 Remplacement (déchargement) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
& %
' $
1.6.1 Chargement
& %
• horloge (clock)
' $
2 Point de vue processus
& %
# 21
& %
' $
2.2 Observation et contrôle de l’utilisation de la mémoire
& %
& %
Noter que la struct rusage renvoyée par getrusage contient d’autres champs, mais ces champs ne
sont pas renseignés par Linux
• time nomExécutable utilise la fonction built-in du shell qui fournit beaucoup moins d’informations
que /usr/bin/time nomExécutable
' $
2.2.2 Observation du type d’accès à la mémoire
& %
Voici un exemple d’utilisation de valgrind sur les programmes gentil et mechant étudiés dans l’exer-
cice 1 (et pour lesquels on constate une différence de temps d’exécution d’un facteur 10 en l’absence de
swap).
Rappelons le contenu de gentil.c :
/************/
/* gentil.c */
/************/
#include <stdlib.h>
#include "constantes.h"
char t[NBRE][PAGE];
int main() {
int i,j,k;
for (i=0 ; i<2 ; i++) {
for (j=0 ; j<NBRE ; j++) {
for (k=0 ; k<PAGE ; k++) {
t[j][k] = 1;
}
}
}
return EXIT_SUCCESS;
}
Et celui de mechant.c :
/*************/
/* mechant.c */
/*************/
#include <stdlib.h>
#include "constantes.h"
char t[NBRE][PAGE];
int main() {
int i,j,k;
for (i=0 ; i<2 ; i++) {
for (k=0 ; k<PAGE ; k++) {
for (j=0 ; j<NBRE ; j++) {
t[j][k] = 1;
}
}
}
return EXIT_SUCCESS;
}
Pour ne pas avoir des temps d’exécution trop longs avec valgrind, le fichier constantes.h définit un
tableau de « seulement » 5 Mo :
/* constantes.h */
#define NBMEG 5
En écriture, on constate qu’avec gentil, les données ne sont pas trouvées dans le cache dans 1, 5% des
cas. Avec mechant, cette valeur monte à 99, 8% des cas, ce qui explique la différence de performances des
deux programmes (sans compter qu’avec gentil, la TLB est mise pleinement à profit).
NB :
• Pour gentil, on fait aussi 2 × N BRE × P AGE = 2 × BY T ES = 10.485.760 écritures sur le tableau t.
Pourtant les D1 misses en écriture sont nettement plus réduits que pour mechant ! L’explication réside
dans l’exécution de l’outil cg_annotate (fourni avec valgrind, cg_annotate –<numéroPID> gentil,
[numéroPID] pouvant être retrouvé avec l’extension du fichier cachegrind.out.[numéroPID] généré
par l’exécution de valgrind --̇tool=cachegrind). Il montre que valgrind considère que le processeur
est muni d’un cache en écriture de 64 octets (cf. 64 B dans la ligne D1 cache). Par conséquent, dans
le cas de gentil qui fait des accès contigus, le processeur n’a besoin d’accéder à la mémoire physique
que tous les 64 octets. Or 10.485.760/64 = 163.840 : on retrouve le nombre de D1 misses en écriture
de gentil.
Par défaut, le compilateur aligne sur des frontières de 4 octets (2 octets pour les
short).
Le compilateur peut donc générer des octets inutilisés au sein d’une structure.
Pour récupérer cet espace perdu
Le mieux est de réordonnancer les feuilles des structures
# 25 On peut aussi compacter toutes les structures d’un source
gcc -fpack-struct
Ou bien compacter juste une structure donnée
typedef struct {
...
} __attribute__((packed)) uneStructure;
Cet alignement peut être insuffisant (besoin d’aligner sur 16 octets, par exemple).
Pour forcer l’alignement :
& %
' $
typVariable nomVariable __attribute__((aligned (nombre)));
Utilisation de posix_memaligned pour les variables de type pointeur
# 26
& %
Par défaut, le compilateur aligne sur des frontières de 4 octets, car certains processeurs ne savent pas
accéder à des entiers qui sont sur une frontière de 4 octets (ou alors ils sont moins performants). C’état notam-
ment le cas des premières générations de processeurs ARM. Aujourd’hui, la plupart des processeurs évolués
(Intel, AMD. . .) n’ont plus ce genre de problème. Pourtant certains compilateurs (dont gcc) continuent à
faire par défaut cet alignement sur des frontières de 4 octets.
Dans le cas où le compactage d’une structure donne lieu à un alignement inefficace (par exemple, int à che-
val sur une frontière de 4 octets), le compilateur ne produit aucun warning du type WARNING : inefficient
alignment.
L’exercice Mémoire/3 illustre la place et les performances perdues si une structure contient des éléments
qui ne sont pas rangés dans le bon ordre.
posix_memaligned correspond à _aligned_malloc sur les compilateurs Microsoft et Intel.
L’exercice Mémoire/8 illustre l’utilisation d’instructions vectorielles SSE des processeurs Intel/AMD qui
imposent des données alignées sur des frontières de 16 octets. Les instructions SSE permettent de faire des
opérations sur 4 flottants (au sens float du langage C) en parallèle, mais requièrent que chaque groupe de
4 flottants soit aligné sur une frontière de 16 octets. Intel propose désormais une extension de SSE appelée
AVX (Advanced Vector Extensions). Elle permet de travailler sur 256 bits au lieu de 128 et n’impose pas
d’alignement sur une frontière d’octets.
La macro offsetof (définie dans stddef.h) permet de connaître la position (offset) d’un champs dans
une structure. Par exemple, le programme suivant affiche la position du champs field_b (ici : 4) par rapport
au début de la structure :
struct my_struct {
int field_a;
int field_b;
};
Cette macro est, par exemple, utilisée dans le noyau Linux pour retrouver l’adresse d’une structure à
partir d’un pointeur sur un de ses champs.
' $
2.2.4 Contrôle des défauts de page
Lutte contre les défauts de page mineurs
Il suffit d’écrire un octet dans la page
Lutte contre les défauts de page majeurs
int mlock(const void *addr, size_t len)
permet de verrouiller en mémoire centrale len octets (à partir de addr) de la
mémoire virtuelle du processus
# 27 int munlock(const void *addr, size_t len)
déverrouille
int mlockall(int flags)
permet de tout verrouiller en mémoire centrale. flags vaut une combinaison de
I MCL_CURRENT : Verrouiller toutes les pages correspondant actuellement à
l’espace d’adressage du processus
I MCL_FUTURE : Verrouiller toutes les pages qui seront dans l’espace d’adressage
int munlockall(void)
déverrouille tout
& %
Sous Linux, la quantité de mémoire physique que l’on peut verrouiller est fixée par RLIMIT_MEMLOCK
La quantité de mémoire physique verrouillable par un processus peut être changée en appelant setrlimit.
Un exemple d’utilisation est disponible dans le fichier exemple_setrlimit.c :
#include <sys/time.h>
#include <sys/resource.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
struct rlimit limits;
if(getrlimit(RLIMIT_MEMLOCK, &limits))
abort();
limits.rlim_cur = limits.rlim_cur / 2;
if(setrlimit(RLIMIT_MEMLOCK, &limits))
abort();
if(getrlimit(RLIMIT_MEMLOCK, &limits))
abort();
return 0;
}
' $
2.3 Allocation/Désallocation dynamique de mémoire
& %
# 30
& %
• Toutes ces fonctions sont des fonctions de la bibliothèque C (qui font dans certains cas des appels
système).
• L’algorithme de malloc(3) est très performant. Il n’est donc pas nécessaire en général de chercher à
l’optimiser.
• Toutefois :
– Lors d’une allocation d’une zone mémoire qui doit être initialisée à 0, on privilégiera calloc(3)
(il est plus efficace qu’un malloc(3) suivi d’un memset(3)).
– Si besoin, on peut affiner les paramètres de fonctionnement de malloc(3) avec mallopt(3).
• Quand on libère une zone avec free, il est vivement conseillé d’affecter NULL au pointeur qu’on avait
sur cette zone. Cela permet un plantage net si, par erreur, dans la suite du programme, on cherche à
nouveau à accéder à cette zone (désormais libérée) à l’aide de ce pointeur.
• [Blaess, 2002] propose d’aller encore plus loin : il suggère de tester systématiquement la valeur d’un
pointeur avant de faire malloc, c’est-à-dire d’avoir systématiquement la séquence : assert(p !=
NULL); p = malloc(taille); ...
Le programme suivant illustre comment comment cette affectation à NULL permet d’avoir un plantage
net et comment le passage sous debugger permet de retrouver rapidement l’origine de l’erreur.
/**********************/
/* ilFautMettreNULL.c */
/**********************/
#include <stdlib.h>
#include <assert.h>
int main(){
char *p = NULL;
f(p);
p = malloc(1);
assert(p != NULL);
f(p);
free(p);
p = NULL;
f(p);
return EXIT_SUCCESS;
}
' $
2.3.3 Digression : algorithmes pour free
Fragmentation : demande refusée car plus de zone de taille suffisante mais somme
des tailles des zones libres supérieure à la taille demandée
Solution : retassement ou compactage : regrouper les zones libres pour en créer une
la plus grande possible
Objectif : reconstruire la liste des zones libres en cherchant à reconstruire le plus
grand trou possible
# 32
Plusieurs cas à envisager
Zone libérée entre 2 zones occupées
Zone libérée entre une zone occupée et une zone libre
Zone libérée entre 2 zones libres
Un algorithme performant au niveau de l’allocation peut s’avérer être plus complexe
pour la libération
Algorithme des frères siamois : la libération d’une zone (un buddy ) est récursive
& %
• L’utilisation d’index permet de traiter une demande d’allocation en une douzaine d’instructions.
& %
' $
2.3.5 Mécanisme d’allocation/désallocation dédié
Principe
Au démarrage du programme, on construit une liste chaînée de blocs de N octets
& %
L’algorithme de malloc étant très efficace, le mécanisme présenté ici n’est en général pas nécessaire.
Il peut toutefois s’imposer dans le cas où l’on doit faire une allocation/désallocation dans un gestionnaire
de signal (dans lequel les opérations de malloc/free sont proscrites).
# 35 void * mmap(void *start, size_t length, int prot, int flags, int
fd, off_t offset)
Permet de mapper un fichier en mémoire. En plus des options qui seront étudiées
dans le chapitre « Entrées/sorties », flags peut prendre la valeur MAP_ANON (ou
MAP_ANONYMOUS) pour indiquer qu’on ne souhaite pas réellement travailler sur un
fichier, mais sur une zone mémoire vierge emplie de zéros
& %
sbrk n’est pas un appel système, juste une fonction de la bibliothèque C qui invoque l’appel système int
brk(void *fin_segment_donnée).
' $
2.4 Déverminage des accès mémoire
# 36 2.4.1 Détecter statiquement les accès erronés à des zones allouées dynamiquement . . 37
2.4.2 Détecter dynamiquement les accès erronés à des zones allouées dynamiquement38
2.4.3 Visualiser quand une zone mémoire est accédée/modifée . . . . . . . . . . . . . . . . . . . . . . 39
& %
& %
Pour illustrer la puissance de splint (et valgrind au slide suivant), étudiez le programme C suivant
(qui s’exécute correctement). Il contient 3 erreurs. Les voyez-vous ?
/********************/
/* jeuDes3Erreurs.c */
/********************/
#define CITATION "Faites que le rêve dévore votre vie, afin que la vie ne dévore pas votre rêve." // Antoine de Saint-Exupery
int main(){
char *p = NULL;
/* On regarde l’affichage */
printf("Contenu pointe par p apres strcpy : \"%s\"\n", p);
// A T T E N T I O N
// S’il avait ecoute les conseils de ce poly, le programmeur de ce
// code source aurait mis la ligne
// p = NULL;
// apres ce free. Mais certaines personnes ne peuvent s’empecher de
// ne pas ecouter ce qu’on leur dit... A moins que cet oubli ne soit
// volontaire...
// A T T E N T I O N
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
Noter le premier warning (peu explicite :-()˙ qui correspond au fait qu’on a écrit p[1] = ’\0’; dans le source
(si on met p[0], l’erreur disparaît).
' $
2.4.2 Détecter dynamiquement les accès erronés à des zones
allouées dynamiquement
# 38 Outils
insure++ (société Paradox ) : au moment de la compilation, cet outil ajoute aux sources
(.c) des instructions de contrôle au niveau des différentes instructions d’affectation
Rational Purify (société IBM) : au moment du link, cet outil ajoute aux objets (.o)
des instructions de contrôle au niveau des différentes instructions (assembleur)
d’affectation
valgrind (logiciel libre, http://valgrind.org) : l’exécutable est lu par un simulateur
de processeur x86 qui se charge de détecter toutes les anomalies d’accès
Limite = On n’est jamais sûr d’être passé par toutes les branches
& %
NB :
1. Dans la pratique, il est recommandé d’utiliser splint (avant de se lancer dans les tests unitaires et les
tests d’intégration), puis d’utiliser insure++, rational Purify ou valgrind.
4. Linux propose d’autres outils nettement moins puissants qu’insure++, rational Purify et valgrind,
mais envisageables dans des cas extrêmes où les outils présentés jusqu’à présent ne seraient pas utili-
sables (contrainte de temps d’exécution, par exemple) :
• mpatrol utilise une version spéciale de malloc et free (ainsi que d’autres fonctions travaillant
sur la mémoire, comme memset par exemple) de manière à repérer des débordements de zone.
• mtrace est une extension GNU (man 3 mtrace) qui permet un suivi intégré des alloca-
tions/désallocations
On peut ainsi repérer des fuites mémoire
• On peut citer enfin electric fence qui est complètement obsolète par rapport aux outils précé-
dents.
5. Pour information :
Pour illustrer la puissance de ces outils, reprenons le programme C (qui s’exécute correctement) présenté
en commentaire du transparent précédent . Il contient 3 erreurs. Les voyez-vous ?
L’exécution de valgrind (valgrind --̇tool=memcheck --̇leak-check=yes jeuDes3Erreurs) sur un
exécutable compilé avec l’option -g donne l’affichage suivant (qui devrait contribuer à la localisation de
ces 3 erreurs) :
==2402== Memcheck, a memory error detector for x86-linux.
==2402== Copyright (C) 2002-2005, and GNU GPL’d, by Julian Seward et al.
==2402== Using valgrind-2.4.0, a program supervision framework for x86-linux.
==2402== Copyright (C) 2000-2005, and GNU GPL’d, by Julian Seward et al.
==2402== For more details, rerun with: -v
==2402==
==2402== Conditional jump or move depends on uninitialised value(s)
==2402== at 0xB2A4CF: vfprintf (in /lib/libc-2.3.5.so)
==2402== by 0xB2F942: printf (in /lib/libc-2.3.5.so)
==2402== by 0x80484DC: main (jeuDes3Erreurs.c:25)
==2402==
==2402== Invalid write of size 1
==2402== at 0x1B90A6BA: memcpy (mac_replace_strmem.c:285)
==2402== by 0x80484F9: main (jeuDes3Erreurs.c:28)
==2402== Address 0x1B92B073 is 0 bytes after a block of size 75 alloc’d
==2402== at 0x1B909222: malloc (vg_replace_malloc.c:130)
==2402== by 0x8048499: main (jeuDes3Erreurs.c:16)
==2402==
==2402== Invalid read of size 1
==2402== at 0xB2A4CF: vfprintf (in /lib/libc-2.3.5.so)
==2402== by 0xB2F942: printf (in /lib/libc-2.3.5.so)
==2402== by 0x804850C: main (jeuDes3Erreurs.c:31)
==2402== Address 0x1B92B073 is 0 bytes after a block of size 75 alloc’d
==2402== at 0x1B909222: malloc (vg_replace_malloc.c:130)
==2402== by 0x8048499: main (jeuDes3Erreurs.c:16)
==2402==
==2402== Invalid read of size 1
==2402== at 0xB2A4CF: vfprintf (in /lib/libc-2.3.5.so)
==2402== by 0xB2F942: printf (in /lib/libc-2.3.5.so)
==2402== by 0x804852D: main (jeuDes3Erreurs.c:37)
==2402== Address 0x1B92B028 is 0 bytes inside a block of size 75 free’d
==2402== at 0x1B909743: free (vg_replace_malloc.c:152)
==2402== by 0x804851A: main (jeuDes3Erreurs.c:34)
==2402==
==2402== Invalid read of size 1
==2402== at 0xB4B94D: _IO_file_xsputn@@GLIBC_2.1 (in /lib/libc-2.3.5.so)
==2402== by 0xB2A140: vfprintf (in /lib/libc-2.3.5.so)
==2402== by 0xB2F942: printf (in /lib/libc-2.3.5.so)
==2402== by 0x804852D: main (jeuDes3Erreurs.c:37)
==2402== Address 0x1B92B072 is 74 bytes inside a block of size 75 free’d
==2402== at 0x1B909743: free (vg_replace_malloc.c:152)
==2402== by 0x804851A: main (jeuDes3Erreurs.c:34)
==2402==
==2402== Invalid read of size 1
==2402== at 0xB4B95F: _IO_file_xsputn@@GLIBC_2.1 (in /lib/libc-2.3.5.so)
==2402== by 0xB2A140: vfprintf (in /lib/libc-2.3.5.so)
==2402== by 0xB2F942: printf (in /lib/libc-2.3.5.so)
==2402== by 0x804852D: main (jeuDes3Erreurs.c:37)
==2402== Address 0x1B92B071 is 73 bytes inside a block of size 75 free’d
==2402== at 0x1B909743: free (vg_replace_malloc.c:152)
==2402== by 0x804851A: main (jeuDes3Erreurs.c:34)
==2402==
==2402== Invalid read of size 1
==2402== at 0xB57465: mempcpy (in /lib/libc-2.3.5.so)
==2402== by 0xB2A140: vfprintf (in /lib/libc-2.3.5.so)
==2402== by 0xB2F942: printf (in /lib/libc-2.3.5.so)
==2402== by 0x804852D: main (jeuDes3Erreurs.c:37)
NB : si vous faites une édition de lien statique de jeuDes3Erreurs, puis que vous exécutez valgrind
--̇tool=memcheck --̇leak-check=yes jeuDes3Erreurs, vous constaterez que valgrind détecte des erreurs
avant le main de votre programme. La version statique de la libc comtient donc des problèmes d’accès
mémoire.
& %
mprotect(2) impose la contrainte suivante : l’adresse de début de zone doit être alignée sur une frontière
de page.
Bibliographie du chapitre
[Blaess, 2002] Blaess, C. (2002). Programmation système en C sous Linux : signaux, processus, threads, IPC
et sockets. Eyrolles, Paris, Paris, France.
Michel Simatic
module CSC4508/M2
Avril 2016
55
Les fichiers (et les entrées-sorties)
' $
Plan du document
& %
Lors de ce cours, on parlera surtout de fichiers, car c’est l’exemple d’entrées-sorties le plus facilement
manipulable. Toutefois, bien noter que le contenu des 3 premières sections s’applique aux entrées-sorties
autres que les fichiers.
Rappel sur les fichiers (notions vues en première année) :
• Un fichier est une suite d’octets contigus stockés dans un support (par exemple, un disque) sous un
nom (le « nom du fichier »).
– Texte : contenant des octets affichables à l’écran. Ce type de fichiers est constitué de lignes
identifiées par le caractère de fin de ligne (sous Unix, caractère de code ASCII 10 alors que sous
Windows, caractère de code ASCII 10 suivi d’un caractère de code ASCII 13)
– Binaire : contenant des octets non affichables à l’écran.
Sous Unix, les commandes hexdump -C nomFichier et bless nomFichier permettent de visualiser le
contenu d’un fichier de manière précise. Utilisez-les pour comparer le contenu de helloWorldUnix.c
et helloWorldWindows.c.
• Quand on « ouvre » un fichier, le système d’exploitation fournit une notion de position courante
(appelée parfois offset dans la suite de ce cours) de lecture/écriture.
– Cette position courante détermine le rang de l’octet dans le fichier à partir duquel les primitives
système liront ou bien écriront des octets dans le fichier.
– Cet offset avance à chaque fois qu’on effectue une lecture ou une écriture.
– Le système d’exploitation met à disposition de l’utilisateur des primitives pour changer explicite-
ment cette position courante (sans lire ou écrire des octets).
• La « fin d’un fichier » correspond à l’endroit situé derrière le dernier octet du fichier. Quand on est
à la fin du fichier, on ne peut pas lire d’octets. En revanche, on peut écrire des octets (selon le mode
dans lequel on a ouvert le fichier).
– 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
séquentiel indexé, il faut s’appuyer sur une librairie (Unix NDBM, GDBM, Oracle Berkeley DB. . .).
' $
1 Primitives Unix d’entrées-sorties
& %
' $
1.1 Primitives de base
& %
• 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)
– Des données qu’on croyait avoir écrites sur disque peuvent être perdues car elles étaient en fait
en mémoire
– Il y aussi risque d’incohérence sur les données qu’on trouve dans le fichier
– Synchronisation implicite (i.e. à chaque écriture) : ajout de l’option O_SYNC au moment du open
ssize_t read(int fd, void *buf, size_t count) : retour = nombre d’octets
lus
Au retour de read, la zone buf est modifiée avec le résultat de la lecture
Dans le cas d’un fichier, le nombre d’octets lus peut ne pas être égal à count :
#6 I On se trouve à la fin du fichier
I On a fait une lecture non bloquante et les données étaient verrouillées de
manière exclusive
& %
Dans le cas où la fonction read est utilisée sur un descripteur autre qu’un fichier (tube, socket), le fait
que le nombre d’octets lus peut ne pas être égal à count peut avoir d’autres significations :
• Pour un tube de communication (cf. chapitre Communications Inter-processus), le correspondant a
fermé son extrémité du tube.
• Pour une socket (cf. cours CSC4509), le protocole réseau utilise des paquets de données de taille
inférieure à celle qui est réclamée.
' $
1.1.3 Écriture sur descripteur
ssize_t write(int fd, const void *buf, size_t count) : retour = nombre
d’octets écrits
Dans le cas d’un fichier, le retour (sans erreur) de l’écriture signifie que :
I Les octets ont été écrits dans les caches du noyau si pas de O_SYNC au
moment du open
I Les octets ont été écrits sur disque si O_SYNC au moment du open.
#7
Dans le cas d’un fichier, si le nombre d’octets écrits est différent de count, cela
signifie une erreur (disque plein, par exemple)
ssize_t writev(int fd, const struct iovec *iov, int iovcnt)
Au retour de writev, les zones référencées par les iovcnt éléments du tableau
iov ont été écrites sur le descripteur fd
int posix_fallocate(int fd, off_t offset, off_t len)
Garantit que l’espace disque requis pour le fichier est effectivement alloué
& %
L’écriture sur disque est atomique : si deux processus P1 et P2 écrivent simultanément dans le même
fichier au même endroit, lorsque les deux processus auront fini leur écriture, on trouvera à cet endroit :
• Soit l’écriture faite par P1 ,
En effet, dans ce dernier cas, l’une des écritures risque d’être écrasée par l’autre.
Le fichier copier.c de la page suivante illustre l’utilisation de open, read, write et close.
/************/
/* copier.c */
/************/
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <stdio.h>
Cette opération de recopie du contenu d’un fichier vers un autre descripteur est une opération fréquem-
ment réalisée dans les serveurs Web. En effet, ces serveurs doivent notamment envoyer le contenu de fichiers
vers des navigateurs qui leur en ont fait la demande. C’est pourquoi le système Linux offre la primitive
sendfile (ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count)). Elle permet
de lire count octets de in_fd pour les écrire sur out_fd (qui doit correspondre à une socket). sendfile est
plus performant que la combinaison read/write.
La fonction fallocate est la version spécifique Linux de la fonction (portable) posix_fallocate.
' $
1.2 Duplication de descripteur
Mécanisme principalement utilisé pour réaliser des redirections des trois fichiers
d’entrée-sorties standard.
int dup(int fdACloner) : retour = nouveauFd
#8 associe le plus petit descripteur disponible du processus appelant à la même entrée
dans la table des fichiers ouverts que le descripteur fdACloner
int dup2(int fdACloner, int fdClone)
force le descripteur fdClone à devenir un synonyme du descripteur fdACloner. Si
le descripteur fdClone n’est pas disponible, le système réalise au préalable une
opération de fermeture close(fdClone)
& %
' $
1.3 Contrôle des entrée-sorties
& %
int fcntl(int fd, int cmd, ...) : retour fonction du type d’opération
permet la réalisation d’un certain nombre d’opérations à différents niveaux dans les
tables du système
Les commandes possibles sont :
# 10
F_GETFL : Valeur des attributs utilisés au moment de l’ouverture du fichier
F_SETFL : Positionnement de certains de ces attributs (NB : ces attributs sont
communs à tous les processus manipulant le fichier concerné)
On peut ainsi positionner O_ASYNC ou O_NONBLOCK après création du descripteur
de fichier.
Opérations liées au verrouillage
& %
' $
1.3.2 Verrouillage de fichier
Les verrous sont attachés à un i-nœud. Donc, l’effet d’un verrou sur un fichier est
visible au travers de tous les descripteurs (et donc tous les fichiers ouverts)
correspondant à ce nœud
Un verrou est la propriété d’un processus : ce processus est le seul habilité à le
# 11
modifier ou l’enlever
Les verrous ont une portée de [entier1 : entier2] ou [entier : ∞]
Les verrous ont un type :
partagé (shared)
exclusif (exclusive)
& %
fd = open("/tmp/ficTest",O_RDWR|O_CREAT, S_IRWXU|S_IRWXG|S_IRWXO);
if (fd < 0) {
perror("open");
exit(EXIT_FAILURE);
}
/* Liberation du verrou */
printf("Relachement verrou par processus %d...\n", getpid());
verrou.l_type = F_UNLCK;
verrou.l_whence = SEEK_SET;
verrou.l_start = 15;
verrou.l_len = 1;
if (fcntl(fd, F_SETLK, &verrou) < 0){
perror("Relachement verrou");
exit(EXIT_FAILURE);
}
printf("...OK\n");
return EXIT_SUCCESS;
}
fd = open("/tmp/ficTest",O_RDWR|O_CREAT, S_IRWXU|S_IRWXG|S_IRWXO);
if (fd < 0) {
perror("open");
exit(EXIT_FAILURE);
}
/* Liberation du verrou */
printf("Relachement verrou par processus %d...\n", getpid());
verrou.l_type = F_UNLCK;
verrou.l_whence = SEEK_SET;
verrou.l_start = 15;
verrou.l_len = 1;
if (fcntl(fd, F_SETLK, &verrou) < 0){
perror("Relachement verrou");
exit(EXIT_FAILURE);
}
printf("...OK\n");
return EXIT_SUCCESS;
}
• Si on exécute vExcl en premier, un autre vExcl ou un vPart doivent attendre avant de pouvoir poser
leur verrou.
• Si on exécute vPart en premier, un autre vPart peut poser le verrou (partagé). En revanche, un vExcl
doit attendre avant de pouvoir poser son verrou.
Pour empêcher cette famine, il faut ajouter une exclusion mutuelle (cf. chapitre « Synchronisation de
processus »).
' $
1.3.3 Conseil au noyau pour les lectures
& %
Depuis janvier 2011, on sait que cette fonction est utilisée dans les prochaines versions de Fi-
refox pour gagner 40% à 50% de temps au moment du démarrage en chargeant plus efficace-
ment les deux librairies de l’interface graphique, xul.dll et mozjs.dll (plus d’informations en
https://bugzilla.mozilla.org/show_bug.cgi?id=627591).
& %
lseek peut être utilisé pour faire un fichier d’index permettant de retrouver plus rapidement des infor-
mations dans un fichier textuel (cf. exercice Entrées-sorties/5). Toutefois, avant d’engager un développement
spécifique et coûteux basé sur lseek, toujours vérifier que la fonctionnalité recherchée n’existe pas déjà dans
une librairie (Open Source) comme Unix NDBM, GDBM ou Oracle Berkeley DB (voir commentaire de la
section 2.6).
' $
1.5 Gestion de la lenteur ou du blocage des entrées-sorties*
La réception d’un signal par un processus arrête toute entrée-sortie en cours dans ce
processus (errno est positionné à EINTR).
Dans le cas où le descripteur est marqué avec l’option O_NONBLOCK : lorsque le
programme invoque une primitive d’entrée-sortie dont le système détecte qu’elle se
bloquerait, la primitive renvoie immédiatement une erreur et positionne errno à
EAGAIN ou EWOULDBLOCK (selon que fichier ou socket).
# 14 Le programme peut s’appuyer sur les primitives d’entrées-sorties asynchrones
aio_read et aio_write. Chacune déclenche l’exécution d’une entrée-sortie de
manière asynchrone :
Elle ne bloque pas l’exécution du programme ;
Quand son exécution est terminée (qu’elle se soit bien passée ou qu’il y ait eu
une erreur), un signal est envoyé au programme qui, dans le gestionnaire de
signal, peut récupérer avec aio_return et aio_error le résultat de l’exécution
de l’entrée-sortie asynchrone.
Les entrées-sorties asynchrones sont déconseillées sous Linux.
& %
NB : L’« * » dans son titre signale que ce transparent est un transparent d’approfondissement du cours
CSC4508.
• Vu que la réception d’un signal par un processus arrête toute entrée-sortie en cours, on invoque souvent
la primitive setitimer avant d’invoquer une opération d’entrée-sortie. Ainsi le système envoie un
signal (en général SIGALRM) et arrête donc l’entrée-sortie si cette dernière est restée bloquée plus d’un
certain temps. Noter que, si on utilise des signaux dans le programme, il faut prévoir la mise en
place d’un gestionnaire de signal (avec sigaction) de sorte que le signal soit géré par le programme
(éventuellement en ne faisant rien). Ainsi le programme ne s’arrête pas parce que le signal n’est pas
géré par le programme.
while (1) {
rc = read( tabFd[i], buf, sizeof(buf));
if (rc >= 0) {
// Faire le traitement associé a la lecture correcte
} else if (errno != EAGAIN) {
// L’erreur ne vient pas du fait que le descripteur est un fichier
// ouvert en non-bloquant et que le read se bloquerait.
// Il faut donc s’arreter.
error_at_line(EXIT_FAILURE, errno, __FILE__, __LINE__, "Pb descripteur");
}
i = (i+1) % NB_ELEMENT_DANS_TABFD;
}
Dans la pratique, cette manière de procéder est peu performante. En effet, on doit systématiquement
appeler read sur tous les descripteurs de fichier, alors que peut-être le read ne donnera un résultat
probant que sur l’un des descripteurs.
C’est pourquoi les différents systèmes d’exploitation proposent une méthode pour déterminer en un seul
appel système le (ou les) descripteur(s) sur lequel(/lesquels) il y a eu un événement. Ainsi, sous Linux,
on utilise en général la primitive select. Lors du cours « Éléments d’architecture client-serveur », nous
utilisons la librairie libevent (http://monkey.org/∼provos/libevent/) qui permet de garantir la
portabilité de la gestion des événements.
• Les entrées-sorties asynchrones (aio_read, aio_write. . .) ne sont pas implémentées dans le noyau
Linux. Toutefois, elles sont émulées dans la librairie gcc : cette dernière se charge de démarrer un
thread chargé de faire la lecture ou l’écriture de manière asynchrone. Cette manière de procéder est
très peu performante. L’utilisation des entrées-sorties asynchrones est donc déconseillée sous Linux.
2.2 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
2.2 Ouverture/fermeture de fichier . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
# 15 2.3 Lecture de fichier. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .19
2.4 Écriture de fichier . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
2.6 Contrôle du tampon . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
2.6 Divers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
& %
' $
2.1 Introduction
L’ensemble des fonctions vues jusqu’à présent sont des appels système spécifiques
Unix : leur utilisation risque de compromettre la portabilité d’une application
# 16 Le langage C définit des mécanismes standard d’entrées-sorties. Leur utilisation
garantit donc la portabilité d’une application
Néanmoins l’implémentation de ces mécanismes standards est une surcouche au
dessus des primitives systèmes : ces mécanismes standard ont une pénalité en terme
de performances
& %
Ouverture :
FILE *fopen (const char *path, const char *mode)
provoque l’ouverture d’un flux vers le fichier path
FILE *fdopen (int fd, const char *mode)
permet d’associer un flux au descripteur de fichier fd obtenu, par exemple, à
# 17 l’aide de open
FILE *freopen (const char *path, const char *mode, FILE *stream)
Fonctionnalité similaire à dup2
FILE *tmpfile (void)
crée un fichier temporaire
Fermeture :
int fclose (FILE *stream)
& %
Un flux est une structure de données qui contient notamment un descripteur de fichier, mais aussi des
pointeurs sur des tampons utilisés en lecture et en écriture.
fopen crée des fichiers avec des droits combinés avec le umask. Pour d’autres droits, faire un open du
fichier avec les droits qui vous intéressent, puis utiliser fdopen pour obtenir un flux.
' $
2.3 Lecture de fichier
int fgetc (FILE *stream)
lit un caractère
équivalent de fgetc, mais implémenté sous forme de macro (→ a priori, plus efficace)
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)
& %
Toujours tester la valeur renvoyée par fscanf et consœurs. Cela évitera des surprises.
La fonction sscanf permet de faire une lecture formatée d’une zone mémoire.
' $
2.4 Écriture de fichier
int fputc (int c, FILE *stream)
écrit un caractère
int putc (int c, FILE *stream)
équivalent de fputc, mais implémenté sous forme de macro (→ a priori, plus efficace)
int putchar (int c)
# 21
& %
Comme fscanf, fprintf et consœurs renvoient un nombre. Toutefois, au contraire de fscanf, cette
valeur de retour n’est en général pas utile (car le compilateur fait des vérifications permettant de contrôler
que tous les paramètres ont bien été utilisés). Noter que splint recommande de caster le retour de fprintf
et consœurs en (void).
La fonction sprintf permet de faire une écriture formatée dans une zone mémoire.
Le fichier copier2.c de la page suivante illustre les primitives de la bibliothèque C d’entrées-sorties.
/*************/
/* copier2.c */
/*************/
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
return EXIT_FAILURE;
}
if (fclose(source) < 0) {
perror(argv[1]);
return EXIT_FAILURE;
}
if (fclose(dest) < 0) {
perror(argv[2]);
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
' $
2.5 Contrôle du tampon
int setvbuf (FILE *stream, char *buf, int buf, size_t size)
contrôle le tampon utilisé par la librairie standard pour cacher certaines opérations
vis-à-vis du système de fichier
# 22
int fflush (FILE *flux)
vide le tampon d’un flux
NB : ceci ne force pas l’écriture physique des données qui n’est assurée que par
O_SYNC ou fsync
& %
& %
Il existe aussi des fonctions de « bases de données », plus précisément des accès aux fichiers plus évolués
que l’accès séquentiel ou direct : fichiers séquentiels indexés (avec des clés d’accès), arbres binaires. . .
• Package Unix NDBM : fonction dbm_open. . . (utilisé pour services réseau NIS, par exemple),
• Package Oracle Berkeley DB : fonction dbopen. . . (fichiers accessible à partir du langage C, Java,
Perl. . .).
void * mmap(void *start, size_t length, int prot, int flags, int
fd, off_t offset)
permet d’établir une projection en mémoire d’un fichier (ou d’un périphérique) de
# 24 manière à accéder au contenu du fichier avec des instructions de manipulation
mémoire
int munmap(void *start, size_t length)
supprime une projection
ATTENTION : mmap ne permet pas d’ajouter des octets à un fichier
& %
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/mman.h>
fd = open(argv[1], O_RDONLY);
if (fd < 0) {
perror("open fic");
return EXIT_FAILURE;
}
i = atoi(argv[2]);
/* mmap */
tab = (char *)mmap(0,
i+7,
PROT_READ,
MAP_PRIVATE,
fd,
0);
if (tab == MAP_FAILED){
perror("mmap");
return EXIT_FAILURE;
}
/* Affichage */
for (j=0 ; j<8 ; j++){
putc(tab[i+j], stdout);
}
puts("");
/* On ferme tout */
rc = munmap(tab,i+7);
if (rc < 0){
perror("munmap");
return EXIT_FAILURE;
}
if (close(fd) < 0){
perror("close fic");
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
Vu que mmap ne permet pas d’ajouter des octets à un fichier, il faut veiller à ce que le fichier ait la taille
maximum souhaitée (par exemple, avec posix_fallocate) avant d’utiliser mmap sur ce fichier.
' $
4 Manipulation des i-noeuds du système de fichiers Unix
Le système de gestion de fichiers Unix est basé sur la notion d’i-nœuds
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
fd = open(argv[1], O_RDONLY);
if (fd < 0) {
perror("open fic");
return EXIT_FAILURE;
}
if (fstat(fd,&sts) < 0) {
perror("fstat");
return EXIT_FAILURE;
}
printf("Fichier %s a %ld octets\n", argv[1], sts.st_size);
if (close(fd) < 0){
perror("close fic");
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
' $
Manipulation des i-noeuds du système de fichiers Unix (suite)
int access(const char *pathname, int mode)
teste les droits d’accès d’un processus vis-à-vis du fichier pathname
int unlink(const char *pathname)
permet de détruire un nom dans le système de fichiers
int rename(const char *OLD, const char *NEW)
# 26
permet de renommer un fichier
& %
Consultation :
DIR *opendir (const char *path) : retour = pointeur sur flux répertoire
ouvre le répertoire path en « lecture »
struct dirent *readdir (DIR *dir) : retour = une entrée de répertoire ou NULL
lit la première entrée (ou la suivante si un appel à readdir a déjà été fait) du répertoire
dir
# 27
void rewinddir (DIR *dir)
int closedir (DIR *dir)
Mise-à-jour :
int mkdir(const char *path, mode_t mode)
int rmdir(const char *path)
Rappel : int unlink(const char *path)
permet de supprimer l’entrée path (et éventuellement de détruire l’i-nœud associé dans
le système de fichier, si cette entrée était la seule pointant sur l’i-œud)
& %
Il existe aussi des fonctions permettant la manipulation de liens symboliques. Elles ne sont pas décrites
ici, car pour l’instant la norme POSIX n’a pas repris ces mécanismes. A titre indicatif, voici les fonctions
disponible dans Linux :
• int symlink(const char *oldpath, const char *newpath)
crée un lien symbolique
• int lstat(const char *path, struct stat *buf)
fournit des informations sur un lien symbolique
• int readlink(const char *path, char *buf, size_t bufsiz)
lit le contenu d’un lien symbolique
' $
6 Limitations de NFS
NFS (Network File System) est un protocole permettant un accès transparent pour
les utilisateurs à des fichiers présents sur des machines distantes
Ses limitations sont les suivantes :
Le serveur NFS est sans état et les verrous ne sont pas supportés pour les
fichiers distants
Problème de cohérence : NFS gère un ensemble de caches pour les fichiers et
# 28 leurs attributs. Ces caches ont une durée de validité de 3 secondes pour les
fichiers et 30 secondes pour les répertoires et il existe des phases d’incohérence
entre les différents clients
Problème de sécurité : l’accès aux fichiers distants est contrôlé au travers de
l’identification (numérique) de l’utilisateur demandeur. Le bon fonctionnement
d’ensemble du mécanisme suppose donc qu’une identification numérique donnée
corresponde à la même personne physique sur le système de serveur de fichiers et
sur les systèmes clients qui peuvent le solliciter via NFS. Solution classique :
utiliser un NIS (Network Information Service)
& %
module CSC4508/M2
Avril 2016
77
Communications inter-processus 1 Gestion des processus
' $
Plan du document
& %
' $
1 Gestion des processus
& %
TELECOM SudParis — Denis Conan, Michel Simatic et François Trahay — Avril 2016 — module CSC4508/M2 78
Communications inter-processus 1 Gestion des processus
' $
1.1 Environnement des processus
& %
/********************/
/* exemple_getenv.c */
/********************/
#include <stdio.h>
#include <stdlib.h>
extern char **environ;
int main(int argc, char **argv, char **envp) {
char *p_shell;
char **ancien_environ = environ;
/* parcours de l’environnement */
printf("---> envp :\n");
while(*envp != NULL) {
printf("%s\n", *envp);
envp++;
}
printf("---> environ :\n");
while(*environ != NULL) {
printf("%s\n", *environ);
environ++;
}
/* affichage de la variable SHELL si elle existe */
printf("---> recherche de la variable SHELL :\n");
if ((p_shell = getenv("SHELL")) != NULL) {
printf("SHELL : %s\n", p_shell);
} else {
printf("variable SHELL non définie\n");
}
environ = ancien_environ;
printf("---> recherche de la variable SHELL (environ réinitialisée):\n");
if ((p_shell = getenv("SHELL")) != NULL) {
printf("SHELL : %s\n", p_shell);
} else {
printf("variable SHELL non définie\n");
}
return EXIT_SUCCESS;
}
TELECOM SudParis — Denis Conan, Michel Simatic et François Trahay — Avril 2016 — module CSC4508/M2 79
Communications inter-processus 1 Gestion des processus
' $
1.2 Informations concernant un processus
Types d’informations :
pid_t PID : numéro du processus, unique et non modifiable
pid_t PPID : numéro du processus parent, unique et non modifiable
gid_t PGID : numéro du processus chef d’un groupea , unique et modifiable
uid_t UID : numéro de l’utilisateur réel, unique et non modifiable
gid_t GID : numéro du groupe de l’utilisateur réel, unique et non modifiable
#5 gid_t EUID : numéro de l’utilisateur effectif, unique et modifiable
gid_t EGID : numéro du groupe de l’utilisateur effectif, unique et modifiable
Primitives correspondantes :
pid_t getpid(), pid_t getppid(), int getpgid(), int setpgid(pid_t
pid, pid_t pgid)
uid_t getuid(), uid_t geteuid(), gid_t getgid(), gid_t getegid(),
int setuid(uid_t id),int setgid(gid_t id)
a. Attention ! un groupe de processus repère un ensemble de processus dans une même session,
ce n’est pas lié au groupe d’utilisateur.
& %
Dans un système de type UNIX, il n’est pas possible de construire un processus ; en revanche, les processus
peuvent être dupliqués à l’identique (à charge ensuite au programmeur de changer l’image du processus s’il
veut exécuter un programme différent). Ainsi, tout processus (à l’exception du processus initial — noté 0)
possède un parent. Ils sont donc logiquement organisés sous la forme d’une arborescence. Depuis la version
2.4 du noyau GNU/Linux, le nombre de processus n’est plus limité à la valeur NT_TASKS (fixée à 512) mais
dépend de la taille mémoire de la machine. Ainsi, pour une machine à 8 Go, le nombre permis est 62993 :
lisible en regardant dans /proc/sys/kernel/threads-max.
La sémantique associée à chacune de ces primitives système est la suivante : getpid et getppid re-
tournent respectivement le numéro du processus courant et le numéro du parent du processus courant ;
getuid et getgid retournent respectivement l’UID réel et le GID réel associés au processus courant. Les
valeurs du propriétaire et du groupe effectifs déterminent les droits du processus lors des accès aux objets du
système (fichiers et processus). Ces valeurs sont modifiées lorsqu’un processus correspond à l’exécution d’un
programme binaire pour lequel le bit setuid (lettre s en lieu et place du droit x) est positionné. Le proprié-
taire réel reste l’identifiant de l’utilisateur ayant lancé la commande et le propriétaire effectif devient alors
l’identifiant du propriétaire du fichier exécuté. C’est ainsi qu’il est possible d’exécuter des commandes comme
passwd demandant les privilèges du super-utilisateur. Seul les processus de propriétaire le super-utilisateur
(0) peuvent changer d’identifiant avec les primitives setuid et setgid.
Le rôle des groupes de processus permet de gérer ensemble, par exemple pour l’envoi de signaux, les
processus créés par une même application importante. Par exemple, pour supprimer tous les processus de
cette application, il suffira d’envoyer un signal au groupe. Prenons le cas de la commande cat | lp -P hp2
qui connecte l’entrée standard vers l’imprimante hp2. Pour sortir de l’entrée clavier, il suffit de taper un
CTL-D qui envoie un signal d’arrêt aux deux processus mais pas au shell qui a lancé ces deux processus.
Le signal est donc envoyé au groupe de processus qui englobe ces deux processus. Voici un exemple de
manipulation des informations relatives à un processus (fichier exemple_getpid.c) :
/********************/
/* exemple_getpid.c */
/********************/
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main() {
printf("pid = %d\n", getpid());
printf("ppid = %d\n", getppid());
printf("pgrp = %d\n", getpgrp());
printf("création d’un groupe = %d\n", setpgrp());
printf("pid = %d\n", getpid());
printf("ppid = %d\n", getppid());
printf("pgrp = %d\n\n", getpgrp());
TELECOM SudParis — Denis Conan, Michel Simatic et François Trahay — Avril 2016 — module CSC4508/M2 80
Communications inter-processus 1 Gestion des processus
return EXIT_SUCCESS;
}
' $
1.3 Création de processus
& %
La primitive fork permet de dupliquer un processus à l’identique. Aussi, afin de pouvoir discriminer
entre le parent (le processus initial) et l’enfant (la copie), la valeur de retour de cette primitive est différente
selon que l’on est dans l’un ou l’autre processus : dans le parent, la valeur retournée est le numéro du PID
de l’enfant ; dans l’enfant, la valeur retournée est 0 ; en cas d’erreur, comme pour la plupart des primitives
systèmes, la valeur retournée par l’appel à fork dans le parent est -1. vfork est une version allégée de fork
ne devant être utilisée que si elle est suivie d’un exec immédiatement après. Voici un exemple de duplication
de processus (fichier exemple_fork.c) :
/******************/
/* exemple_fork.c */
/******************/
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main() {
pid_t pid;
printf("démonstration de fork\n");
pid = fork();
switch (pid) {
case -1 :
fprintf(stderr, "echec de fork\n");
return EXIT_FAILURE;
case 0 : /* enfant */
sleep(2);
printf("je suis l’enfant\n");
printf("pid = %d\n", getpid());
printf("ppid = %d\n", getppid());
printf("pgrp = %d\n", getpgrp());
printf("uid = %d\n", getuid());
printf("euid = %d\n", geteuid());
printf("gid = %d\n", getgid());
break;
TELECOM SudParis — Denis Conan, Michel Simatic et François Trahay — Avril 2016 — module CSC4508/M2 81
Communications inter-processus 1 Gestion des processus
default : /* parent */
printf("je suis le parent\n");
printf("pid = %d\n", getpid());
printf("ppid = %d\n", getppid());
printf("pgrp = %d\n", getpgrp());
printf("uid = %d\n", getuid());
printf("euid = %d\n", geteuid());
printf("gid = %d\n", getgid());
printf("pid de l’enfant= %d\n", pid);
break;
}
return EXIT_SUCCESS;
}
' $
1.4 Terminaison de processus
Terminaison :
Code retour du processus
Libération des segments de mémoire
Entrée dans la table des processus (cf. /proc) :
I Gardée : stockage code retour, temps CPU. . .
I Libérée par le processus parent (dans le wait)
Attente de la terminaison de l’enfant :
#7
Informations obtenues lors de la fin du processus enfant
I Mode trace → octet poids fort = signal reçu, octet poids faible = 0xb1
I exit → octet poids fort = code retour enfant, octet poids faible = 0x00
I Signal → octet poids fort = 0xb1, octet poids faible = signal reçu
On utilise plutôt les macros WIFEXITED, WEXITSTATUS, WIFSIGNALED et WTERMSIG
Primitives correspondantes :
void exit(int code_retour), int atexit(void(*fonction_a_executer)())
pid_t wait(int *code_retour), pid_t waitpid(pid_t pid_enfant, int
*code_retour, int options)
& %
La primitive exit permet de quitter le processus courant en retournant un code de retour ; atexit permet
d’empiler les fonctions devant être exécutées à la terminaison du processus, le paramètre consistant en un
pointeur sur la fonction devant être exécutée. Le système GNU/Linux possède une primitive (on_exit). La
primitive atexit est préférable à on_exit, car elle est standard.
Les primitives wait et waitpid permettent d’attendre la fin d’un processus enfant. Dans le premier cas,
le premier enfant se terminant est pris en compte et la valeur pointée par le paramètre contient le code de
retour de l’enfant ; dans le second cas, le premier paramètre spécifie l’enfant devant être attendu : <-1 pour
n’importe quel enfant appartenant à un groupe de processus d’identité pid_enfant ; -1 pour n’importe quel
enfant ; 0 pour n’importe quel enfant appartenant au même groupe que le parent ; >0 pour l’enfant ayant
le PID indiqué. Le dernier paramètre permet de spécifier le comportement de la primitive ; en particulier,
WNOHANG permet de retourner même si aucun enfant ne s’est terminé. Voici un exemple de terminaison de
processus (fichier exemple_wait.c) :
/******************/
/* exemple_wait.c */
/******************/
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
void f(void) {
printf("On va bientot sortir\n");
}
void g(void) {
printf("On sort\n");
}
int main() {
int pid, code_retour;
TELECOM SudParis — Denis Conan, Michel Simatic et François Trahay — Avril 2016 — module CSC4508/M2 82
Communications inter-processus 1 Gestion des processus
pid = fork();
if (pid) { /* parent */
wait(&code_retour);
code_retour = WEXITSTATUS(code_retour);
printf("Parent : code de retour obtenu : %d\n", code_retour);
return(code_retour);
} else { /* enfant */
atexit(g);
atexit(f);
printf("enfant : terminaison avec code de retour %d\n", 5);
return 5;
}
return EXIT_SUCCESS;
}
' $
1.5 Exécution d’un nouveau programme
& %
La primitive exec, quelle qu’en soit sa forme, permet de modifier l’image (le segment de code) du processus
courant. Il est important de noter qu’il n’est pas possible de revenir d’un exec et que les instructions qui
suivent son appel ne sont accessibles que s’il s’est produit une erreur.
La liste des primitives est la suivante :
• int execl(char *nom_absolu, char *arg0, char *arg1,..., 0)
• int execv(char *nom_absolu, char *argv[])
• int execle(char *nom_absolu, char *arg0, char *arg1,..., 0, char **envp)
• int execve(char *nom_absolu, char **argv, char **envp)
• int execlp(char *nom_relatif, char *arg0, char *arg1,..., 0)
• int execvp(char *nom_relatif, char **argv)
Les primitives execlp et execvp agiront comme le shell dans la recherche du fichier exécutable si le nom
fourni ne contient pas de « slash » (/). Le chemin de recherche est spécifié dans la variable d’environnement
PATH. Si cette variable n’est pas définie, le chemin par défaut sera “/bin:/usr/bin:”. Voici un exemple de
recouvrement de code (fichier exemple_exec.c) :
/******************/
/* exemple_exec.c */
/******************/
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
TELECOM SudParis — Denis Conan, Michel Simatic et François Trahay — Avril 2016 — module CSC4508/M2 83
Communications inter-processus 2 Communications à l’aide de tubes
#include <unistd.h>
int main() {
int code_retour;
system("ps");
printf("pid = %d\n", getpid());
printf("ppid = %d\n", getppid());
printf("pgrp = %d\n", getpgrp());
printf("uid = %d\n", getuid());
printf("euid = %d\n", geteuid());
printf("gid = %d\n", getgid());
code_retour = execlp("ps", "ps", (char *)NULL);
printf("code_retour vaut %d\n",code_retour);
perror("pb sur exec");
printf("fin du programme appelant\n");
return EXIT_FAILURE;
}
' $
2 Communications à l’aide de tubes
& %
TELECOM SudParis — Denis Conan, Michel Simatic et François Trahay — Avril 2016 — module CSC4508/M2 84
Communications inter-processus 2 Communications à l’aide de tubes
' $
2.1 Principe des tubes
& %
' $
2.2 Tubes ordinaires (ou locaux)
Accès par création ou héritage
Perte par un processus = perte d’accès définitive par ce processus
Position courante entièrement déterminée par les lectures/écritures
Primitive lseek interdite
Taille maximum déterminée par PIPE_BUF définie dans <limits.h>
Opération de lecture par défaut bloquante
# 11
Bloquant jusqu’à lecture de la taille demandée ou disparition de tous les écrivains
Pour non bloquante : utiliser fcntl avec le drapeau 0_NONBLOCK
Opération d’écriture atomique (tout est écrit) si opération bloquante
Signal SIGPIPE si aucun lecteur
Primitive correspondante :
int pipe(int descfich[2])
I 0 pour la lecture et 1 pour l’écriture
→ Se mettre d’accord pour l’utilisation (celui qui écrit, ceux qui lisent)
& %
• Le tube est un moyen de communication inter-processus de type FIFO. Le tableau de deux entiers
passé en paramètre permet au retour de l’appel de connaître les deux flux associés au tube. Lors
de l’appel de la primitive pipe, le tube est créé au sein du processus réalisant l’appel. Pour réaliser
une communication inter-processus, il est nécessaire — suite à la création du tube — de dupliquer
le processus. L’écriture et la lecture des informations dans le tube s’effectuent à l’aide des primitives
write et read comme avec les fichiers classiques ; il n’est pas possible de se déplacer dans le tube (avec
la primitive lseek par exemple).
• Pour éviter les problèmes d’interblocage dûs à l’attente de données qui n’arriveront pas sur des des-
cripteurs que les écrivains ne ferment pas, il est fortement conseillé de ne conserver que les descripteurs
utiles et de fermer systématiquement tous les autres.
TELECOM SudParis — Denis Conan, Michel Simatic et François Trahay — Avril 2016 — module CSC4508/M2 85
Communications inter-processus 2 Communications à l’aide de tubes
• La taille maximum déterminée par PIPE_BUF détermine la taille des atomic write, i.e. la taille maximale
au delà de laquelle l’écriture risque de ne pas être atomique. En effet, si 2 processus écrivent sur le
même tube et que la taille des données écrites est proche de PIPE_BUF, il y a un fort risque que le
contenu de ces écritures soit mélangées dans le tube (un morceau de l’écriture du processus 1, puis un
morceau de l’écriture du processus 2, puis un morceau de la suite de l’écriture du processus1. . .).
/******************/
/* exemple_pipe.c */
/******************/
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
int pid, descfich[2], code_retour;
char tampon[80];
pipe(descfich);
pid = fork();
switch (pid) {
case -1 :
fprintf(stderr, "echec de fork\n");
exit(EXIT_FAILURE);
case 0 : /* enfant */
close(descfich[1]); /* ... n’ecrit pas dans le tube mais lit */
enfant(descfich[0]);
break;
default : /* parent */
close(descfich[0]); /* ... ne lit pas dans le tube mais ecrit */
parent(descfich[1], tampon);
wait(&code_retour);
break;
}
return EXIT_SUCCESS;
}
• Cet exemple permet à un parent d’envoyer des octets à un enfant. L’enfant sait que le parent a terminé
d’écrire ses données quand le parent ferme le tube. Dans la pratique, ce moyen de savoir que le parent
a terminé d’envoyer un bloc de données à l’enfant n’est pas pratique puisque le tube ne peut servir
qu’une seule fois ! Trois méthodes plus adaptées sont à disposition :
1. Le parent envoie à l’enfant des données qui ont une taille fixe (un entier, une structure de don-
nées. . .). Le parent écrit ses données et l’enfant fait un read de taille fixe octets. read renvoie
donc taille fixe (le parent a écrit une donnée) ou 0 (le parent a fermé le tube).
2. Le parent peut aussi envoyer des données d’une taille variable (par exemple, une chaîne de carac-
tères). Dans ce cas, le parent commence par envoyer un entier contenant la taille de la donnée qu’il
s’apprête à envoyer, puis il envoie cette donnée. L’enfant commence donc par faire un premier
read de sizeof(int) pour déterminer la taille du read qu’il doit faire juste après, puis il fait
le-dit read.
TELECOM SudParis — Denis Conan, Michel Simatic et François Trahay — Avril 2016 — module CSC4508/M2 86
Communications inter-processus 2 Communications à l’aide de tubes
NB : si les tailles sont faibles (quelques dizaines, voire centaines d’octets), il est plus perfor-
mant d’appliquer la méthode 1 en écrivant/lisant systématiquement le nombre maximum d’oc-
tets. Certes, on envoie des octets pour rien, mais on économise des 2 appels système par échange
d’information entre le parent et l’enfant : on est plus performant.
3. Le parent et l’enfant peuvent utiliser les fonctions d’écriture/lecture de la libC. Par exemple, le
parent fait des fputs pendant que l’enfant fait des fgets.
L’implémentation de ces 3 méthodes est laissée à titre d’exercice.
' $
2.3 Tubes nommés
Création par la primitive mknod qui permet de créer les fichiers spéciaux ou par
mkfifo
mknod était préalablement réservée au super-utilisateur
Ouverture possible par les processus connaissant le nom du tube
Par défaut, ouverture bloquante : lecteurs et écrivains s’attendent →
synchronisation
# 12
Suppression du tube lorsque explicitement demandé et pas d’ouverture en cours
Si suppression alors qu’il existe des lecteurs et écrivains, fonctionnement comme
un fichier ordinaire
Primitives correspondantes :
int mknod(const char *nom_fich, mode_t mode, dev_t n_periph)
I Mode = S_IFIFO (ou 0010000) plus les droits sur les trois derniers octets
int mkfifo(const char *nom_fich, mode_t mode)
I Mode = droits
& %
Remarque : les tubes nommés ont longtemps été le moyen privilégié pour communiquer entre plusiseurs
processus. Cette méthode tend à disparaître au profit de l’utilisation de files de message. La présente Section
n’est maintenue que pour des raisons historiques.
Le mode indique les permissions d’accès. Ces permissions sont modifiées par la valeur d’umask du pro-
cessus : les permissions d’accès effectivement adoptées sont (mode & NOT(umask)).
L’open d’un tube nommé en lecture (respectivement écriture) est bloquant jusqu’à ce qu’un écrivain
(respectivement un lecteur) ouvre le tube en écriture (respectivement lecture) à l’autre extrémité.
Dans l’exemple suivant, le serveur (fichier exemple_mkfifo_serveur.c) attend des requêtes (de la part
de clients décrits ci-après) sur le tube nommé ./client-serveur.
Noter que exemple_mkfifo_serveur.c ouvre le le tube qu’il crée en lecture/écriture au lieu d’une lecture
simple de sorte que :
• Il ne se bloque pas au moment du open en attendant qu’un processus ouvre le tube en écriture
• Si nous ouvrons le tube en lecture seule, à chaque fois que le client qui s’est connecté se termine,
la lecture du tube nommé va échouer car le noyau détecte une fermeture de fichier. En demandant
l’ouverture d’un tube en lecture et en ecriture, nous évitons ce genre de situation, car il reste toujours
au moins un descripteur ouvert sur la sortie.
/****************************/
/* exemple_mkfifo_serveur.c */
/****************************/
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
TELECOM SudParis — Denis Conan, Michel Simatic et François Trahay — Avril 2016 — module CSC4508/M2 87
Communications inter-processus 2 Communications à l’aide de tubes
#include <stdlib.h>
#include <string.h>
#include <errno.h>
/* Analyse de la requete */
/* NB : sscanf presente l’inconvenient que si le client envoie */
/* une chaine de caractere contenant un espace (par exemple : */
/* "PING PING") la chaine lue s’arrete au premier espace ! */
/* fgets serait probablement meilleur. */
sscanf(requete,"%s\n%s", typRequete, nomTubeClient);
/* Preparation de la reponse */
sprintf(reponse, "%s", "PONG");
/* Affichage */
printf("Serveur a recu \"%s\" et repond \"%s\" sur le tube nomme \"%s\"\n",
typRequete,
reponse,
nomTubeClient );
/* Réponse sur le tube nommé du client */
fdW = open(nomTubeClient, O_WRONLY);
if (fdW == -1) {
perror("open(nomTubeClient)");
exit(EXIT_FAILURE);
}
nbWrite = write(fdW, reponse, sizeof(reponse));
if (nbWrite < sizeof(reponse)) {
perror("pb ecriture sur tube nomme");
}
/* Dans cette application, le client ne renvoie pas de requête ultérieure*/
/* nécessitant une réponse ==> On peut fermer ce tube */
close(fdW);
}
int main() {
char requete[TAILLEMSG];
int fdR;
int nbRead;
Le client exemple_mkfifo_client.c envoie au serveur des requêtes contenant le message PING et le nom
du tube (./serveur-client.<numéroProcessusClient>) sur lequel le client attend la réponse PONG.
/***************************/
/* exemple_mkfifo_client.c */
TELECOM SudParis — Denis Conan, Michel Simatic et François Trahay — Avril 2016 — module CSC4508/M2 88
Communications inter-processus
/***************************/
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main() {
char nomTubeClient[128];
char requete[TAILLEMSG];
char reponse[TAILLEMSG];
int fdW, fdR;
int nbRead, nbWrite;
Dans cet exemple, on a donc un tube nommé pour les communications client-serveur et un tube nommé
(par client) pour les communications serveur-client.
TELECOM SudParis — Denis Conan, Michel Simatic et François Trahay — Avril 2016 — module CSC4508/M2 89
Communications inter-processus 3 Communication à l’aide des IPC POSIX
' $
3 Communication à l’aide des IPC POSIX
& %
' $
3.1 Inter Process Communication POSIX
& %
Le terme « IPC » recouvre trois mécanismes de communication introduits à partir du System V : les files
de messages, la mémoire partagée et les sémaphores. Ces mécanismes utilisent un système de fichier virtuel
permettant la persistance. Chaque type d’IPC est donc accessible depuis le système de fichiers :
• Les sémaphores nommés sont stockés dans /dev/shm/ sous la forme sem.X (où X est la clé du sémaphore)
• Les files de message sont accessibles via un montage dans le système de fichier (par exemple : mount
-t mqueue none /dev/mqueue)
TELECOM SudParis — Denis Conan, Michel Simatic et François Trahay — Avril 2016 — module CSC4508/M2 90
Communications inter-processus 3 Communication à l’aide des IPC POSIX
Les IPC nécessitent une clé permettant d’identifier l’objet IPC. Une même clé doit donc être fournie par
tous les processus désirant accéder à l’objet. Une clé est une chaîne de caractère commençant par “/” et ne
comportant pas le caractère “/” ailleurs.
Il existe des formes plus évoluées de communication entre les processus d’une même machine. Citons D-Bus
(http://dbus.freedesktop.org), ICE (http://www.zeroc.com), ZeroMQ (http://www.zeromq.org). . .Vu
les fonctionnalités offertes en général par ces librairies (gestion de cycle de vie, gestion de publication-
souscription. . .), ces librairies s’apparentent plus à des intergiciels (middleware) de communication qu’à des
mécanismes d’IPC. Ils seront donc plutôt étudiés en CSC5002 (Intergiciels pour applications réparties).
' $
3.2 IPC System V versus POSIX IPC
System V = une des premières versions majeures d’Unix. A servi de base pour
l’élaboration du standard POSIX
POSIX = Portable Operating System Interface (défini par IEEE)
# 15
System V et POSIX définissent les IPC de manières différentes
Pendant longtemps, implantation incomplète des IPC POSIX sur certains systèmes
Les IPC System V ont perduré pendant longtemps
Aujourd’hui IPC POSIX implantées correctement dans Linux et la plupart des Unix
& %
Les IPC POSIX sont implantées correctement dans la plupart des Unix actuels. Toutefois, Mac OS X
n’offre pas une implantation complète du standard.
' $
3.3 Files de messages
Création / destruction :
mqd_t mq_open(const char *name, int oflag, mode_t mode, struct
mq_attr *attr)
int mq_unlink(mqd_t mq)
Ouverture / fermeture d’une file déjà créée :
# 16
mqd_t mq_open(const char *name, int oflag)
int mq_close(mqd_t mq)
Émission / réception (bloquante si file pleine et oflag ne contient pas O_NONBLOCK) :
int mq_send(mqd_t mqdes, const char *msg_ptr, size_t msg_len,
unsigned msg_prio)
ssize_t mq_receive(mqd_t mqdes, char *msg_ptr, size_t msg_len,
unsigned *msg_prio)
& %
TELECOM SudParis — Denis Conan, Michel Simatic et François Trahay — Avril 2016 — module CSC4508/M2 91
Communications inter-processus 3 Communication à l’aide des IPC POSIX
Lorsqu’un processus envoie un message dans une file, celui-ci est placé par ordre de priorité dans la file, les
messages de même priorité étant classés par ordre d’arrivée. Lorsqu’un processus désire recevoir un message,
il lit le premier message disponible (le plus prioritaire). Ceci se traduit par cinq opérations.
mq_open permet d’obtenir un identifiant de file de messages à partir d’une clé et des droits d’accès et les
conditions de création associées. Le quatrième paramètre de mq_open permet de spécifier les attributs (taille
maximale des messages, nombre maximal de messages dans la file, etc.) de la file de message.
mq_close permet de fermer une file de message. Chaque processus utilisant la file de message doit appeler
cette fonction.
mq_unlink permet de détruire définitivement une file de message.
mq_send permet d’envoyer un message dans la file identifiée par le premier paramètre et dont l’adresse
et la taille sont respectivement les deuxième et troisième paramètres. Le dernier paramètre défini la priorité
du message.
mq_receive demande la réception d’un message à partir de l’identifiant de file de messages. Ce message
sera placé à l’adresse indiquée par le deuxième paramètre et ne pas dépasser la taille indiquée par le troisième.
Si le quatrième paramètre n’est pas NULL, mq_receive y affecte la priorité associée au message lors de l’appel
à mq_send.
À moins que le drapeau O_NONBLOCK ne soit passé lors de l’appel à mq_open, la réception des messages est
toujours bloquante ; l’envoi n’est bloquant que quand la file de message est pleine. Pour des envois/réceptions
non bloquants, on peut utiliser les fonctions mq_timedsend et mq_timedreceive :
ssize_t mq_timedsend(mqd_t mqdes, const char *msg_ptr, size_t msg_len, unsigned
msg_prio, const struct timespec *abs_timeout)
ssize_t mq_timedreceive(mqd_t mqdes, char *msg_ptr, size_t msg_len, unsigned
*msg_prio, const struct timespec *abs_timeout);
Ces fonctions prennent un paramètre supplémentaire de type struct timespec correspondant à la date
jusqu’à laquelle la fonction doit se bloquer si aucun message n’est reçu (pour mq_timedreceive) ou si la file
est pleine (pour mq_timedsend).
Il est également possible de traiter les messages reçu depuis une file de message de manière événementielle
en utilisant :
int mq_notify(mqd_t mqdes, const struct sigevent *sevp)
Lorsqu’un message est reçu dans la file mqdes, la fonction sevp->sigev_notify_function est
appelée (si sevp->sigev_notify vaut SIGEV_THREAD) ou le signal sevp->sigev_signo est émis (si
sevp->sigev_notify vaut SIGEV_SIGNAL). Un exemple d’utilisation de mq_notify est disponible dans
msg_queue/example_msg_server_notify.c.
Voici un exemple de communication à l’aide de files de messages Posix (fichiers
msg_queue/example_msg_server.c et msg_queue/example_msg_client.c) :
/*************************/
/* example_msg_server.c */
/*************************/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <mqueue.h>
#include "example_msg.h"
if(argc != 2) {
fprintf(stderr, "Usage: %s /message_queue_name\n", argv[0]);
exit(EXIT_FAILURE);
}
TELECOM SudParis — Denis Conan, Michel Simatic et François Trahay — Avril 2016 — module CSC4508/M2 92
Communications inter-processus 3 Communication à l’aide des IPC POSIX
for (;;) {
/* wait for any requests */
int len = mq_receive(requests, (char*)&req, sizeof(req), NULL);
if(len < 0) {
perror("Server: mq_receive");
exit(EXIT_FAILURE);
}
if (req.text[0] == ’@’) {
/* ask to terminate the server */
break;
}
return EXIT_SUCCESS;
}
/************************/
/* example_msg_client.c */
/************************/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
TELECOM SudParis — Denis Conan, Michel Simatic et François Trahay — Avril 2016 — module CSC4508/M2 93
Communications inter-processus 3 Communication à l’aide des IPC POSIX
#include <string.h>
#include <mqueue.h>
#include "example_msg.h"
if (argc != 3) {
printf("Client: Usage : %s /message_queue_name text_to_send\n",argv[0]);
exit(EXIT_FAILURE);
}
return EXIT_SUCCESS;
}
TELECOM SudParis — Denis Conan, Michel Simatic et François Trahay — Avril 2016 — module CSC4508/M2 94
Communications inter-processus 3 Communication à l’aide des IPC POSIX
' $
3.4 Mémoire partagée
Création :
int shm_open(const char *name, int oflag, mode_t mode)
Création (oflag contient O_CREAT) d’un objet de taille nulle
Retour = descripteur de fichier
Attachement :
# 17 void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset)
addr = 0 → choix de l’adresse par le système d’exploitation
Une même région peut être attachée plusieurs fois à des adresses différentes
prot : droit d’accès (lecture/écriture/exécution)
flags : MAP_SHARED pour partager la zone mémoire avec un autre processus
fd : descripteur de fichier retourné par shm_open
offset : emplacement de départ de la zone dans la mémoire partagée
Retour : adresse d’attachement effective ou MAP_FAILED
Détachement :
int munmap(void *addr, size_t length)
& %
' $
Fermeture :
int close(int fd)
Destruction :
int shm_unlink(const char *name)
# 18
& %
Un segment de mémoire partagée est un morceau de mémoire qui peut être commun à plusieurs pro-
cessus : l’espace mémoire virtuel est ainsi partagé. Toute modification effectuée par l’un des processus peut
automatiquement être connue des autres processus partageant ce morceau de mémoire. Trois étapes jalonnent
l’utilisation de la mémoire partagée :
• shm_open permet d’obtenir un identifiant associé au segment de mémoire partagée dont la clé est name.
Le paramètre oflag précise les conditions de création. Le dernier paramètre (mode) précise les droits
d’accès. Une fois le segment de mémoire partagé créé, il se comporte comme un fichier classique et peut
être manipulé à partir du descripteur de fichier fd.
• Lors de sa création, le segment de mémoire partagé est de taille nulle. Le programme peut, par la suite,
changer sa taille à l’aide de la fonction ftruncate.
TELECOM SudParis — Denis Conan, Michel Simatic et François Trahay — Avril 2016 — module CSC4508/M2 95
Communications inter-processus 3 Communication à l’aide des IPC POSIX
• mmap permet d’attacher (c’est-à-dire d’insérer) le segment de mémoire partagé dans l’espace d’adressage
virtuel du processus en précisant le descripteur de fichier du segment de mémoire partagé, l’adresse
préférée où attacher le segment (si la valeur NULL est fournie, le système place le segment dans le premier
espace libre suffisamment grand), les droits d’accès (une combinaison de PROT_EXEC, PROT_WRITE,
PROT_READ et PROT_NONE) et un drapeau permettant — entre autres — de spécifier que le segment
de mémoire est destiné à être partagé entre plusieurs processus (MAP_SHARED). En retour, la primitive
retourne l’adresse, dans l’espace d’adressage virtuel du processus, où le segment a été attaché.
Lors d’un fork, l’enfant hérite de la mémoire partagée. Lors d’un exec ou d’un exit, les segments
mémoire du processus sont automatiquement détachés.
La terminaison d’un programme utilisant un segment de mémoire partagée se décompose en trois étapes :
• munmap détache le segment de mémoire partagé
• close ferme le segment de mémoire partagé
• shm_unlink supprime le segment de mémoire partagé. Une fois le segment supprimé, plus aucun pro-
cessus ne peut l’utiliser.
Voici un exemple de communication à l’aide de segments de mémoire partagée IPC (fichiers
exemple_shm.c :
/*************************/
/* exemple_shm.c */
/*************************/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <sys/stat.h> /* For mode constants */
#include <fcntl.h> /* For O_* constants */
#include <errno.h>
#include <time.h>
#include <semaphore.h>
#include <stdint.h>
#define NB_ELEMENTS 10
typedef struct {
int tab[NB_ELEMENTS];
} shared_memory;
shared_memory *buffer;
void print_buffer() {
int i;
printf("The shared memory segment contains:\n");
for(i=0; i<NB_ELEMENTS; i++) {
printf("%d\t", buffer->tab[i]);
}
printf("\n");
}
rank = atoi(argv[1]);
TELECOM SudParis — Denis Conan, Michel Simatic et François Trahay — Avril 2016 — module CSC4508/M2 96
3 Communication à l’aide des IPC POSIX 3.5 Sémaphores
return EXIT_SUCCESS;
}
' $
3.5 Sémaphores
& %
TELECOM SudParis — Denis Conan, Michel Simatic et François Trahay — Avril 2016 — module CSC4508/M2 97
3 Communication à l’aide des IPC POSIX 3.5 Sémaphores
' $
3.5.1 Introduction aux sémaphores
# 21
& %
TELECOM SudParis — Denis Conan, Michel Simatic et François Trahay — Avril 2016 — module CSC4508/M2 98
3 Communication à l’aide des IPC POSIX 3.5 Sémaphores
−1 sur valeur
+1 sur valeur
• 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 :
– OU BIEN feu rouge alors que le parking contient des places libres
' $
3.5.3 Introduction aux sémaphores : algorithmes P ET V
Initialisation(sémaphore,n)
valeur[sémaphore] = n
P(sémaphore)
valeur[sémaphore] = valeur[sémaphore] - 1
si (valeur[sémaphore] < 0) alors
étatProcessus = Bloqué
# 22 mettre processus en file d’attente
finSi
invoquer l’ordonnanceur
V(sémaphore)
valeur[sémaphore] = valeur[sémaphore] + 1
si (valeur[sémaphore] <= 0) alors
extraire processus de file d’attente
étatProcessus = Prêt
finSi
invoquer l’ordonnanceur
& %
TELECOM SudParis — Denis Conan, Michel Simatic et François Trahay — Avril 2016 — module CSC4508/M2 99
3 Communication à l’aide des IPC POSIX 3.5 Sémaphores
' $
3.5.4 Sémaphores POSIX : initialisation
2 types de sémaphores :
Sémaphores nommés : identifiés par une clé de la forme /un_nom. Les sémaphores
nommés sont persistants.
Création / ouverture :
sem_t *sem_open(const char *name, int oflag, mode_t mode,
# 23 unsigned int value)
sem_t *sem_open(const char *name, int oflag)
Fermeture / destruction :
int sem_close(sem_t *sem)
int sem_unlink(const char *name)
Sémaphores anonymes : placés dans une zone mémoire partagée par plusieurs
threads ou processus (par exemple dans un segment de mémoire partagée).
Initialisation :
int sem_init(sem_t *sem, int pshared, unsigned int value)
& %
' $
Destruction :
int sem_destroy(sem_t *sem)
# 24
& %
TELECOM SudParis — Denis Conan, Michel Simatic et François Trahay — Avril 2016 — module CSC4508/M2 100
3 Communication à l’aide des IPC POSIX 3.5 Sémaphores
' $
3.5.5 Sémaphores POSIX : utilisation
Une fois ouvert/initialisés les deux types de sémaphores peuvent être manipulés avec :
Prise :
int sem_wait(sem_t *sem)
# 25 int sem_trywait(sem_t *sem)
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout)
Validation :
int sem_post(sem_t *sem)
Consultation :
int sem_getvalue(sem_t *sem, int *sval)
& %
La création d’un sémaphore nommé se fait en fournissant son identifiant (name), un drapeau (O_CREATE
pour créer le sémaphore), les droits d’accès au sémaphore, ainsi que la valeur initiale du sémaphore.
La création d’un sémaphore anonyme consiste à initialiser un objet de type sem_t accessible par tous
les threads succeptibles d’utiliser le sémaphore. Si le sémaphore est utilisé par plusieurs processus, on place
généralement le sémaphore dans un segment de mémoire partagé par les processus. Dans ce cas, le paramètre
pshared doit être à 1 lors de l’appel à sem_init. Dans tous les cas, le paramètre value désigne la valeur
initiale du sémaphore.
Une fois la phase d’initialisation terminée, le sémaphore est utilisable avec sem_post (équivalent à l’opé-
ration V()) et sem_wait (équivalent à l’opération P()).
La fonction sem_wait est bloquante : si la valeur du sémaphore devient inférieure à 0, le thread se bloque
en attendant que la valeur du sémaphore redevienne positive. Pour éviter de bloquer indéfiniement, on peut
utiliser sem_timedwait qui fixe une date à partir de laquelle le thread doit être débloqué. Si sem_timedwait
se termine du fait de l’expiration du timer, la fonction retourne -1 et errno vaut ETIMEDOUT. Il est également
possible d’utiliser la fonction non-bloquante sem_trywait qui tente de prendre le sémaphore. En cas d’échec
(si la valeur du sémaphore est inférieure à 1), la fonction ne se bloque pas, mais retourne -1 et errno vaut
EAGAIN.
Deux fonctions sont traditionnellement réalisées lorsque l’on utilise les sémaphores. La première P(s)
(correspondant à la fonction sem_post) se traduit par « Puis-je ? », tandis que la seconde V(s) (correspondant
à la fonction sem_wait) signifie « Vas-y ! ». Lorsque l’on désire prendre une ressource, on effectue un P(s) ;
lorsque la ressource n’est plus nécessaire, on la relâche en effectuant un V(s).
L’exemple suivant utilise un sémaphore pour réaliser un parking.
Les deux programmes se partagent la constante CLE_PARKING définie dans exempleSemInit.h.
/****************/
/* exempleSem.h */
/****************/
Le programme exempleSemInit.c crée ce sémaphore (dont la clé est basée sur un nom de fichier) et
l’initialise :
/********************/
/* exempleSemInit.c */
/********************/
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h> /* For O_* constants */
#include <sys/stat.h> /* For mode constants */
#include <semaphore.h>
TELECOM SudParis — Denis Conan, Michel Simatic et François Trahay — Avril 2016 — module CSC4508/M2 101
Communications inter-processus
#include "exempleSem.h"
#define NB_PLACES 2
if (argc != 1) {
fputs("USAGE = exempleSemInit\n", stderr);
exit(EXIT_FAILURE);
}
return EXIT_SUCCESS;
}
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h> /* For O_* constants */
#include <sys/stat.h> /* For mode constants */
#include <semaphore.h>
#include <unistd.h>
#include "exempleSem.h"
if (argc != 1) {
fputs("USAGE = exempleSem\n", stderr);
exit(EXIT_FAILURE);
}
P(semParking);
printf("\a...OK\n");
sleep(5);
sleep(5);
return EXIT_SUCCESS;
}
TELECOM SudParis — Denis Conan, Michel Simatic et François Trahay — Avril 2016 — module CSC4508/M2 102
Communications inter-processus
' $
4 Comparaison des mécanismes de synchronisation
& %
Cette comparaison des mécanismes de synchronisation a été réalisée lors des TP notés de 2009 (première
et deuxième session). Pour le code des expériences réalisées, se reporter au corrigé de ces TP notés.
Le principe de ces expériences est le suivant : un thread distributeur envoie 1.000.000 messages via un
medium de communication à des threads traiteur. Les performances sont mesurées à l’aide de la commande
clock_gettime sur une machine munie d’un processeur quadri-cœur Intel Core i7-4600U cadencé à 2.1 GHz,
8 Go de RAM et, du noyau Linux 4.4.0. Noter que cette comparaison est réalisée à l’aide de threads. De ce
fait, les threads sont en mesure de partager l’espace mémoire. Par exemple, une zone mémoire allouée via
malloc par le thread distributeur est libérable via free par un thread traiteur.
Voici le détail des expériences réalisées :
1. Le thread distributeur envoie systématiquement 4096 octets aux threads traiteur via un tube.
• Le thread distributeur écrit d’abord dans le tube la longueur de la chaîne qu’il s’apprête à écrire,
puis la chaîne elle-même (et pas tous les octets du tableau de caractères dans lequel est stockée
cette chaîne) ;
• Chaque thread traiteur lit d’abord la longueur de la chaîne qui a été écrite par le thread distributeur,
puis la chaîne elle-même.
Le gain obtenu (47%) est dû au fait qu’on divise par 4096/416 ' 10 le nombre d’octets transférés
(inutilement) entre la mémoire du thread distributeur et le tube d’une part, et entre le tube et la
mémoire des threads traiteurs d’autre part.
3. Dans cette méthode, on écrit encore moins d’octets dans le tuyau et on économise d’une part les
recopies d’octets entre la mémoire du thread distributeur et le tube, et d’autre part celles entre le tube
et la mémoire de chaque thread traiteur.
• À chaque requête, le thread distributeur alloue, à l’aide de malloc, une zone mémoire qu’il remplit ;
puis il stocke le pointeur (vers cette zone mémoire) dans le tube ;
• Chaque thread traiteur lit ce pointeur dans le tube, puis traite les données dans cette zones
mémoire, et enfin libère cette zone mémoire à l’aide de free.
Si on a gagné 7% de temps par rapport à la première méthode de travail, on a perdu par rapport au code
de la deuxième méthode (73% d’augmentation). Les malloc/free sont ici beaucoup plus pénalisants
que l’écriture/lecture des octets du message sur le tube.
TELECOM SudParis — Denis Conan, Michel Simatic et François Trahay — Avril 2016 — module CSC4508/M2 103
Communications inter-processus
4. Dans cette méthode, on remplace le tube par un tableau de 2 ∗ N cases (N étant le nombre de threads
traiteur) géré par les threads selon un paradigme de synchronisation Producteur/Consommateur, en
utilisant des sémaphores POSIX (sem_t).
• On définit un tableau de 2 ∗ N zones de TAILLE_MAX_REQUETE octets ;
• À chaque requête, le thread distributeur :
(a) essaye d’accéder à l’une des cases de ce tableau selon le paradigme de synchronisation Pro-
ducteur/Consommateur,
(b) communique l’adresse de cette case à la fonction chargée de remplir cette zone mémoire,
(c) informe les threads traiteur grâce au paradigme Producteur/Consommateur qu’une informa-
tion est prête ;
• Chaque thread traiteur attend qu’une information soit prête dans ce tableau ;
• Quand c’est le cas, le thread traiteur :
(a) recopie le contenu de la case prête dans une variable locale,
(b) indique au thread distributeur (grâce au paradigme Producteur/Consommateur) que cette
place est à nouveau disponible,
(c) transmet l’adresse de cette variable locale à sa fonction de traitement ;
• Toutes les opérations de synchronisation sur ces tableaux sont réalisées à l’aide de sémaphores
sem_t.
Le gain obtenu est dû au fait qu’on économise au maximum les transferts mémoire entre la mémoire
des threads et celle du tube. De plus, le mécanisme de synchronisation à base de sémaphore semble
plus efficace que celui basé sur les tubes.
5. Dans cette méthode, on utilise des conditions POSIX au lieu de sémaphores POSIX. Il apparaît que
les conditions sont moins performantes (ce qui semble logique, vu qu’on fait beaucoup plus d’appels à
des primitives de synchronisation tout au long du code).
6. Cette méthode s’appuie sur une file de message IPC (et non POSIX).
• le thread distributeur écrit ses requêtes en tant que messages dans une file de messages IPC
(primitive msgsnd et non mq_send) qu’il a créée au moment de l’initialisation du programme ;
• chaque thread traiteur lit un message sur cette file, puis appelle sa fonction de traitement avec la
requête contenue dans ce message.
• La file de message est supprimée, une fois que le programme a fini son exécution.
Les performances de ce code sont légèrement moins bonnes que les performances de la solution 4.
Toutefois, la solution à base de file de message a l’avantage de pouvoir fonctionner entre plusieurs
processus et nécessite un code plus “propre”. Une expérience réalisée avec les files de messages POSIX
montre que ces dernières ont un niveau de performance équivalent au files de messages IPC.
7. Quelles sont les performances d’un tube nommé par rapport à un tube standard ? La question est
ouverte (au sens où le test reste à faire).
TELECOM SudParis — Denis Conan, Michel Simatic et François Trahay — Avril 2016 — module CSC4508/M2 104
Synchronisation entre processus
module CSC4508/M2
Avril 2016
105
Synchronisation entre processus 1 Introduction
' $
Plan du document
1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
#2 2 Sémaphore = Outil de base . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
3 Résolution de problèmes de synchronisation typiques . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
4 Interblocage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
5 Mise en oeuvre dans un système d’exploitation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
& %
' $
1 Introduction
& %
TELECOM SudParis — Dominique Bouillet et Michel Simatic — Avril 2016 — module CSC4508/M2 106
Synchronisation entre processus 2 Sémaphore = Outil de base
' $
1.1 Correspondance problèmes vie courante/informatique
Banque Problème d’exclusion mutuelle : une ressource ne doit être accessible que par une
entité à un instant donné. Cas, par exemple, d’une zone mémoire contenant le solde d’un
compte.
#4 Roméo et Juliette Problème de passage de témoin : on divise le travail entre des processus.
Cas, par exemple, de 2 processus qui doivent s’échanger des informations à un moment
donné de leur exécution avant de continuer.
' $
2 Sémaphore = Outil de base
#5 2.1 Généralités . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
2.2 Analogie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
3.0 Algorithmes P ET V . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
& %
TELECOM SudParis — Dominique Bouillet et Michel Simatic — Avril 2016 — module CSC4508/M2 107
Synchronisation entre processus 2 Sémaphore = Outil de base
' $
2.1 Généralités
#7
& %
TELECOM SudParis — Dominique Bouillet et Michel Simatic — Avril 2016 — module CSC4508/M2 108
Synchronisation entre processus
+1 sur valeur
• 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 :
– OU BIEN feu rouge alors que le parking contient des places libres
' $
2.3 Algorithmes P ET V
Initialisation(sémaphore,n)
valeur[sémaphore] = n
P(sémaphore)
valeur[sémaphore] = valeur[sémaphore] - 1
si (valeur[sémaphore] < 0) alors
étatProcessus = Bloqué
#8 mettre processus en file d’attente
finSi
invoquer l’ordonnanceur
V(sémaphore)
valeur[sémaphore] = valeur[sémaphore] + 1
si (valeur[sémaphore] <= 0) alors
extraire processus de file d’attente
étatProcessus = Prêt
finSi
invoquer l’ordonnanceur
& %
TELECOM SudParis — Dominique Bouillet et Michel Simatic — Avril 2016 — module CSC4508/M2 109
Synchronisation entre processus 3 Résolution de problèmes de synchronisation typiques
' $
3 Résolution de problèmes de synchronisation typiques
& %
Le sémaphore permet d’élaborer des mécanismes de plus haut niveau qui sont étudiés dans cette partie.
On disposera ainsi de paradigmes (ou patrons), c’est-à-dire d’exemples-type permettant de modéliser
des classes de problèmes qui sont fréquemment rencontrés et qui sont présents à tous les niveaux dans les
systèmes et applications concurrentes.
Leur solution fournit des schémas de conception et de programmation de composants concurrents. Ils
servent de briques de base à toute étude, analyse ou construction de systèmes ou d’applications coopératives.
NB : il existe de nombreux autres problèmes/paradigmes aux noms plus poétiques les uns que
les autres (le problème du salon de coiffure, le problème du Père Noël, le problème de la montagne
russe. . .) [Downey, 2005].
' $
3.1 Exclusion mutuelle
Quand Prog1 s’exécute sans que Prog2 soit activé, alors P(mutex) fait passer mutex à 0 et Prog1 entre
en section critique.
TELECOM SudParis — Dominique Bouillet et Michel Simatic — Avril 2016 — module CSC4508/M2 110
3 Résolution de problèmes de synchronisation typiques 3.3 Passage de témoin
Si Prog2 est activé quand Prog1 est en section critique, alors P(mutex) fait passer mutex à −1. Donc
Prog2 se retrouve bloqué. Quand Prog1 exécute V(mutex), alors mutex passe à 0 et Prog2 est débloqué.
' $
3.2 Cohorte
Le (N + 1)e véhicule qui cherche à entrer dans le parking se retrouve bloqué par son P(parking). Il n’est
débloqué que quand un autre véhicule sort en faisant V(parking).
Noter que valeurSem[parking] = valeurInitialeSem[parking] − nb(P ) + nb(V )
' $
3.3 Passage de témoin
# 12 3 types :
Envoi de signal
Rendez-vous entre 2 processus
Appel procédural entre processus
& %
TELECOM SudParis — Dominique Bouillet et Michel Simatic — Avril 2016 — module CSC4508/M2 111
3 Résolution de problèmes de synchronisation typiques 3.3 Passage de témoin
' $
3.3.1 Envoi de signal
Ce mécanisme peut être utilisé par Prog1 pour donner à Prog2 le droit d’accès à une ressource quand
Prog1 estime que cette ressource est prête.
' $
3.3.2 Rendez-vous entre deux processus
& %
Ce patron permet à Prog1 et Prog2 de s’échanger des informations à un moment précis de leur exécution
avant de continuer.
Généralisation : principe d’un rendez-vous entre N processus (NB : cet algorithme ne fonctionne qu’une
seule fois)
TELECOM SudParis — Dominique Bouillet et Michel Simatic — Avril 2016 — module CSC4508/M2 112
Synchronisation entre processus 3 Résolution de problèmes de synchronisation typiques
...
P(mutex)
si nbAttente < N - 1 alors
nbAttente = nbAttente + 1
V(mutex)
P(rdv)
sinon
V(mutex)
répéter (N-1) fois
V(rdv)
fin répéter
nbAttente = 0 // Pas obligatoire, vu qu’algorithme ne fonctionne qu’une fois
finsi
...
' $
3.3.3 Appel procédural entre processus
& %
Serveur est le serveur d’« appel de procédure ». Il se met en attente d’un appel en faisant P(appel).
Client démarre. Il prépare ses paramètres d’appel en les mettant, par exemple, dans une zone de mémoire
partagée. Avec V(appel), il prévient Serveur de la disponibilité de ces informations.
Serveur est alors réactivé. Il analyse les paramètres d’appel, effectue son traitement et stocke les pa-
ramètres de retour dans une autre zone de mémoire partagée. Avec V(retour), il prévient Client de la
disponibilité de ces informations. Il se met ensuite en attente d’un autre appel.
Client est réactivé, analyse les paramètres retour et poursuit son traitement.
TELECOM SudParis — Dominique Bouillet et Michel Simatic — Avril 2016 — module CSC4508/M2 113
3 Résolution de problèmes de synchronisation typiques 3.4 Producteurs/Consommateurs
' $
3.4 Producteurs/Consommateurs
3.4.2 Objectif . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
3.4.2 Principe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
# 16 3.4.4 Déposer et extraire . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
3.4.4 Exemple . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
3.4.6 K producteurs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
3.4.6 Exemple de problème avec 2 producteurs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
3.5.1 Solution complète . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
& %
' $
3.4.1 Objectif
& %
Ce patron est une généralisation des fonctionnalités offertes par les tubes (transit d’information entre K
processus producteurs et P processus consommateurs).
TELECOM SudParis — Dominique Bouillet et Michel Simatic — Avril 2016 — module CSC4508/M2 114
3 Résolution de problèmes de synchronisation typiques 3.4 Producteurs/Consommateurs
' $
3.4.2 Principe
& %
Ici, le producteur Produc peut écrire N informations avant d’être bloqué (degré de liberté N ).
' $
3.4.3 Déposer et extraire
& %
TELECOM SudParis — Dominique Bouillet et Michel Simatic — Avril 2016 — module CSC4508/M2 115
3 Résolution de problèmes de synchronisation typiques 3.4 Producteurs/Consommateurs
' $
3.4.4 Exemple
1. On suppose qu’à un instant donné, le tampon est dans l’état suivant :
Tampon : info8
valeurSem[placeDispo] = 4 et valeurSem[inf oP rete] = 1
iDépot = 4 et iExtrait = 3
2. Exécution Produc : il produit info9
# 21
& %
NB :
• 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 :
TELECOM SudParis — Dominique Bouillet et Michel Simatic — Avril 2016 — module CSC4508/M2 116
3 Résolution de problèmes de synchronisation typiques 3.4 Producteurs/Consommateurs
iDépot = 0 et iExtrait = 2
' $
3.4.5 K producteurs
& %
' $
3.4.6 Exemple de problème avec 2 producteurs
& %
TELECOM SudParis — Dominique Bouillet et Michel Simatic — Avril 2016 — module CSC4508/M2 117
3 Résolution de problèmes de synchronisation typiques 3.4 Producteurs/Consommateurs
' $
Exemple de problème avec 2 producteurs (suite)
1. Début d’exécution de Produc1 :
P(placeDispo);tampon[iDépot] = infoP1;...
Tampon : info0 info1 infoP1
valeurSem[placeDispo] = 2 et valeurSem[inf oP rete] = 2
iDépot = 2 et iExtrait = 0
# 25
& %
Conclusion de l’exemple :
• Mais
TELECOM SudParis — Dominique Bouillet et Michel Simatic — Avril 2016 — module CSC4508/M2 118
3 Résolution de problèmes de synchronisation typiques 3.5 Lecteurs/rédacteurs
' $
3.4.7 Solution complète
& %
' $
3.5 Lecteurs/rédacteurs
3.5.1 Objectif . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
# 27 3.5.3 Solution de base . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .30
3.5.3 Analyse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
3.5.4 Solution avec priorités égales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
& %
TELECOM SudParis — Dominique Bouillet et Michel Simatic — Avril 2016 — module CSC4508/M2 119
3 Résolution de problèmes de synchronisation typiques 3.5 Lecteurs/rédacteurs
' $
3.5.1 Objectif
Permettre une compétition cohérente entre deux types de processus (les « lecteurs »
# 28 et les « rédacteurs ») :
Plusieurs lecteurs peuvent accéder simultanément à la ressource
Les rédacteurs sont exclusifs entre eux pour leur exploitation de la ressource
Un rédacteur est exclusif avec les lecteurs
& %
Ce patron permet, par exemple, d’interdire à un processus d’écrire dans un fichier, si des processus sont
en train de lire (simultanément) ce fichier. De plus, deux processus ne peuvent écrire simultanément dans le
fichier. En revanche, plusieurs processus peuvent lire simultanément le fichier.
Le paradigme Lecteurs/Rédacteur permet de réaliser une exclusion mutuelle entre un groupe d’entités (les
lecteurs) et une entité (le rédacteur). Il est généralisable à l’exclusion mutuelle entre deux groupes d’entités
(cf. TP noté 2006, question 2 : exclusion mutuelle entre les nains et les ours).
' $
3.5.2 Solution de base
Sémaphore mutexG initialisé à 1
Sémaphore mutexL initialisé à 1
Entier NL initialisé à 0
Lecteur Rédacteur
P(mutexL) P(mutexG)
|NL = NL + 1 | ecrituresEtLectures()
|si NL == 1 alors V(mutexG)
# 29
| P(mutexG)
|finSi
V(mutexL)
lectures()
P(mutexL)
|NL = NL - 1
|si NL == 0 alors
| V(mutexG)
|finSi
V(mutexL)
& %
TELECOM SudParis — Dominique Bouillet et Michel Simatic — Avril 2016 — module CSC4508/M2 120
3 Résolution de problèmes de synchronisation typiques 3.5 Lecteurs/rédacteurs
' $
3.5.3 Analyse
& %
• Un lecteur L3 arrive. . .
' $
3.5.4 Solution avec priorités égales
Sémaphore mutexG initialisé à 1
Sémaphore mutexL initialisé à 1
Sémaphore fifo initialisé à 1
Entier NL initialisé à 0
Lecteur Rédacteur
P(fifo) P(fifo)
P(mutexL) P(mutexG)
NL = NL + 1 V(fifo)
# 31 si NL == 1 alors ecrituresEtLectures()
P(mutexG) V(mutexG)
finSi
V(mutexL)
V(fifo)
lectures()
P(mutexL)
NL = NL - 1
si NL == 0 alors
V(mutexG)
finSi
V(mutexL)
& %
TELECOM SudParis — Dominique Bouillet et Michel Simatic — Avril 2016 — module CSC4508/M2 121
Synchronisation entre processus 4 Interblocage
Attention, cette solution ne marche que parce qu’on suppose que la file d’attente de processus en attente
sur le sémaphore fifo est elle-même gérée en FIFO ! Si ce n’est pas le cas, on peut encore avoir famine.
' $
4 Interblocage
# 32 4.2 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
4.2 Généralités . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
& %
' $
4.1 Introduction
Interblocage (Deadlock) = toute situation telle que deux processus au moins sont
chacun en attente d’une ressource non partageable déjà allouée à l’autre
Exemple : exclusion mutuelle sur deux ressources différentes
Sémaphore mutex1 initialisé à 1
Sémaphore mutex2 initialisé à 1
Prog1 Prog2
# 33 ... ...
P(mutex1) P(mutex2)
|accès à ressource 1 |accès à ressource 2
|P(mutex2) |P(mutex1)
||accès à ressource 2 ||accès à ressource 1
|V(mutex2) |V(mutex1)
V(mutex1) V(mutex2)
... ...
Les deux programmes se bloqueront mutuellement si Prog1 fait P(mutex1) alors
que simultanément Prog2 fait P(mutex2)
& %
TELECOM SudParis — Dominique Bouillet et Michel Simatic — Avril 2016 — module CSC4508/M2 122
Synchronisation entre processus
' $
4.2 Généralités
& %
& %
• Les moniteurs (plus exactement les conditions) C sont étudiées dans le chapitre « Threads ».
• En Java, on a :
TELECOM SudParis — Dominique Bouillet et Michel Simatic — Avril 2016 — module CSC4508/M2 123
Synchronisation entre processus
• Des exemples de code en environnement Windows sont présentés dans l’article « OMG, Multi-Threading
is easier than Networking » (http://www.gamasutra.com/view/feature/4006/sponsored_feature_
omg_.php)
Bibliographie du chapitre
[Downey, 2005] Downey, A. B. (2005). The Little Book of Semaphores. GreenTeaPress, http ://greentea-
press.com/semaphores/. Version 2.1.5.
TELECOM SudParis — Dominique Bouillet et Michel Simatic — Avril 2016 — module CSC4508/M2 124
Threads ou processus légers
module CSC4508/M2
Avril 2016
125
Threads ou processus légers 1 Présentation
' $
Plan du document
1 Présentation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
2 Création/destruction de threads . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
#2 3 Partage des données . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
4 Synchronisation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
5 Utilisation et limitations des threads . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
6 Autres fonctions de la bibliothèque POSIX threads . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
& %
' $
1 Présentation
#3 1.1 Bibliographie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.3 Threads . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.3 Détacher le flot d’exécution des ressources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
& %
TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 126
Threads ou processus légers 1 Présentation
' $
1.1 Bibliographie
« Computer Systems : A Programmer’s Perspective », R. E. Bryant et D. R.
O’Hallaron, Prentice Hall, 2003.
« UNIX SYSTEMS Programming : communication, Concurrency and Threads », K.
A. Robbins et S. Robbins, Prentice Hall, 2003.
« Programmation système en C sous Linux », C. Blaess, Eyrolles, 2000.
« Understanding the Linux kernel, 2nd edition », Daniel P. Bovet et M. Cesati,
#4 O’Reilly, 2003.
Threads spécifiquement
« Pthreads Programming : A POSIX Standard for Better Multiprocessing », B.
Nichols, D. Buttlar et J. P. Farrell, O’Reilly and Associates, 1996.
« Threads Primer : A Guide to Multithreaded Programming », B. Lewis, D. Berg
et B. Lewis, Prentice Hall, 1995.
« Programming With POSIX Threads », D. Butenhof, Addison Wesley, 1997.
« Techniques du multithread : du parallélisme dans les processus », B. Zignin,
Hermès, 1996.
& %
Le cours de Bryant et Hallaron est très bien fait. Il part des couches basses pour expliquer les différentes
notions : les images mémoire des processus pour expliquer la notion de thread, les instructions liées à une
lecture et à une écriture pour expliquer les problèmes de synchronisation avec pour exemple le compteur.
Le livre Robbins et Robbins est très progressif. Il explique notamment le sens de thread-safe avec strtok()
comme exemple (p. 39) et y revient dans un chapitre sur les threads POSIX avec comme exemples strerr()
et errno (p. 432). Les threads sont expliqués sur trois chapitres avec beaucoup d’exemples, notamment des
exemples détaillés de gestion des signaux avec des threads.
Le livre de C. Blaess fournit beaucoup d’explications sur l’implantation des threads sous Linux même s’il
est déjà un peu ancien par exemple ce qui concerne la directive _REENTRANT est déjà obsolète.
' $
1.2 Threads
#5 Programmation concurrente
Utilisation des ressources simultanément : recouvrement du calcul et des
entrées/sorties
Exploitation des architectures multiprocesseurs
I Architectures SMP ou CMP
I Plus généralement des architectures à mémoire partagée
Exploitation de la technologie hyper-threading
& %
TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 127
1 Présentation 1.3 Détacher le flot d’exécution des ressources
' $
1.3 Détacher le flot d’exécution des ressources
& %
' $
1.3.1 Vision traditionnelle d’un processus
Processus mono-thread
Contexte : contexte d’exécution + contexte du noyau
Espace d’adressage : code, données et pile
#7
& %
Sur une machine 32 bits, le processus dispose d’un espace d’adressage s’étendant jusqu’à 3 Go. La taille
de l’espace d’adressage d’un processus est plus importante sur une machine 64 bits, mais les explications
suivantes restent valables. Sur le schéma de cette page, les différents segments sont schématisés en trois zones.
La zone “Code” contient le code et les données en écriture seule, la zone “Données” contient les données en
lecture/écriture et le “Tas” contient les données allouées dynamiquement. La zone “Pile” représente la pile
d’exécution du processus.
Le contexte du processus est représenté divisé en deux. Le contexte d’exécution représente les informations
sur l’état d’exécution du programme : registres, instruction courante et pointeur de pile. Le contexte du noyau
représente les informations sur l’état des ressources nécessaires au processus : structures permettant de gérer
TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 128
1 Présentation 1.3 Détacher le flot d’exécution des ressources
la mémoire, les tables de descripteurs pour accéder aux fichiers, aux ports de communication... et le pointeur
vers la fin du segment de données, indiqué pointeur brk (voir le chapitre sur la mémoire).
Les schémas sont tirés du cours de O’Hallaron et al. à l’université Carnegie Mellon.
' $
1.3.2 Autre vision d’un processus
#8
& %
Cette autre vision du processus détache le flot d’exécution des ressources. Les informations nécessaires au
cours de déroulement du programme c’est-à-dire la pile et le contexte d’exécution sont isolées des informations
concernant les ressources stockant le code, les données et le contexte du noyau. Ceci fait donc apparaître
les informations nécessaires au fil d’exécution, en anglais « thread» et les informations qui pourront être
partagées par plusieurs fils d’exécution.
' $
1.3.3 Processus multi-thread
Processus multi-thread
Plusieurs fils d’exécution
Code, données et contexte du noyau partagés : notamment partage des fichiers
et des ports de communication
#9
& %
Dans le cas d’un processus multi-thread, les différents fils d’exécution caractérisés par leur pile, l’état des
registres et le compteur ordinal se partagent le code, les données, le tas et le contexte du noyau notamment
TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 129
Threads ou processus légers 2 Création/destruction de threads
les tables de descripteurs. Les fichiers et les ports de communication par exemple sont partagés.
La pile d’un processus habituel n’est contrainte que par la limite de la zone nommée tas, dans la-
quelle les variables dynamiques sont allouées. En principe, la pile d’un tel processus pourrait croître
jusqu’à remplir l’essentiel de l’espace d’adressage du programme, soit environ 3 Go. Dans le cas d’un
programme multithread, les différentes piles doivent être positionnées à des emplacements figés dès
la création des threads, ce qui impose des limites de taille puisqu’elles ne doivent pas se rejoindre.
Dans la bibliothèque POSIX, l’adresse et la taille maximum de la pile sont données par : _PO-
SIX_THREAD_ATTR_STACKADDR et _POSIX_THREAD_ATTR_STACKSIZE. La taille minimum
de la pile est définie par PTHREAD_STACK_MIN correspondant à 16 Ko sous Linux. (Voir le “Blaess”
p.286-287)
' $
2 Création/destruction de threads
& %
' $
2.1 Pthread “Hello world”
# 11
TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 130
Threads ou processus légers 2 Création/destruction de threads
Voilà un premier programme multi-thread. Ce programme utilise l’interface POSIX Pthread. Il doit être
compilé avec l’option -pthread de gcc (voir le man de gcc) :
gcc helloworld.c -pthread -o helloworld
#include <pthread.h>
#include <time.h>
#include <error.h>
#include <stdlib.h>
#include <stdio.h>
printf("Hello world\n");
/* ralentissement */
sleeptime.tv_sec = 10;
sleeptime.tv_nsec = 0;
nanosleep(&sleeptime, NULL);
pthread_exit(NULL);
}
rc = pthread_join(thread, NULL);
if(rc)
error(EXIT_FAILURE, rc, "pthread_join");
return EXIT_SUCCESS;
}
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).
TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 131
2 Création/destruction de threads 2.3 Threads POSIX : création/destruction
' $
2.2 Ensemble de threads pairs
# 12
& %
Les threads ne forment pas une arborescence comme les processus. Lorsqu’un processus crée un thread,
on parle alors de thread principal et de thread pair. L’ensemble de threads contient alors deux threads. Un
thread, principal ou non, peut créer un autre thread qui rejoint alors l’ensemble de threads.
' $
2.3 Threads POSIX : création/destruction
# 13 2.3.1 Identification . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
2.3.2 Utilisation. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .15
2.3.3 Attributs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
& %
TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 132
2 Création/destruction de threads 2.3 Threads POSIX : création/destruction
' $
2.3.1 Identification
& %
Création
int pthread_create (pthread_t *thread, pthread_attr_t *attr, void *(*
start_routine) (void *), void *arg)
Terminaison
# 15
void pthread_exit (void *retval)
int pthread_join (pthread_t thread, void **thread_return)
int pthread_detach (pthread_t thread)
Attention : pour la plupart des fonctions de la bibliothèque Threads POSIX, un code
d’erreur non nul est renvoyé en cas de problème mais errno n’est pas
nécessairement positionné.
& %
pthread_create permet de créer un nouveau fil d’exécution pour le processus courant. Pour cela, le
premier paramètre contient l’adresse où sera placé l’identifiant du thread au sortir de la fonction. Le second
paramètre pointe sur les attributs associés au thread dès sa création (cf les transparents « Attributs »). Les
deux derniers paramètres représentent respectivement l’adresse de la fonction devant être exécutée par le
nouveau thread et le paramètre fourni à cette fonction. La valeur retournée est 0 en cas de succès et le code
TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 133
2 Création/destruction de threads 2.3 Threads POSIX : création/destruction
de l’erreur sinon.
Le thread s’exécute jusqu’à la fin de la fonction, sauf s’il effectue un appel à pthread_exit. Dans ce cas,
le fil d’exécution est explicitement interrompu et la valeur passée en paramètre correspond à la valeur de fin
d’exécution du thread. L’objet spécifié est de type void * pour des raisons de généricité.
Les ressources liées à un thread ne sont pas libérées lorsque ce thread se termine sauf s’il s’agit d’un
thread détaché. Pour libérer les ressources liées à un thread non détaché, il faut qu’un autre thread appelle
pthread_join.
pthread_join permet d’attendre la fin de l’exécution d’un thread. Cette fonction bloque jusqu’à ce que
le fil d’exécution associé à l’identifiant du premier paramètre se termine. La valeur de fin d’exécution du
thread est alors copiée à l’emplacement pointé par le second paramètre.
pthread_detach permet de « détacher » un fil d’exécution, c’est-à-dire qu’il n’est pas possible pour
un autre thread de se synchroniser avec lui avec la fonction pthread_join et que ses ressources seront
automatiquement libérées dès qu’il se terminera.
Les threads peuvent aussi se terminer par un mécanisme d’annulation. Tout thread peut deman-
der l’annulation d’un autre thread. Chaque thread contrôle le fait de pouvoir être annulé ou non (voir
pthread_cancel()).
' $
2.3.3 Attributs
& %
Chaque fil d’exécution est doté d’un certain nombre d’attributs, regroupé dans un type opaque
pthread_attr_t. Les attributs sont fixés lors de la création du thread. Lorsque les attributs par défaut
sont suffisants, on passe généralement un pointeur NULL. Un grand nombre de fonctions sont fournies afin
de manipuler les attributs associés aux fils d’exécution. Les deux premières — pthread_attr_init et
pthread_attr_destroy — permettent respectivement d’initialiser et de détruire le lot d’attributs sur lequel
pointe le premier paramètre. Les autres fonctions permettent respectivement de lire (pthread_attr_getX)
et de modifier (pthread_attr_setX) l’état des attributs. Un couple de fonctions est fourni par attribut, le
X représentant le nom de l’attribut. Le premier paramètre est un pointeur sur le lot d’attributs ; tandis que
le second peut être la nouvelle valeur de l’attribut ou l’adresse où sera placée la valeur courante de l’attribut
selon que l’on effectue une mise à jour ou une lecture.
L’attribut detachstate permet de spécifier s’il sera possible de se synchroniser sur le fil d’exécution une
fois qu’il sera terminé. Deux valeurs sont possibles : PTHREAD_CREATE_JOINABLE pour autoriser la synchro-
nisation et PTHREAD_CREATE_DETACHED pour la décliner.
TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 134
Threads ou processus légers
' $
Attributs (2/2)
Politique d’ordonnancement des threads
int pthread_attr_getschedpolicy(const pthread_attr_t *attr, int *policy)
→ policy = SCHED_[OTHER|RR|FIFO]
Paramètres d’ordonnancement
# 17 Priorité d’ordonnancement
int pthread_attr_getschedparam(const pthread_attr_t *attr, const struct
sched_param *param)
' $
int pthread_attr_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)
→ scope = PTHREAD_SCOPE_[SYSTEM|PROCESS]
# 18 Adresse de la pile du thread
int pthread_attr_getstackaddr(const pthread_attr_t *attr, void *addr)
& %
Les attributs schedpolicy, schedparam, scope et inheritsched concernent l’ordonancement des fils
d’exécution. Ils ne sont disponibles que si la constante _POSIX_THREAD_PRIORITY_SCHEDULING est définie
dans unistd.h.
En particulier, l’attribut schedpolicy correspond à la politique d’ordonnancement employée par le thread.
Trois valeurs sont possibles : SCHED_OTHER pour l’ordonnancement classique ; SCHED_RR pour un séquence-
ment temps-réel avec l’algorithme Round Robin ; SCHED_FIFO pour un ordonnancement temps-réel FIFO.
Pour plus de détails, cf. chapitre « Interaction système multi-tâche et processus ».
Les attributs stackaddr et stacksize permettent de configurer la pile utilisée par le fil d’exécution. Ils
permettent respectivement de préciser l’adresse de départ et la taille maximale de la pile.
TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 135
Threads ou processus légers 3 Partage des données
' $
3 Partage des données
& %
' $
3.1 Notion de variable partagée
Variable partagée : deux définitions possibles
Conceptuellement : variable utilisée par plusieurs threads
Techniquement : pendant l’exécution une seule instance de la variable pour tous
les threads
I Variable globale : une seule instance “partagée”
I Variable statique locale : une seule instance “partagée”
I Variable automatique locale : une instance dans chacune des piles des threads
# 20
appelants
Attention : les threads partagent l’intégralité de l’espace d’adressage du processus
Toutes les variables peuvent potentiellement être partagées.
Même les variables automatiques locales peuvent être partagées : à utiliser avec
précaution.
Partage du contexte du noyau
Gestion des flux
Gestion des signaux
& %
Conceptuellement, une variable partagée est une variable utilisée par plusieurs threads. Techniquement,
on appelle variable partagée une variable pour laquelle durant l’exécution du programme, il n’existe qu’une
seule instance. En C, les variables globales et les variables locales statiques ne sont allouées qu’une seule
fois pour l’ensemble des threads, sur le tas. Donc il n’en existe qu’une seule instance pendant l’exécution
du programme. En revanche, les variables automatiques locales à une fonction sont allouées dans la pile de
chaque thread appelant cette fonction. Donc il existe plusieurs instances de ces variables pendant l’exécution
du programme.
Attention : les threads partagent l’intégralité de l’espace d’adressage du processus ce qui signifie que
TOUTES les variables peuvent être partagées. MAIS généralement, on utilise des variables globales, visibles
dans l’ensemble des fonctions utiles, pour définir des variables partagées.
Voir Briant et O’Hallaron.
TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 136
Threads ou processus légers 3 Partage des données
' $
3.2 Partage non-intentionnel des données
Lorsqu’une même instance de variable est utilisée par plusieurs threads alors que
# 21 conceptuellement ce n’est pas une variable partagée
Peut provoquer des situations de compétition (race condition)
I Le résultat varie selon les conditions d’exécution
Erreurs difficiles à détecter !
& %
/*
* race.c: code faux (poly Briant et Hallaron)
*
* sans temporisation, semble s’executer correctement!
* en ajoutant une temporisation, on voit l’erreur.
*/
#include <pthread.h>
#include <error.h>
#include <stdlib.h>
#include <stdio.h>
me = *((int *)arg);
printf("Hello from %d\n", me);
pthread_exit(NULL);
}
rc = pthread_join(threads[i], NULL);
if(rc)
error(EXIT_FAILURE, rc, "pthread_join");
}
exit(EXIT_SUCCESS);
}
TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 137
Threads ou processus légers 3 Partage des données
L’exécution du programme race.c affiche le numéro du thread créé en passant en paramètre l’adresse
de l’indice de la boucle. Une seule instance de l’indice de boucle est utilisée par tous les threads donc en
fonction de l’instant où l’affichage s’effectue, le résultat est différent. Le code est donc faux. Il s’agit d’une
race en anglais (situation de compétition en français), c’est-à-dire que le résultat de l’exécution dépend du
fait qu’un thread atteint un point x d’exécution avant qu’un autre thread atteigne un point y (voir le Briant
et O’Hallaron).
Les exécutions semblent correctes mais en ajoutant une temporisation d’une seconde avant stockage de
la valeur, l’erreur est bien visible : exécution de raceTempo.c.
De manière générale, l’utilisation de threads, il est nécessaire d’exécuter plusieurs fois le même code
simultanément. En effet, sur une machine multiprocesseur, il est possible que pendant l’exécution du code
par un processeur, un autre processeur commence l’exécution de ce même code. Il faut donc vérifier que
dans ce cas, les exécutions se déroulent correctement. Sur une machine monoprocesseur, le problème se
pose aussi. En effet, dans la plupart des systèmes d’exploitation, lorsqu’un processus est en cours, il peut
être interrompu pour exécuter un autre processus. De plus, les fonctions récursives sont aussi un exemple
d’exécutions simultanées du même code.
& %
On trouve dans la littérature deux termes différents : réentrant et thread-safe. Pour certains, ils sont
équivalents. Pour d’autres, bien que ces notions soient liées à la façon de gérer les ressources , ce sont deux
notions différentes. Une fonction thread-safe produit un résultat correct lors d’une exécution multithread.
Une fonction réentrante n’utilise pas de variables partagées lors d’une exécution multithread (voir le Briant
et O’Hallaron). Une fonction peut être soit réentrante, soit thread-safe, soit les deux, soit ni l’un ni l’autre.
Une fonction réentrante ne conserve pas d’état entre deux appels successifs ; elle ne retourne pas non
plus de pointeur sur des données statiques. Toutes les données sont fournies par l’appelant. Une fonction
réentrante ne doit pas appeler non plus de fonction non-réentrante.
Le standard POSIX spécifie que toutes les fonctions nécessaires, notamment celles de la bibliothèque C,
doivent être implantées de façon à pouvoir être exécutées par plusieurs threads simultanément. Certaines
fonctions, néanmoins, échappent à cette spécification parmi lesquelles dirname, getenv, gethostbyname,
gethostbyadddr, rand, readdir, setenv, putenv, strerror, strtok... (Voir le Robbins et Robbins p.432)
Pour l’ensemble de ces fonctions, une version réentrante doit être implantée désignée avec le suffixe _r.
Une fonction non-réentrante peut souvent, mais pas toujours, être identifiée par son interface externe et
son utilisation. Par exemple, en C, la primitive strtok n’est pas réentrante parce qu’elle conserve la chaîne
de caractères pour la diviser en éléments. La primitive ctime n’est pas non plus réentrante ; elle retourne un
TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 138
Threads ou processus légers 3 Partage des données
pointeur sur des données statiques qui est écrasé à chaque appel. Il faut alors utiliser les versions ré-entrantes
de ces primitives, par exemple strtok_r.
Une fonction thread-safe protège les données partagées (variables mais aussi écriture dans des fichiers par
exemple) des accès concurrents par des verrous. La notion de thread-safe ne concerne que l’implantation de
la fonction et non son interface externe.
Une fonction thread-safe non-réentrante est souvent moins performante qu’une fonction réentrante puis-
qu’il a été nécessaire d’ajouter des synchronisations pour la rendre thread-safe.
Dans une application multi-thread, toutes les fonctions appelées par plusieurs threads, doivent être thread-
safe. Il faut remarquer aussi que la plupart du temps, les fonctions non-réentrantes ne sont pas thread-safe
mais que les rendre réentrantes les rend aussi thread-safe.
Les bibliothèques réentrantes et thread-safe sont utiles dans tous les environnements de programmation
parallèles et asynchrones et pas seulement pour des threads.
errno est par défaut une variable globale externe. Cette implantation ne peut pas fonctionner avec des
threads. En effet, le résultat de la lecture de errno n’est pas correct puisqu’un autre thread peut l’avoir
modifiée. Dans le cas de threads, errno est propre à chaque thread. La bibliothèque pthread redéfinit la
fonction _errno_location() (voir bits/errno.h) pour fournir une adresse propre à chaque thread. Par
défaut, pour un processus monothread, _errno_location() renvoie l’adresse de la variable globale errno.
/*
* Exemple de code non-réentrant
* strtok() maintient un état persistant entre les appels
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <error.h>
#include <pthread.h>
#define MAXSIZE 256
pthread_exit(NULL);
}
pthread_join(thread, NULL);
TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 139
Threads ou processus légers 3 Partage des données
if (rc)
error(EXIT_FAILURE, rc, "pthread_join");
return EXIT_SUCCESS;
}
Dans les programmes fournis, deux threads, le thread principal et le thread pair, analysent chacun une
chaîne de caractères différente pour la décomposer en élément.
Le programme str.c utilise la fonction strtok(). Cette fonction découpe une chaîne de caractères
en plusieurs éléments, retournés successivement par différents appels à strtok(). Entre chaque appel à
strtok(), un pointeur vers l’élément suivant de la chaîne est maintenu par l’intermédiaire d’une variable
statique à l’intérieur de la fonction strtok().
Le résultat du programme dépend donc de l’ordre des appels à strtok(). Dans notre cas, le premier
appel est effectué par le thread principal. Ensuite le thread pair est créé. Si le thread pair effectue alors un
appel à strtok(), à l’appel suivant, le thread principal renverra un résultat correspondant à la chaîne du
thread pair et non à la sienne !
Pour corriger ce problème et faire en sorte que le résultat ne dépende pas de l’ordonnancement des
exécutions des threads, il faut utiliser la fonction strtok_r(). Cette fonction permet au programme appelant
de fournir lui-même la zone de mémoire permettant de conserver un pointeur vers l’élément suivant (en
troisième argument de la fonction).
/*
* Attention le buffer en entree, phrase, est modifie
* par l’appel a strotk donc il ne sert a rien d’appeler
* plusieurs fois strtok sur le meme buffer!
*
* appel a strtok_r
*
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <error.h>
#include <pthread.h>
#define MAXSIZE 256
pthread_exit(NULL);
}
TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 140
Threads ou processus légers 3 Partage des données
return EXIT_SUCCESS;
}
Destruction
int pthread_key_delete ( pthread_key_t cle)
Utilisation
int pthread_setspecific (pthread_key_t cle, const void *pointeur)
void * pthread_getspecific( pthread_key_t cle)
Ceci peut utilement être utilisé avec pthread_once
& %
Lorsqu’une valeur doit être conservée d’un appel de fonction à l’autre, elle peut être déclarée statique ou
globale. Elle est alors commune à l’ensemble des threads. Il n’est pas non plus possible de la stocker dans la
pile. On utilise alors les « données privées ».
Une clé (de type pthread_key_t) doit être associée à chaque donnée privée et peut résider en variable
statique. La bibliothèque associe la clé avec un pointeur générique différent pour chaque thread.
pthread_key_create initialise la clé dont l’adresse est passée en premier argument ; le second argument
est l’adresse de la fonction qui sera appelée lors de la destruction de la clé.
pthread_key_delete détruit la clé pointée par le paramètre.
pthread_setspecific associe la clé passée en premier argument aux données personnelles composant le
second argument. Le second argument est typiquement un tableau dont chaque élément sera utilisé par un
thread différent. Cette opération ne doit être réalisée qu’une seule fois, quel que soit le nombre de threads.
pthread_getspecific retourne l’adresse de la donnée personnelle associée au thread pour la clé passée en
paramètre.
/*
specific.c
*/
#include <error.h>
#include <unistd.h>
TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 141
Threads ou processus légers
#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>
return val;
}
/* doit etre appelee par chaque thread, le buffer en arg doit etre
different */
pthread_setspecific(cleUnique, malloc(sizeof(int)));
set_buffer(cleUnique, 1);
maval = get_buffer(cleUnique);
printf("thread1: maval = %d\n", *maval);
pthread_exit(EXIT_SUCCESS);
}
/* doit etre appelee par chaque thread, le buffer en arg doit etre
different */
pthread_setspecific(cleUnique, malloc(sizeof(int)));
set_buffer(cleUnique, 2);
maval = get_buffer(cleUnique);
rc = pthread_join(thread1, NULL);
if (rc)
error(EXIT_FAILURE, rc, "pthread_join");
maval = get_buffer(cleUnique);
printf("thread maitre: maval = %d\n", *maval);
pthread_key_delete(cleUnique);
pthread_exit(EXIT_SUCCESS);
}
Afin de simplifier l’utilisation de variables TLS, certains compilateurs (GCC, Intel CC, etc.) permettent
l’utilisation du mot-clé __thread lors de la déclaration d’une variable. Par exemple :
Les variables TLS sont stockées dans la section .tdata de l’espace d’adressage. Lorsqu’un nouveau
thread est créé, la section .tdata est recopiée, permettant ainsi d’allouer toutes les variables TLS du
thread. De plus amples explications sur l’implémentation de variables TLS sous Linux sont disponibles dans :
TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 142
Threads ou processus légers 4 Synchronisation
Drepper, U. Elf handling for thread-local storage. Technical report, Red Hat, Inc., 2003.
' $
4 Synchronisation
4.2 Synchronisation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
# 24 4.2 Exclusions mutuelles. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .27
4.3 Sémaphores POSIX (rappel) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
4.4 Attente de conditions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
& %
' $
4.1 Synchronisation
& %
Les threads doivent synchroniser leurs activités pour interagir. Ceci inclut les synchronisations implicites
par la modification de données partagées et les synchronisations explicites informant les autres des événements
qui se sont produits.
Le programme compteurBOOM.c est un exemple de mauvaise utilisation d’une donnée partagée, la variable
compteur. Deux threads incrémentent simultanément cette variable un nombre de fois identiques et n’arrivent
pas au même résultat.
Il est donc nécessaire d’introduire les fonctions P et V pour synchroniser les différents threads (voir le
chapitre Synchronisation entre processus).
TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 143
Threads ou processus légers 4 Synchronisation
Type : pthread_mutex_t
Création
# 26 int pthread_mutex_init (pthread_mutex_t *mutex, const
pthread_mutexattr_t *mutexattr)
pthread_mutex_init retourne toujours 0.
Destruction
int pthread_mutex_destroy (pthread_mutex_t *mutex)
& %
Les variables globales et les descripteurs de fichiers (et d’autres choses encore) étant partagés par l’en-
semble des threads d’un processus, un mécanisme dit d’« exclusion mutuelle » est fourni afin d’assurer que cer-
taines opérations seront synchronisées. Pour cela, on utilise un « mutex » dont le type est pthread_mutex_t.
L’initialisation d’un mutex peut s’effectuer soit de manière statique (en assignant la va-
leur PTHREAD_MUTEX_INITIALIZER au mutex), soit de manière dynamique (à l’aide de la fonction
pthread_mutex_init). Dans le cas dynamique, le premier paramètre est un pointeur sur le mutex et le
second est un pointeur sur les attributs que l’on veut associer au mutex (une valeur nulle associe les attri-
buts par défaut).
pthread_mutex_destroy permet de détruire un mutex (non verrouillé) en précisant l’adresse du mutex.
' $
Exclusions mutuelles (2/2)
Utilisation
int pthread_mutex_lock (pthread_mutex_t *mutex)
int pthread_mutex_unlock (pthread_mutex_t *mutex)
# 27
int pthread_mutex_trylock (pthread_mutex_t *mutex)
Attributs
Les attributs associés aux MUTEX ne sont pas portables
Il convient de ne pas trop les utiliser
Ou de se référer à la documentation en ligne
& %
TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 144
Threads ou processus légers 4 Synchronisation
pthread_mutex_lock permet de prendre le mutex passé en paramètre. Si le mutex est disponible, il est
pris de suite ; sinon, le thread se bloque jusqu’à obtenir le mutex.
pthread_mutex_unlock libère le mutex. Le thread libérant le mutex doit en être le possesseur.
pthread_mutex_trylock permet de savoir s’il est possible de prendre le mutex. Ceci ne garantit en rien
que le mutex sera toujours libre lors du prochain appel à pthread_mutex_lock.
Il est à noter qu’à ce jour la norme POSIX ne prévoit aucun attribut pour les mutex (des attri-
buts existent dans certaines implantations, en particulier les mutex récursifs sous Linux, voir les fonctions
pthread_mutexattr_init, pthread_mutexattr_destroy...).
/*
* compteurMutex.c
*
* Acces au compteur protege par mutex
* mutex_init ne renvoie pas de code d’erreur
*
*/
#include <unistd.h>
#include <error.h>
#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>
/* INT_MAX / 2 */
#define NBITER 1000000000
int compteur = 0;
pthread_mutex_t mutex;
compteur ++;
rc = pthread_mutex_unlock(&mutex);
if(rc)
error(EXIT_FAILURE, rc, "pthread_mutex_unlock");
}
pthread_exit(NULL);
}
pthread_mutex_init(&mutex, NULL);
rc = pthread_join(thread1, NULL);
if(rc)
error(EXIT_FAILURE, rc, "pthread_join");
rc = pthread_join(thread2, NULL);
if(rc)
error(EXIT_FAILURE, rc, "pthread_join");
if (compteur != 2 * NBITER)
printf("BOOM! compteur = %d\n", compteur);
else
printf("OK compteur = %d\n", compteur);
rc = pthread_mutex_destroy(&mutex);
if (rc)
error(EXIT_FAILURE, rc, "pthread_mutex_destroy");
exit(EXIT_SUCCESS);
TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 145
Threads ou processus légers 4 Synchronisation
' $
4.3 Sémaphores POSIX (rappel)
Création et destruction
int sem_init ( sem_t *sem, int pshared, u_int value)
int sem_destroy ( sem_t *sem)
# 28 Utilisation
int sem_wait ( sem_t *sem)
int sem_post ( sem_t *sem)
int sem_trywait ( sem_t *sem)
int sem_getvalue ( sem_t *sem, int *sval)
Toutes ces fonctions renvoient -1 en cas de problème et positionnent errno.
& %
Remarque : les sémaphores POSIX sont également abordée dans le chapitre « Communications inter-
processus ».
Les sémaphores POSIX font partie de la bibliothèque des threads Linux si la constante
_POSIX_SEMAPHORES est définie dans unistd.h ; les fonctionnalités sont alors définies dans semaphore.h.
Ils sont sensiblement différents des sémaphores IPC System V. Le type associé aux sémaphores POSIX est
sem_t.
sem_init permet d’initialiser le sémaphore dont l’adresse constitue le premier paramètre et la valeur le
dernier. Le deuxième paramètre indique si le sémaphore peut être partagé par plusieurs processus .
sem_destroy détruit le sémaphore passé en argument.
sem_wait attend que le sémaphore passé en argument soit libre pour le prendre.
sem_post libère le sémaphore passé en argument.
sem_trywait renvoie -1 (et place la valeur EAGAIN dans errno) si le sémaphore est déjà pris ; elle renvoie
0 dans le cas contraire.
sem_getvalue place la valeur courante du sémaphore à l’adresse passé en second argument.
/**
** semaphore.c
**
** synchronisation de deux threads: utilisation d’un semaphore
**
** exemple d’un schéma producteur/consommateur
**
** Le thread principal écrit un caractère dans une zone de mémoire commune.
** Un deuxième thread le lit.
**
** sem_init, sem_wait, sem_post et sem_destroy renvoient -1 en cas de
** probleme et positionnent errno.
**
**/
#include <pthread.h>
#include <semaphore.h>
#include <error.h>
#include <stdlib.h>
#include <stdio.h>
char buffer;
sem_t placeDispo;
sem_t infoPrete;
void production()
{
TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 146
Threads ou processus légers 4 Synchronisation
buffer = ’a’;
void consommation()
{
printf("consommateur: <0x%x> item = %c \n", (unsigned int) pthread_self(), buffer);
}
/* 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;
}
/* P(infoPrete) */
rc = sem_wait(&infoPrete);
if (rc)
{
perror("sem_wait");
exit(EXIT_FAILURE);
}
consommation( buffer );
/* V(placeDispo) */
rc = sem_post(&placeDispo);
if (rc)
{
perror("sem_post");
exit(EXIT_FAILURE);
}
pthread_exit(NULL);
}
int main()
{
int rc;
pthread_t lecteur;
sem_init(&placeDispo, 0, 1);
sem_init(&infoPrete, 0, 0);
ecrire(NULL);
rc = pthread_join(lecteur, NULL);
if(rc)
error(EXIT_FAILURE, rc, "pthread_join");
rc = sem_destroy(&placeDispo);
if (rc)
{
perror("sem_destroy");
exit(EXIT_FAILURE);
}
rc = sem_destroy(&infoPrete);
TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 147
Threads ou processus légers 4 Synchronisation
if (rc)
{
perror("sem_destroy");
exit(EXIT_FAILURE);
}
return EXIT_SUCCESS;
}
' $
4.4 Attente de conditions
Principe
Un premier thread se met en attente d’un condition
Lorsqu’un second thread réalise la condition, il émet un signal à destination de la
condition, réveillant un thread en attente
I Si aucun thread n’est en attente, rien ne se passe
I Si plusieurs threads sont en attente, un thread est réveillé
# 29 Type : pthread_cond_t
Création
int pthread_cond_init (pthread_cond_t *cond, const pthread_condattr_t
*cond_attr)
pthread_cond_init retourne toujours 0.
Destruction
int pthread_cond_destroy ( pthread_cond_t *cond)
Notes : il n’existe pas d’attribut pour les conditions
& %
Lorsqu’un fil d’exécution doit patienter jusqu’à ce qu’un événement survienne dans un autre fil d’exé-
cution, on peut utiliser une autre technique de synchronisation : les « conditions » dont le type est
pthread_cond_t. Le principe est le suivant : un premier thread se met en attente d’une condition devant
être réalisée ; lorsqu’un second thread réalise la condition, il émet un signal à destination de la condition qui
réveille le thread en attente. Si aucun thread n’est en attente, rien ne se passe ; si plus d’un thread est en
attente, l’un d’eux est réveillé, mais on ne peut déterminer lequel.
Une condition peut être initialisée soit statiquement en assignant la valeur définie par la macro
PTHREAD_COND_INITIALIZER, soit dynamiquement en réalisant un appel à pthread_cond_init. Cette fonc-
tion nécessite deux paramètres : le premier est un pointeur sur la condition à initialiser ; le second est
un pointeur les attributs associés à la condition (ou la valeur nulle si on désire les attributs par défaut).
pthread_cond_destroy détruit la condition sur laquelle pointe le paramètre. Il est à noter que la biblio-
thèque de threads Linux n’implémente aucun attribut pour les conditions.
TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 148
Threads ou processus légers 4 Synchronisation
' $
Attente de conditions (2/2)
Utilisation
int pthread_cond_signal ( pthread_cond_t * cond)
int pthread_cond_wait (pthread_cond_t *cond, pthread_mutex_t *mutex)
int pthread_cond_timedwait (pthread_cond_t *cond, pthread_mutex_t
# 30
*mutex, const struct timespec *abstime)
int pthread_cond_broadcast ( pthread_cond_t * cond)
Les fonctions pthread_cond_signal, pthread_cond_wait et
pthread_cond_broadcast renvoient toujours 0.
Notes
Toujours prendre le mutex associé à la condition avant et le relâcher ensuite.
I Pas d’interblocage entre le thread se plaçant en attente de la condition et
celui la réalisant.
& %
' $
I pthread_cond_wait libére le mutex puis se met en attente de la condition.
Une fois que la condition est réalisée, la fonction bloque à nouveau le mutex
avant de sortir.
pthread_cond_wait peut se terminer même si la condition n’a pas été réalisée.
# 31
& %
TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 149
Threads ou processus légers 4 Synchronisation
le relâcher ensuite. Il n’y a pas d’interblocage entre le fil d’exécution se plaçant en attente de la condition
et celui la réalisant. En effet, la fonction pthread_cond_wait commence par libérer le mutex puis se met en
attente de la condition. Une fois que la condition est réalisée, la fonction bloque à nouveau le mutex avant
de sortir.
/**
** conditionUn.c
**
** synchronisation de deux threads: utilisation d’une variable condition
**
** exemple d’un schéma producteur/consommateur
**
** Le thread principal écrit un caractère dans une zone de mémoire commune.
** Un deuxième thread le lit.
**
** mutex_init, cond_init, cond_signal, cond_broadcast et cond_wait ne
** renvoient pas de code d’erreur
**
**/
#include <pthread.h>
#include <error.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
pthread_cond_t condPlaceDispo;
pthread_cond_t condInfoPrete;
int infoPrete = 0;
void production()
{
printf("producteur: <0x%x> buffer = %c \n", (unsigned int) pthread_self(), buffer);
}
void comsommation()
{
printf("consommateur: <0x%x> buffer = %c \n", (unsigned int) pthread_self(), buffer);
}
/* P(placeDispo) */
rc = pthread_mutex_lock(&mutex);
if(rc)
error(EXIT_FAILURE, rc, "pthread_mutex_lock");
rc = pthread_mutex_unlock(&mutex);
if(rc)
error(EXIT_FAILURE, rc, "pthread_mutex_unlock");
production();
/* V(infoPrete) */
rc = pthread_mutex_lock(&mutex);
if(rc)
error(EXIT_FAILURE, rc, "pthread_mutex_lock");
infoPrete = 1;
pthread_cond_signal(&condInfoPrete);
rc = pthread_mutex_unlock(&mutex);
if(rc)
error(EXIT_FAILURE, rc, "pthread_mutex_unlock");
return NULL;
}
/* P(infoPrete) */
TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 150
Threads ou processus légers 4 Synchronisation
rc = pthread_mutex_lock(&mutex);
if(rc)
error(EXIT_FAILURE, rc, "pthread_mutex_lock");
comsommation( buffer );
/* V(placeDispo) */
rc = pthread_mutex_lock( &mutex);
if(rc)
error(EXIT_FAILURE, rc, "pthread_mutex_lock");
infoPrete = 0;
pthread_cond_signal(&condPlaceDispo);
rc = pthread_mutex_unlock(&mutex);
if(rc)
error(EXIT_FAILURE, rc, "pthread_mutex_unlock");
pthread_exit(NULL);
}
int main()
{
int rc;
pthread_t lecteur;
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&condPlaceDispo, NULL);
pthread_cond_init(&condInfoPrete, NULL);
ecrire(NULL);
rc = pthread_join(lecteur, NULL);
if(rc)
error(EXIT_FAILURE, rc, "pthread_join");
rc = pthread_mutex_destroy(&mutex);
if(rc)
error(EXIT_FAILURE, rc, "pthread_mutex_destroy");
rc = pthread_cond_destroy(&condPlaceDispo);
if(rc)
error(EXIT_FAILURE, rc, "pthread_cond_destroy");
rc = pthread_cond_destroy(&condInfoPrete);
if(rc)
error(EXIT_FAILURE, rc, "pthread_cond_destroy");
return EXIT_SUCCESS;
}
/**
** conditionMult.c
**
** synchronisation de deux threads: utilisation d’une variable condition
**
** exemple d’un schéma producteur/consommateur
**
** Le thread principal écrit caractère a caractere dans une zone de mémoire commune.
** Un deuxième thread lit caractere a caractere.
**
** mutex_init, cond_init, cond_signal, cond_broadcast et cond_wait ne
** renvoient pas de code d’erreur
**
**/
#include <pthread.h>
#include <error.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 151
Threads ou processus légers 4 Synchronisation
#define NBCHARS 26
char buffer = ’a’ - 1;
pthread_mutex_t mutex;
pthread_cond_t condPlaceDispo;
pthread_cond_t condInfoPrete;
int infoPrete = 0;
void production()
{
buffer++;
void comsommation()
{
printf("consommateur: <0x%x> buffer = %c \n", (unsigned int) pthread_self(), buffer);
}
int i;
for (i = 0; i < NBCHARS; i++)
{
int rc;
/* P(placeDispo) */
rc = pthread_mutex_lock(&mutex);
if(rc)
error(EXIT_FAILURE, rc, "pthread_mutex_lock");
rc = pthread_mutex_unlock(&mutex);
if(rc)
error(EXIT_FAILURE, rc, "pthread_mutex_unlock");
production();
/* V(infoPrete) */
rc = pthread_mutex_lock(&mutex);
if(rc)
error(EXIT_FAILURE, rc, "pthread_mutex_lock");
infoPrete = 1;
pthread_cond_signal(&condInfoPrete);
rc = pthread_mutex_unlock(&mutex);
if(rc)
error(EXIT_FAILURE, rc, "pthread_mutex_unlock");
return NULL;
}
rc = pthread_mutex_lock( &mutex);
if(rc)
error(EXIT_FAILURE, rc, "pthread_mutex_lock");
/* P(infoPrete) */
comsommation( buffer );
infoPrete = 0;
/* V(placeDispo) */
pthread_cond_signal(&condPlaceDispo);
TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 152
Threads ou processus légers 5 Utilisation et limitations des threads
rc = pthread_mutex_unlock(&mutex);
if(rc)
error(EXIT_FAILURE, rc, "pthread_mutex_unlock");
pthread_exit(NULL);
}
int main()
{
int rc;
pthread_t lecteur;
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&condPlaceDispo, NULL);
pthread_cond_init(&condInfoPrete, NULL);
ecrire(NULL);
rc = pthread_join(lecteur, NULL);
if(rc)
error(EXIT_FAILURE, rc, "pthread_join");
rc = pthread_mutex_destroy(&mutex);
if(rc)
error(EXIT_FAILURE, rc, "pthread_mutex_destroy");
rc = pthread_cond_destroy(&condPlaceDispo);
if(rc)
error(EXIT_FAILURE, rc, "pthread_cond_destroy");
rc = pthread_cond_destroy(&condInfoPrete);
if(rc)
error(EXIT_FAILURE, rc, "pthread_cond_destroy");
return EXIT_SUCCESS;
}
' $
5 Utilisation et limitations des threads
& %
TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 153
Threads ou processus légers 5 Utilisation et limitations des threads
' $
5.1 Utilisation des threads
# 34
& %
Programmer une application interactive avec des threads peut permettre à un programme de continuer à
s’exécuter même si une partie de l’application est bloquée ou est en train d’effectuer une opération coûteuse.
Cela permet donc d’accroître l’interactivité de l’application.
Les architectures maître/esclave, diviser pour régner et producteurs/consommateurs sont des exemples
d’architectures pouvant être facilement implantées avec des threads. Ces architectures mènent toutes à des
programmes modulaires efficacement implantés par des threads.
Dans le cas d’une architecture maître/esclave, une entité maître reçoit la ou les requêtes et crée les
entités esclaves pour les exécuter. Le maître contrôle, par exemple, le nombre d’esclaves existants et ce que
fait chaque esclave. Un esclave s’exécute indépendamment des autres esclaves.
Un gestionnaire d’impressions est un exemple d’architecture maître/esclaves. Un gestionnaire d’impres-
sions gère plusieurs imprimantes, s’assure que toutes les requêtes d’impression reçues sont effectuées en un
temps raisonnable. Quand le gestionnaire reçoit une requête, l’entité maître choisit une imprimante et de-
TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 154
Threads ou processus légers 5 Utilisation et limitations des threads
mande à une entité esclave d’effectuer l’impression sur une imprimante donnée. Chaque esclave peut effectuer
une impression à un instant donné sur une imprimante. Le gestionnaire est chargé de traiter les suppressions
des requêtes d’impressions.
Dans le modèle diviser pour régner, les entités effectuent les tâches en parallèle, indépendamment les
unes des autres. Il n’y a pas d’entité maître.
Un exemple de modèle diviser pour régner serait d’exécuter une commande grep parallèle. La commande
grep établit tout d’abord un ensemble de fichiers à examiner. Elle crée ensuite un ensemble d’entités. Chaque
entité traite un fichier et effectue la recherche du schéma envoyant le résultat sur une sortie commune. Quand
une entité termine sa recherche dans le fichier, elle traite un autre fichier ou s’arrête.
Le modèle producteur/consommateur représente typiquement une chaîne de production (modèle pipe-
liné).
Voilà quelques exemples d’application pouvant tirer profit d’une implantation multi-thread. Un navigateur
web peut avoir un thread affichant les images et le texte pendant qu’un autre thread récupère les données
sur le réseau. Un traitement de texte peut avoir un thread gérant l’interface graphique, un deuxième thread
gérant les entrées de l’utilisateur par l’intermédiaire du clavier et enfin un troisième thread pour faire les
vérifications grammaticales et orthographiques.
Certaines applications ont besoin d’effectuer plusieurs fois les mêmes tâches. Typiquement, un serveur
web accepte des requêtes clientes. Ces requêtes demandent des pages web, des images, des sons, ... Un
serveur peut devoir servir beaucoup de requêtes clientes simultanément. Si ce serveur web est un processus
mono-thread, il ne pourra servir qu’une requête à la fois. Une solution est d’implanter le serveur comme un
processus multi-thread. Un thread est chargé de recevoir les requêtes. Lorsque ce thread reçoit une requête,
il crée un autre thread pour traiter cette requête. Une amélioration de cette solution consiste à utiliser un
ensemble (pool) de threads créés au préalable.
Si vous êtes un programmeur et que vous voulez tirer profit d’une implantation multi-thread, il faut
identifier les parties du programme qui devraient et celles qui ne devraient pas être multi-thread. Voilà un
certain nombre de bonnes questions :
• Y-a-t-il des opérations coûteuses ne dépendant pas du CPU (dessin d’une fenêtre, impression d’un
document, réponse à un clic de souris, calcul d’une colonne de feuille de calcul, gestion de signaux, ...) ?
• Les tâches peuvent-elles être réparties en plusieurs ? par exemple un thread pour la gestion des signaux,
un autre pour l’interface graphique, ...
TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 155
Threads ou processus légers
' $
5.2 Limitations des threads
Ressources que l’on ne souhaite pas partager
ID utilisateur, groupe
droits
quotas d’utilisation de ressources : nombre maximal de fichiers ouverts par un
processus, ...
Corruption des ressources partagées en mémoire
Mort d’un thread → mort de l’application entière
# 35
Problématiques
Exécution d’un fork
I Duplication de tous les threads ou nouveau processus mono-thread
Gestion de signaux : à qui envoyer le signal ?
I Au thread auquel il s’applique ?
I À tous les threads du processus ?
I À certains threads ?
I À un thread spécifique qui se chargera de le gérer correctement ?
Éviter de communiquer par signaux dans une application multi-thread !
& %
Programmer une application avec des threads est utile pour implanter des applications utilisant plusieurs
entités indépendantes. Néanmoins, dans certains cas, il est préférable d’utiliser plusieurs processus.
Beaucoup de ressources sont gérées par les systèmes d’exploitation au niveau du processus. Par exemple,
les identifiants d’utilisateurs et de groupe et les permissions qui leur sont associées sont gérés au niveau du
processus. Les programmes qui ont besoin d’affecter un utilisateur différent aux différentes entités de leur
programme utiliseront plusieurs processus plutôt qu’un seul processus possédant plusieurs threads. D’autre
part, les attributs du système de fichiers tels que le répertoire de travail courant ou le nombre maximal
de fichiers ouverts sont aussi partagés par tous les threads appartenant à un même processus. Ainsi, une
application ayant besoin de gérer ces attributs de manière indépendante utilisera plusieurs processus. Dans
un programme multi-thread, les sémantiques de fork() et de exec() peuvent être modifiées. Si un thread
appelle fork() à l’intérieur d’un programme, ce nouveau processus doit-il dupliquer tous les threads ? Ce
nouveau processus doit-il être mono-thread ?
Dans Linux, lorsqu’un thread appelle un fork(), le processus entier est dupliqué y compris les zones de
mémoire partagées avec les autres threads. Par contre, il n’y a dans le processus fils qu’un seul fil d’exécution,
celui du thread ayant invoqué fork(). Donc il faut faire attention aux ressources utilisées par les autres threads
sauf s’il y a appel à exec(). Les ressources allouées dynamiquement existeront dans le nouveau processus
mais ne pourront pas être libérées et les ressources verrouillées ne pourront pas être déverrouillées (voir le
Blaess p. 291)
Le traitement d’un signal dépend du type de signal. Un signal synchrone (accès illégal à la mémoire,
division par zéro) doit être envoyé au thread concerné et non aux autres. Pour les signaux asynchrones, la
situation n’est pas claire. Certains signaux asynchrones comme Control-C doivent être envoyés à tous les
threads du processus. Certaines implantations d’UNIX permettent à un thread de spécifier quel signal il
recevra et quel signal il bloquera. Cependant, les signaux ne doivent être gérés qu’une seule et unique fois.
C’est pourquoi, le signal n’est souvent envoyé qu’au premier thread qui ne bloque pas le signal. Solaris 2
implante la quatrième solution, un thread spécifique gère tous les signaux.
TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 156
Threads ou processus légers 6 Autres fonctions de la bibliothèque POSIX threads
' $
6 Autres fonctions de la bibliothèque POSIX threads
# 36 6.1 Annulation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
6.2 Nettoyage des ressources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
6.3 Initialisation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
& %
' $
6.1 Annulation
& %
Un thread peut vouloir annuler un autre thread. Il envoie alors une demande d’annulation par l’inter-
médiaire de la fonction pthread_cancel. Le thread annulé se termine comme s’il avait lui-même invoqué la
fonction pthread_exit. La valeur 0 est retournée en cas de succès ; dans le cas contraire, un code d’erreur
est renvoyé.
Le thread récepteur peut accepter la requête, la refuser ou la repousser jusqu’à atteindre un « point
d’annulation ». Ceci est particulièrement intéressant si le code exécuté par le thread est sensible (comme la
manipulation des sémaphores par exemple).
pthread_setcancelstate permet de préciser si la prochaine requête d’annulation sera prise en compte
(PTHREAD_CANCEL_ENABLE) ou non (PTHREAD_CANCEL_DISABLE). Le second paramètre permet de récupérer
l’état précédent.
pthread_setcanceltype précise si les requêtes d’annulation sont prises en compte, le comportement du
TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 157
Threads ou processus légers 6 Autres fonctions de la bibliothèque POSIX threads
/*
cancel.c
*/
#include <assert.h>
#include <error.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>
/* pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &old_value); */
rc = pthread_cancel(thread1);
if (rc)
error(EXIT_FAILURE, rc, "pthread_cancel");
rc = pthread_cancel(thread1);
if (rc)
error(EXIT_FAILURE, rc, "pthread_cancel");
TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 158
Threads ou processus légers 6 Autres fonctions de la bibliothèque POSIX threads
pthread_exit(EXIT_SUCCESS);
}
' $
Annulation (2/2)
& %
Il existe deux types de point d’annulation. Les points d’annulation explicites sont précisés par le program-
meur avec la fonction pthread_testcancel. Les points d’annulation implicites correspondent en général à
des fonctions pouvant attendre un événement indéfiniment. La liste des points d’annulation dépend des sys-
tèmes (et surtout de leur implantation !). Se reporter à la documentation (GNU en particulier) pour plus
d’information.
' $
6.2 Nettoyage des ressources
Deux routines
void pthread_cleanup_push (void (* routine) (void *), void *arg)
# 39 void pthread_cleanup_pop (int execute)
I execute à 0 : fonction supprimée de la pile mais non exécutée
I execute à 1 : fonction supprimée de la pile ET exécutée
Attention
Les deux appels doivent appartenir au même bloc d’instructions
& %
Un point d’annulation pouvant intervenir à n’importe quel moment et les ressources associées aux threads
n’étant pas libérées en fin d’exécution (elles ne le sont qu’à la fin du processus), un mécanisme a été mis en
place afin de libérer ses ressources avant qu’il se termine vraiment.
TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 159
Threads ou processus légers 6 Autres fonctions de la bibliothèque POSIX threads
Pour une ressource que l’on vient d’allouer à un thread, pthread_cleanup_push permet de préciser la
fonction devant être exécutée afin de libérer la ressource, et le paramètre qui sera passé à cette fonction. Ces
fonctions sont placées dans une pile spéciale. À la terminaison du programme, les fonctions sont dés-empilées
et exécutées.
Le programmeur peut lui-même dés-empiler ces fonctions par l’intermédiaire d’un appel à
pthread_cleanup_pop. L’unique paramètre précise si la fonction doit être simplement dés-empilée (0) ou
aussi exécutée (1).
pthread_cleanup_push et pthread_cleanup_pop sont généralement implémentés sous la forme de ma-
cros dont la première ouvre un bloc lexical que ferme la seconde. Les deux appels doivent appartenir au
même bloc d’instructions.
& %
Une fonction peut être utilisée par plusieurs threads. Toutefois, certaines de ses variables peuvent ne devoir
être initialisées qu’une seule fois (c’est le cas pour l’ouverture d’une base de données par exemple). La fonction
pthread_once permet de réaliser cette opération en s’affranchissant des problèmes de synchronisation si
plusieurs threads l’appellent simultanément.
Le type pthread_once_t est opaque. Pour être utile, une variable de ce type doit être déclarée de
manière statique ou globale (afin de ne pas avoir une copie de la variable pour chaque appel de fonction).
L’initialisation s’effectue à l’aide de la valeur prédéfinie PTHREAD_ONCE_INIT. La fonction précisée en second
paramètre de l’appel à pthread_once n’est exécutée que lors du premier passage. Le premier paramètre est
un pointeur sur une variable de type pthread_once_t.
Lors d’un appel à la primitive fork, seul le fil d’exécution réalisant l’appel est dupliqué. Cependant, l’en-
semble des ressources (en particulier les piles d’exécutions et les segments de mémoire alloués dynamiquement
par les autres threads) sont aussi dupliqués. La primitive pthread_atfork permet d’empiler — pour chaque
ressource devant être libérée lors d’un appel à fork — les fonctions devant être appelées respectivement
avant l’appel à fork, par le père après l’appel à fork et par le fils après l’appel à fork.
TELECOM SudParis — Éric Renault et Frédérique Silber-Chaussumier — Avril 2016 — module CSC4508/M2 160
Architecture
François Trahay
module CSC4508/M2
Avril 2016
161
Architecture 1 Introduction
' $
Plan du document
1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
#2 3 Processeur séquentiel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
3 Pipeline . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
4 Parallel Processing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
5 Hiérarchie mémoire . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
& %
' $
1 Introduction
#3 Pourquoi ce cours ?
Comprendre ce qui se passe dans la partie “hardware” de la pile d’exécution
Pour écrire des programmes adaptés aux machines modernes
& %
Dans les faits, le compilateur se débrouille généralement pour générer un binaire qui permette d’exploiter
toutes les capacités du processeur. Mais le compilateur échoue parfois et génère un code non optimisé. Il
faut donc être capable de détecter le problème, et être capable d’écrire un code que le compilateur saura
optimiser.
' $
2 Processeur séquentiel
Une instruction nécessite N étapes
Fetch : chargement de l’instruction depuis la mémoire
Decode : identification de l’instruction
Execute : exécution de l’instruction
Writeback : stockage du résultat
Decode Writeback
instructions
cycles d'horloge
& %
Le nombre d’étapes nécessaire à l’exécution d’une instruction dépend du type de processeur (Pentium 4 :
31 étages, Intel Haswell : 14-19 étages, ARM9 : 5 étages, etc.)
3 Pipeline
Pipeline d’instructions
À chaque étape, plusieurs circuits sont utilisés
→ Une instruction est exécutée à chaque cycle
Fetch Execute
#6
Decode Writeback
instructions
cycles d'horloge
& %
' $
& %
test branch
mem writeback
& %
' $
Decode Writeback
#9
instructions
cycles d'horloge
& %
& %
' $
3.5 Gestion des branchements
Comment remplir le pipeline quand les instructions contiennent des branchements
conditionnels ?
cmp a, 7 ; a > 7 ?
ble L1
mov c, b ; b = c
br L2
L1: mov d, b ; b = d
# 11
L2: ...
En cas de mauvais choix : il faut “vider” le pipeline
→ perte de temps
Fetch
Execute
mov c,b
cycles d'horloge
& %
Le coût d’un mauvais choix lors du chargement d’une branche dépend de la profondeur du pipeline : plus
le pipeline est long, plus il faut vider d’étages (et donc attendre avant d’exécuter une instruction). Pour cette
raison (entre autres), la profondeur du pipeline dans un processeur est limitée.
& %
Les algorithmes de prédiction de branchement implémentés dans les processeurs modernes sont aujour-
d’hui très évolués et atteignent une efficacité supérieure à 98 % (sur la suite de benchmarks SPEC89).
Pour connaître le nombre de bonnes/mauvaises prédictions, on peut consulter les compteurs matériels
du processeur. Avec la bibliothèque PAPI 1 , les compteurs PAPI_BR_PRC et PAPI_BR_MSP donnent le nombre
de branchements correctement et incorrectement prédits.
' $
contient (entre autres) les jeux d’instructions disponibles sur le processeur d’une machine. Par exemple, sur
un Intel Core i7 :
$ cat /proc/cpuinfo
processor : 0
vendor_id : GenuineIntel
cpu family : 6
model : 69
model name : Intel(R) Core(TM) i7-4600U CPU @ 2.10GHz
stepping : 1
microcode : 0x1d
cpu MHz : 1484.683
cache size : 4096 KB
physical id : 0
siblings : 4
core id : 0
cpu cores : 2
apicid : 0
initial apicid : 0
fpu : yes
fpu_exception : yes
cpuid level : 13
wp : yes
flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov
pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx
pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good nopl
xtopology nonstop_tsc aperfmperf eagerfpu pni pclmulqdq dtes64
monitor ds_cpl vmx smx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid
sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx
f16c rdrand lahf_lm abm ida arat epb pln pts dtherm tpr_shadow vnmi
flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms
invpcid xsaveopt
bugs :
bogomips : 5387.82
clflush size : 64
cache_alignment : 64
address sizes : 39 bits physical, 48 bits virtual
power management:
[...]
Le champs flags contient la liste de toutes les capabilities du processeur, notamment les jeux d’instruc-
tions disponibles : mmx, sse, sse2, ssse3, sse4_1, sse4_2, avx2.
L’utilisation de ces jeux d’instructions vectorielles peut se faire en programmant directement en assem-
bleur ou en exploitant les intrinsics que fournissent les compilateurs. Toutefois, devant le nombre de jeux
d’instructions disponible et l’évolutilité des architectures de processeurs, il est recommandé de laisser le
compilateur optimiser lui même le code, par exemple en utilisant l’option -O3.
# 14 4 Parallel Processing
& %
' $
4.1 Hyperthreading / SMT
Problème du superscalaire/vectoriel :
Il faut que l’application ait suffisamment de parallélisme à exploiter
Il y a d’autres applications qui attendent d’avoir le CPU
Simultaneous Multi-Threading (SMT) :
Modifier un processeur superscalaire pour l’exécution de plusieurs threads
decode &
Shared fetch dispatch
FPU (float) writeback
Mem/Branch
& %
Le SMT est un moyen peu cher d’augmenter les performances d’un processeur : en dupliquant les “petits”
circuits (ALU, registres, etc.) et en mettant en commun les “gros” circuits (FPU, prédiction de branchements,
caches), on peut exécuter plusieurs threads simultanément. Le surcoût en terme de fabrication est léger et
le gain en performances peut être grand.
Comme le dispatcher ordonnance les instructions de plusieurs threads, une erreur du prédicteur de bran-
chement devient moins grave puisque pendant que le pipeline du thread fautif se renouvelle, un autre thread
peut s’exécuter.
Le gain de performance lorsque plusieurs threads s’exécutent n’est pas systématique puisque certains
circuits restent partagés (par exemple, le FPU).
Mem/Branch
Core A
# 16
decode &
fetch ALU (int) writeback
dispatch
FPU (float)
ALU (int)
Core B
decode &
fetch Mem/Branch writeback
dispatch
FPU (float)
& %
Il est bien sûr possible de combiner multi-cœur avec SMT. La plupart des fondeurs produisent des
processeurs multi-cœur SMT : Intel Core i7 (4 cœurs x 2 threads), SPARC T3 Niagara-3 (16 cœurs x 8
threads), IBM POWER 7 (8 cœurs x 4 threads).
' $
CPU CPU
Mem
& %
CPU CPU
Mem Mem
I/O
I/O
Mem
Mem
CPU CPU
& %
Les premières machines NUMA (dans les années 1990) étaient simplement des ensembles de machines
reliées par un réseau propriétaire chargé de gérer les transferts mémoire. Depuis 2003, certaines cartes mères
permettent de relier plusieurs processeurs Opteron (AMD) reliés par un lien HyperTransport. Intel a par la
suite développé une technologie similaire (Quick Path Interconnect, QPI) pour relier ses processeurs Nehalem
(sortis en 2007).
' $
# 19 5 Hiérarchie mémoire
& %
5.1 Enjeux
Jusqu’en 2005 : augmentation des perfs des CPUs : 55%/an
Depuis 2005 : augmentation du nombre de cœur par processeur
Augmentation des perfs de la mémoire : 10%/an
Ce sont les accès mémoire qui sont coûteux : Memory Wall
# 20 Il faut des mécanismes pour améliorer les performances de la mémoire
& %
Jusqu’aux années 1990, le goulet d’étranglement était le processeur. Du point de vue logiciel, on cherchait
alors à minimiser le nombre d’instructions à exécuter.
Avec l’augmentation des performances des processeurs, la pression est maintenant sur la mémoire. Côté
logiciel, on cherche donc à minimiser le nombre d’accès à la mémoire. Cette pression sur la mémoire est
exacerbée par le développement des processeurs multi-cœurs.
Par exemple, un processeur Intel Core i7 peut engendrer jusqu’à 2 accès mémoire par cycle d’horloge. Un
processeur à 8 cœurs hyper-threadés (donc 16 threads) tournant à 3.0 Ghz 1 peut donc générer 2×16.0×109 =
96 milliards de références mémoire par seconde. Si l’on considère des accès à des données de 64 bits, cela
représente 3072 Go/s (3.072 To/s). À ces accès aux données, il faut ajouter les accès aux instructions (jusqu’à
128 bits par instruction). On arrive donc à 6144 Go/s (donc 6.144 To/s !) de débit maximum.
À titre de comparaison, en 2016, une barette de mémoire RAM (DDR4) a un débit maximum de l’ordre
de 20 Go/s. Il est donc nécessaire de mettre en place des mécanismes pour éviter que le processeur ne passe
son temps à attendre la mémoire.
& %
Pour visualiser la hiérarchie mémoire d’une machine, vous pouvez utiliser l’outil lstopo fourni par la
bibliothèque hwloc.
pages
Une fois l’adresse physique trouvée, de-
mande les données au cache/mémoire
& %
' $
& %
La taille d’une ligne de cache dépend du processeur (généralement entre 32 et 128 octets). On peut
retrouver cette information dans /proc/cpuinfo :
& %
' $
# 25 cache) 0
n-1
32
& %
De nos jours, les caches (L1, L2 et L3) sont généralement associatifs à 4 (ARM Cortex A9 par exemple),
8 (Intel Sandy Bridge), voire 16 (AMD Opteron Magny-Cours) voies.
& %
' $
5.4 Bibliographie
« Computer Architecture, Fifth Edition : A Quantitative Approach », J. L. Hennessy
et D. A. Patterson, Morgan Kaufmann, 2011.
# 27
« Computer Organization and Design, Fifth Edition : The Hardware/Software
Interface », J. L. Hennessy et D. A. Patterson, Morgan Kaufmann, 2013.
« Computer Systems A Programmer’s Perspective » (2nd Edition), R. E. Bryant et
D. R. O’Hallaron, Prentice Hall, 2010
& %
Pour détailler un peu plus ce cours, je vous conseille cette page web : Modern microprocessors – A 90
minutes guide ! (http://www.lighterra.com/papers/modernmicroprocessors/).
Pour avoir (beaucoup) plus de détails, tournez vous vers les livres « Computer Systems A Programmer’s
Perspective » et « Computer Organization and Design, Fifth Edition : The Hardware/Software Interface »
décrivent en détail l’architecture d’un ordinateur. Si vous cherchez à connaître le fonctionnement très en
détail d’un point précis, lisez « Computer Architecture, Fifth Edition : A Quantitative Approach ».
Michel Simatic
module CSC4508/M2
Avril 2016
177
Éléments d’architecture client-serveur 1 Introduction
' $
Plan du document
1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
2 Serveur mono-tâche gérant un client à la fois . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
#2 3 Serveur avec autant de tâches que de clients . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
4 Serveur avec N tâches gérant tous les clients . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
5 Serveur mono-tâche gérant tous les clients à la fois . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
6 Conclusion. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .23
& %
' $
1 Introduction
& %
#4
& %
Exemples d’applications :
• Serveur d’impression
• Serveur Web
• Serveur de mail
' $
1.2 Objectif de cette présentation
& %
Selon l’unité de multi-programmation utilisée, une « tâche » sera implémentée via un « processus » ou
un « thread ».
Ne pas hésiter à compléter ce cours avec la lecture de [Leitner, 2003] et [Kegel, 2006]. Ils contiennent de
nombreuses informations techniques.
' $
1.3 À propos des communications
Tout processus (client ou serveur) peut recevoir des messages via un (ou plusieurs)
« point(s) d’accès » qui lui est (sont) propre(s)
Pour qu’un autre processus lui envoie un message via un « point d’accès », il faut,
au préalable, que cet autre processus se « connecte » (au sens protocolaire du
terme) (exemple : TCP, tube nommé) ou non (exemple : UDP, Système de
#6 signalisation CCITT No 7)
Par abus de langage, on dit aussi qu’un client se « connecte » au serveur lorsqu’il a
une suite d’échanges requête/réponse avec le serveur (cette « connexion client »
nécessitant une connexion protocolaire ou non).
Dans la suite de cette présentation, le terme de « connexion » désignera toujours
une « connexion client »
L’aspect communication inter-machine est hors du contexte de ce cours. Donc, dans
les TPs, on utilisera des « files de messages » en guise de « points d’accès »
& %
La programmation des « sockets » pour gérer des flux TCP ou des messages UDP est abordée dans l’U.V.
« Algorithmique et communications des applications » (CSC4509).
Même si les TPs se font en intra-machine, tous les éléments présentés ici sont valides en environnement
inter-machine.
Dans les TPs, nous utilisons des « files de messages ». On pourrait utiliser aussi des « tubes nommés ».
#7 2.1 Principe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
3.1 Analyse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
& %
' $
2.1 Principe
La tâche serveur traite les connexions client de bout en bout
#8
& %
• En 1, le serveur est en attente d’une « connexion client ». Le client C1 peut donc se connecter au
serveur pour envoyer la requête Requête11 (contenant la référence de son point d’accès sur lequel il
attend des réponses).
• En 3, le serveur répond aux différentes requêtes du client C1. En revanche, comme il n’est pas à l’écoute
sur son « point d’accès » principal d’attente de « connexion client », il ne se rend pas compte que C2
a envoyé la requête Requête21 pour laquelle il attend une réponse.
• Serveur d’impression
' $
2.2 Analyse
Avantages
Architecture simple
Bien adaptée au cas de traitements courts
Machine serveur peu chargée
#9 Pas de risque de surcharge
Inconvénients
Architecture inadaptée au cas de traitements longs, que cette longueur soit dûe
à:
I Temps de traitement long au niveau du serveur
I Nombreux échanges requête/réponse entre client et serveur avant que le client
ne se déconnecte
& %
' $
3 Serveur avec autant de tâches que de clients
3.1 Principe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
# 10 3.3 Variante . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
3.3 Analyse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
4.0 Réduction du temps de connexion des clients . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
& %
# 11
& %
• En 1, le serveur est en attente d’une « connexion client ». Le client C1 peut donc se connecter au
serveur pour envoyer la requête Requête11 (contenant la référence de son point d’accès sur lequel il
attend des réponses).
• En 2, le serveur crée un enfant (Enfant1) chargé de gérer les requêtes venant du client C1. Il lui fait
suivre via un « point d’accès » local (par exemple, un tube, une file de messages ou bien une zone
mémoire) la requête Requête11 :
– Le client ferme le « point d’accès » d’attente de « connexion client » (c’est le serveur qui est
responsable de surveiller ce « point d’accès »)
– Enfant1 ouvre un « point d’accès » dédié C1-Enfant1
– Il envoie sa réponse Réponse11(avec la référence du « point d’accès » dédié)
– Il attend d’autres requêtes (ou un message de fermeture de « connexion client ») sur ce « point
d’accès » dédié.
• En 3 :
• Serveur de mail
Pour gagner en performance (4.000 forks de processus nécessitent environ 9 secondes sur une machine à
1 GHz, soit 2 millisecondes par fork ; de plus l’arrêt de ces 4.000 processus nécessitent environ 25 secondes,
soit 6 millisecondes par libération), au lieu de créer des enfants à chaque connexion de client, on peut créer
(au moment du démarrage du serveur) un « pool d’enfants disponibles ».
Le « pool d’enfants disponibles » est un patron de conception dénommé Thread pool en anglais. En
plus d’éviter les risques de surcharge, il permet d’économiser les opérations de création/nettoyage d’en-
fants/threads pendant que le système est sollicité.
Conceptuellement, le « pool d’enfants disponibles » consiste à avoir une structure de données dans laquelle
on range les identifiants des enfants créés lors du démarrage de l’application. Quand le serveur a besoin de
faire travailler un enfant, il prend l’identifiant du premier enfant de la structure de données et il lui donne la
tâche à faire. Quand l’enfant a fini, cet enfant stocke son identifiant dans la structure de données (il signifie
ainsi qu’il est à nouveau disponible pour recevoir du travail de la part du serveur).
Dans la pratique (cf. Cf. exercice « Éléments d’architecture client-serveur » numéro 2), le « pool d’enfants
disponibles » peut être implémenté de la manière suivante :
• Tous les enfants qui sont créés se mettent directement en attente (en lecture) sur le point d’entrée
du serveur. Ainsi, quand le serveur reçoit une requête, dans le cas où le système gère l’atomicité des
lectures sur le point d’entrée (i.e. un seul processus peut réussir à faire la lecture sur le point d’entrée,
même s’ils sont plusieurs à être en attente [en lecture] sur le point d’entrée), un seul enfant est réveillé
par le système et traite la requête qu’il a lue sur le point d’entrée. Quand cet enfant a fini, il se remet
en attente sur le point d’entrée.
• Tous les enfants qui sont créés se mettent en attente (en lecture) sur le même tube (ou la même file de
message, ou bien encore le même tampon contrôlé par un mécanisme de producteur consommateur).
Quand le serveur reçoit une requête, il « redirige » cette requête en écrivant sur le tube (ou file de
message ou tampon). Un enfant est donc réveillé. Il traite la requête. Quand il a fini, il se met en
attente (en lecture) sur le tube.
' $
3.2 Variante
# 12
& %
Noter que les enfants n’ont pas besoin de « point d’accès » dédié C-Enfant dans la mesure où seul le
serveur s’adresse directement à eux.
Avantages
Adapté au cas de traitements longs
Inconvénients
Le serveur doit être hébergé par une machine puissante
& %
' $
3.4 Réduction du temps de connexion des clients
Principe :
L’enfant n’est connecté avec le client que pendant le temps de traitement d’une
requête
Si le client a une requête ultérieure faisant partie du même contexte, il transmet
# 14 le contexte avec la requête ultérieure
Exemple : Google
Limite : Solution inapplicable au cas d’applications où le client ne peut héberger le
contexte :
Soit pour des raisons de capacité (mémoire, traitement. . .)
Soit pour des raisons de sécurité
& %
# 15 4.1 Principe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
5.0 Analyse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
& %
' $
4.1 Principe
Le serveur héberge dans une « base de données »a le contexte de chacun de ses
clients potentiels
Au démarrage, le serveur démarre N enfants
Lors de la réception d’une requête, le serveur confie à l’un de ses enfants la tâche de
gérer la requête compte tenu du contexte client
• Dans le cas où le serveur est multiprocesseur, la base de donnée peut être distribuée entre les différents
processeurs. Dans ce cas, sélection de l’enfant dont la base de données contient le contexte du client.
' $
4.2 Analyse
Avantages
Adapté au cas de connexions longues avec de longs temps d’inactivité
Cas, par exemple, des applications télécomsa
# 17
Inconvénients
Complexité
Risques de surcharge
• Un bon exemple est le comportement du réseau GSM français le 01/01/2002. Beaucoup de gens avaient
décidé d’envoyer leurs voeux par SMS le 01/01/2002 à 00h00. Certains voeux sont arrivés plusieurs
jours plus tard à leurs destinataires.
– Méthode de niveau « réseau téléphonique » : certains numéros de téléphone peuvent donner lieu
à beaucoup d’appels concentrés dans le temps (par exemple, numéro d’un concours lors d’une
émission télévisée). Dans ce cas, les commutateurs traversés lors de l’établissement de l’appel ne
laissent passer qu’un faible pourcentage d’appels vers ce numéro (par exemple, 1%).
# 18 5.1 Principe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
5.2 Aperçu de la programmation événementielle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
5.3 Analyse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
& %
' $
5.1 Principe
La tâche serveur travaille avec un modèle de programmation événementielle.
# 19
& %
• En 1, le serveur est en attente d’une « connexion client ». Le client C1 peut donc se connecter au
serveur pour envoyer la requête Requête11 (contenant la référence de son point d’accès sur lequel il
attend des réponses).
• En 3, le serveur répond aux différentes requêtes du client C1. Parce que le serveur travaille avec un
modèle de programmation événementiel, il est en mesure de prendre en compte : il ouvre un « point
d’accès » dédié C2-Serveur et envoie sa réponse Réponse12 (avec la référence du « point d’accès »
dédié). Il se met ensuite en attente d’événements sur les trois points d’accès.
Exemple d’applications basées sur cette architecture :
• Memcached (http://memcached.org/) : une mémoire distribuée pour cacher des objets
• Nginx (http://nginx.net/) : un serveur Web (notamment) très performant
' $
5.2 Aperçu de la programmation événementielle
Principe
On ne déclenche une opération de lecture sur un descripteur que quand on est
sûr qu’il y a effectivement quelque chose à lire sur ce descripteur.
Pour savoir si c’est le cas, on s’appuie sur des primitives système comme
select, poll, epoll. . .
# 20
Des intergiciels (middleware) facilitent le développement d’applications basées sur
ce modèle. Ils garantissent la portabilité entre systèmes différents tout en utilisant
les primitives système les plus performantes.
Exemple : libevent (http://monkey.org/~provos/libevent/
event_set permet de spécifier le descripteur à surveiller et la fonction de rappel
(callback) à invoquer si un événement arrive sur le descripteur.
event_add permet de donner cette spécification à la tâche principale de
libevent.
& %
' $
event_dispatch démarre la tâche principale de libevent. Si un événement
arrive sur un descripteur, cette tâche appelle la fonction callback associée.
# 21
& %
L’exemple suivant présente un serveur (respectivement un client) inspiré de exemple_mkfifo_serveur.c
(respectivement exemple_mkfifo_client.c) étudié lors du cours « Communications inter-processus ». Mais
ici, le client envoie un message au serveur qui répond en signalant REQUETE_OK. Le client envoie ensuite le
même message au serveur qui répond à nouveau en signalant REQUETE_OK. Lorsque le client envoie le même
message pour la onzième fois, le serveur répond en signalant REQUETE_KO. Le client s’arrête alors.
serveurUtilisantLibevent.c illustre l’utilisation de libevent et la mémorisation du contexte du client
(en l’occurrence, le nombre de fois qu’il a déjà répondu à ce client).
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <ctype.h>
#include <error.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <event2/event.h>
struct event_base *base;
#include "client-serveur.h"
typedef struct{
int nbReponsesAuClient;
int fdW;
char nomTube[TAILLE_POINT_ACCES];
struct event *ev;
} contexte_t;
#define NB_MAX_CLIENTS 600 // Vient du fait qu’on ne pourra pas ouvrir plus de 1024 descripteurs (cf. ulimit -a ou ulimit -n),
// et que chaque client requiert 2 descripteurs
contexte_t contexte[NB_MAX_CLIENTS];
#define NB_MAX_REPONSES 10
FILE *f = NULL;
if (pctxt == NULL)
error_at_line(EXIT_FAILURE, 0, __FILE__, __LINE__, "malloc");
rangClient++;
printf("%6d-ieme client a traiter (point d’access client = %s)\n", rangClient, msg.infoSup.pointAcces);
/* On ouvre le tube nomme sur lequel le client pourra venir nous faire */
/* les demandes ultérieures. */
sprintf(pctxt->nomTube, "%s.%d", NOM_POINT_ACCES_SERV,rangClient);
if (mkfifo(pctxt->nomTube, S_IRUSR|S_IWUSR) < 0) {
if (errno != EEXIST)
error_at_line(EXIT_FAILURE, errno, __FILE__, __LINE__, "mkfifo pour requetes client-->serveur");
else
error_at_line(0, errno, __FILE__, __LINE__, "Tube nomme %s existe deja : on continue quand meme", pctxt->nomTube);
}
/* On traite la requete */
traiterConnexion(msg);
}
int main() {
int fdR;
struct event *eventTubePrincipal;
/* Attente d’evenements */
event_base_dispatch(base);
return EXIT_SUCCESS;
}
#include "client-serveur.h"
if (argc != 2) {
printf("Usage = client identifiantClient\n");
exit(EXIT_FAILURE);
}
idClient = argv[1];
/* On lit la réponse */
nbRead = read(fdR, &msgRepConnexion, sizeof(msgRepConnexion));
if (nbRead != sizeof(msgRepConnexion))
error_at_line(EXIT_FAILURE, errno, __FILE__, __LINE__, "Communication avec le serveur probablement rompue\n");
/* On analyse la réponse */
if (msgRepConnexion.typ != CONNEXION_OK)
error_at_line(EXIT_FAILURE, 0, __FILE__, __LINE__, "Le serveur m’a renvoye un type de message inconnu : %d", msgRepConnexion.typ);
msgRepConnexion.typ = REQUETE;
strncpy(msgRequete.infoSup.message,"Coucou",sizeof(msgRequete.infoSup.message));
do {
nbWrite = write(fdW, &msgRequete, sizeof(msgRequete));
if (nbWrite < sizeof(msgRequete))
error_at_line(EXIT_FAILURE, errno, __FILE__, __LINE__, "pb write sur tube nomme");
nbRead = read(fdR, &msgRepRequete, sizeof(msgRepRequete));
if (nbRead != sizeof(msgRepRequete))
error_at_line(EXIT_FAILURE, errno, __FILE__, __LINE__, "Communication avec le serveur probablement rompue\n");
if (msgRepRequete.typ == REQUETE_OK) {
printf("Client %6s : Nouvelle reponse du serveur = %s\n", idClient, msgRepRequete.infoSup.message);
}
usleep(10000);
} while (msgRepRequete.typ == REQUETE_OK);
printf("Client %6s : Envoi de requetes termine !\n", idClient);
return EXIT_SUCCESS;
}
/********************/
/* client-serveur.h */
/********************/
/**
* \enum typMessage_t
* \brief Type de message circulant entre le client et le serveur.
*
* typMessage_t définit les types de message circulant dans
* les sens client-->serveur et serveur-->client
*/
typedef enum
{
CONNEXION, /*!< (sens client-->serveur) Demande de connexion. */
CONNEXION_OK, /*!< (sens serveur-->client) Demande acceptee. */
REQUETE, /*!< (sens client-->serveur) Requete. */
REQUETE_OK, /*!< (sens serveur-->client) Reponse positive a la requete. */
REQUETE_KO /*!< (sens serveur-->client) reponse negative a la requete. */
}
typMessage_t;
/**
* \struct message_t
* \brief Message circulant entre le client et le serveur.
*
* message_t définit les structures de message circulant dans les
* sens client-->serveur et serveur-->client
*/
typedef struct
{
typMessage_t typ; /*!< Type de message */
union {
char pointAcces[TAILLE_POINT_ACCES]; /*!< Nom du point d’acces. */
char message[TAILLE_REQUETE_REPONSE]; /*!< Message associe a la requete ou la reponse. */
} infoSup; /*!< Info supplementaire associee a ce message. */
}
message_t;
1. Tester l’exécution de 600 clients en parallèle avec la ligne shell suivante : for((i=1;i<=600;i+=1));
do (./client $i &);done
Avant d’aller plus loin, commencez par taper la commande killall -9 client (pour tuer tous les
clients qui seraient en attente sur leur tube nommé), puis la commande make clean (pour nettoyer
tous les tubes nommés qui n’ont pas été effacés par les clients du fait qu’ils sont morts brutalement).
L’erreur du serveur est due au fait que le noyau a été configuré pour autoriser au maximum 1024
descripteurs ouverts simultanément par processus. La commande shell ulimit -a (ou bien ulimit
-n pour avoir seulement cette limite) nous rappelle cette limite. De ce fait, comme le serveur ouvre
2 tubes nommés par client connectés, il atteint rapidement cette limite (508 ∗ 2 = 1016 + stdin +
stdout + stderr + descripteurP ourLeP ointDAccesP rincipal [il en manque 1024 − 1020 = 4, mais le
rédacteur de ce poly n’a pas d’idées. . .]).
Que proposez-vous pour qu’au niveau du serveur, on n’atteigne pas cette limite ?
La première idée qui vient à l’esprit est d’utiliser le paradigme de synchronisation entre processus de
type Cohorte : le serveur utilise un sémaphore nbClientsAutorises initialisé à 508 (puisque le serveur
plante au delà de 508 clients connectés simultanément), fait P(nbClientsAutorises) à chaque nouveau
client qui se connecte au serveur et V(nbClientsAutorises) à chaque client qui se déconnecte. Dans
le cas présent, ce n’est pas une bonne idée (alors que c’est une excellente idée lorsqu’un travaille avec
des enfants/threads. Selon vous, pourquoi ?
Que proposez-vous à la place ?
Avantages
Adapté au cas de connexions longues avec de longs temps d’inactivité
Inconvénients
# 22
Complexité
Risques de surcharge
Le traitement d’une requête ne doit pas comprendre des appels-systèmes lents (à
moins que ce ne sois des entrées-sorties) : le serveur se comporterait alors
comme un serveur mono-processus gérant un client à la fois
& %
& %
À tort ou à raison, le Green Computing est une notion clairement en vogue actuellement. D’un point de
vue programmation et architecture logicielle, il semble que cette notion consiste essentiellement à optimiser
au maximum l’efficacité de l’application développée.
On peut y ajouter le besoin qu’une application consomme le moins de ressources possibles et surtout
soit en mesure de diminuer son utilisation de ressources en périodes creuses d’utilisation. Ainsi, pour une
application client-serveur, on peut imaginer que l’application soit très utilisée à des heures de pointe et très
peu le reste du temps. Dans ce cas, l’architecture « Serveur avec N tâches gérant tous les clients », ces
N tâches étant stockés dans un pool d’enfants disponibles, est moins Green que l’architecture « Serveur
mono-tâche gérant N clients à la fois ». En effet, en heures creuses, le pool d’enfants disponibles reste alloué
alors qu’il ne sert pas. On consomme donc de la RAM inutilement.
Bibliographie du chapitre
[Kegel, 2006] Kegel (2006). The C10K problem. http ://www.kegel.com/c10k.html. Last accessed in May
2011.
[Leitner, 2003] Leitner (2003). Scalable Network Programming Or : The Quest for a Good Web Server (That
Survives Slashdot). http ://bulk.fefe.de/scalable-networking.pdf. Last accessed in May 2011.
François Trahay
module CSC4508/M2
Avril 2016
197
Récapitulatif des outils
' $
Plan du document
& %
' $
1 Infos sur l’architecture matérielle
a
HWLoc
Fournit des informations sur la topologie de la machine (hyperthreads, cœurs,
nœuds NUMA, caches partagés, etc.)
#3 Outils permettant de placer les threads/processus sur la machine
b
PAPI
Donne accès aux compteurs matériels du processeur (nombre d’instructions
exécutées, nombre de cache miss, etc.)
a. https ://www.open-mpi.org/projects/hwloc/
b. http ://icl.cs.utk.edu/projects/papi/wiki/Main_Page
& %
Le reverse debugging est une fonctionnalité méconnue, mais très puissante de gdb. Elle per-
met d’exécuter une application en mode pas à pas, mais en reculant. Ceci permet, à partir
d’une erreur de segmentation (par exemple), de revenir en arrière pour comprendre pourquoi le
pointeur ptr est NULL (par exemple). Vous trouverez un tutoriel sur cette fonctionnalité ici :
http://jayconrod.com/posts/28/tutorial-reverse-debugging-with-gdb-7
Pour aller plus loin dans l’utilisation de gdb, je vous conseille la vidéo de Greg Law lors de la CppCon 2015
“Give me 15 minutes & I’ll change your view of GDB” : https://www.youtube.com/watch?v=PorfLSr3DDI
' $
3 Entrées/Sorties – Bases de données
Bases de données
NDBM
GDBM
#5
Oracle Berkeley DB
Ces bases de données permettent de stocker des fichiers sous la forme clé/valeur
Gestion de données locales au format SQL
a
SQLite
a. https://www.sqlite.org/
& %
ZeroMQ, nanomsg
#6
RabbitMQ, NATS, Apache Kafka
Pour échanger des messages entre processus sur une ou plusieurs machines, y
compris avec des architectures / langages différents
& %
' $
5 Traitement de tâches parallèles
GNU parallel
#7
Celery
Task-spooler
Permettent d’exécuter des listes de tâches en parallèle
& %
module CSC4508/M2
Avril 2016
201
Bibliographie
' $
Plan du document
& %
' $
1 Concepts des systèmes d’exploitation
[Kaiser, 2006] : est un cours du CNAM qui évoque les concepts évoqués dans
CSC4508/M2, mais sous la forme d’un document rédigé. Disponible sur Internet.
[Silberschatz et al., 2002] : traite de la conception des systèmes d’exploitation. Il
aborde les différentes problématiques qui apparaissent lorsqu’on veut créer un
système d’exploitation efficace et détaille les différentes manières de résoudre ces
problèmes, leurs avantages et leurs inconvénients. Enfin, pour chaque grande
thématique (ordonnancement de l’allocation CPU, allocation de la mémoire, système
#3
de fichiers distribués. . .), ce livre explique comment les systèmes d’exploitation
existants (Linux, Solaris, Windows 2000 entre autres) ont résolu ces problèmes.
[Tannenbaum, 2001] : une référence dans le domaine de la conception des systèmes
d’exploitation. Peut-être un peu moins complet que le [Silberschatz et al., 2002]
[Bloch, 2008] : présente l’histoire des systèmes d’exploitation, leur fonctionnement
et les enjeux.
[Downey, 2005] : une référence pour tout ce qui concerne les paradigmes de
synchronisation.
& %
[Downey, 2005] a un seul défaut : il utilise wait et signal en lieu et place de P() et V() ; cela peut prêter
à confusion avec les primitives wait et signal étudiées pendant le cours sur les threads.
TELECOM SudParis — Frédérique Silber-Chaussumier et Michel Simatic — Avril 2016 — module CSC4508/M2 202
Bibliographie
' $
2 Ouvrages dédiés à Unix
[Leffler and Kuzick, 1997] : présentation des concepts qui ont guidé l’élaboration du
système UNIX BSD par deux maîtres du sujet
#4
[Rifflet, 1995] : mon ancien livre de chevet avant de connaître [Blaess, 2002]
[Stevens, 1992] : considéré comme une bible
[UNIX, 2003]) et [GNU, 2003] : man et info sont des sources d’informations
inestimables.
& %
' $
3 Documents dédiés à Linux
[Blaess, 2002] : un livre (voire une référence) très complet sur la programmation
système sous Linux
[Mitchell et al., 2001] : un bon livre (désormais en français !) pour développer des
applications avancées (ou non) sous Linux
[linuxCrossRef, 2005] : pour se promener aisément dans les sources Linux
#5 [Drepper, 1999] : pour ceux qui s’intéressent aux problèmes d’optimisation
[HowTo, 2005] : l’endroit où chercher des réponses à des questions classiques sur
Linux
[linuxCenter, 2005] : un index thématique de pages Web consacrées au système
d’exploitation Linux
[linuxMag, 2005] : un magazine en ligne sur Linux
[linuxDoc, 2005] : point central pour la documentation Linux (notamment les FAQ)
& %
TELECOM SudParis — Frédérique Silber-Chaussumier et Michel Simatic — Avril 2016 — module CSC4508/M2 203
Bibliographie
' $
4 Documents spécifiques threads
#6 Threads spécifiquement
[Nichols et al., 1996] : décrit comment faire des programmes à base efficaces de
threads POSIX
[Lewis et al., 1995] : présente les principes généraux des threadset, notamment,
leur implantation sous Linux, Windows. . .
[Butenhof, 1997] : décrit comment faire des programmes efficaces à base de
threads POSIX
[Zignin, 1996] : décrit les principes généraux des threads. Commence à dater,
mais présente l’avantage d’être disponible à la bibliothèque de TSP
& %
' $
[Lampkin, 2006] : une semi-FAQ sur les threads POSIX, disponible sur le Web
[Williams, 2012] : décrit le multi-threading en C++ 11, disponible à la
bibliothèque de TSP
#7
& %
Le cours [Bryant and O’Hallaron, 2003] est très bien fait. Il part des couches basses pour expliquer les
différentes notions : les images mémoire des processus pour expliquer la notion de thread, les instructions
liées à une lecture et à une écriture pour expliquer les problèmes de synchronisation avec pour exemple le
compteur.
Le livre [Robbins and Robbins, 2003] est très progressif. Il explique notamment le sens de thread-safe avec
strtok() comme exemple (p. 39) et y revient dans un chapitre sur les threads POSIX avec comme exemples
strerr() et errno (p. 432). Les threads sont expliqués sur trois chapitres avec beaucoup d’exemples, no-
tamment des exemples détaillés de gestion des signaux avec des threads. Introduit aussi des primitives non
implantées sur Linux comme les primitives pthread_rw_*.
Le livre [Blaess, 2002] fournit beaucoup d’explications sur l’implantation des threads sous Linux même
s’il est déjà un peu ancien par exemple ce qui concerne la directive _REENTRANT est déjà obsolète.
TELECOM SudParis — Frédérique Silber-Chaussumier et Michel Simatic — Avril 2016 — module CSC4508/M2 204
Bibliographie
' $
5 Divers
Symbian OS
[Morris, 2007] présente Symbian OS.
& %
[Morris, 2007] est mis ici par curiosité intellectuelle. En effet, après l’annonce par Nokia de l’utilisation
de Windows Mobile pour ses téléphones, le projet Symbian OS a été arrété.
' $
6 Bibliographie
[Blaess, 2002] Blaess, C. (2002). Programmation système en C sous Linux : signaux,
processus, threads, IPC et sockets. Eyrolles, Paris, Paris, France.
[Bloch, 2008] Bloch, L. (2008). Les systèmes d’exploitation des ordinateurs : histoire,
fonctionnement, enjeux. Vuilbert, http ://greenteapress.com/semaphores/.
http ://www.laurent-bloch.org/spip.php ?article13.
[Bovet and Cesati, 2003] Bovet, D. P. and Cesati, M. (2003). Understanding the Linux
#9 kernel. O’Reilly, 2 edition.
[Bryant and O’Hallaron, 2003] Bryant, R. E. and O’Hallaron, D. R. (2003). Computer
Systems : A Programmer’s Perspective. Prentice Hall.
[Butenhof, 1997] Butenhof, D. (1997). Programming with POSIX Threads. Addison
Wesley.
[Downey, 2005] Downey, A. B. (2005). The Little Book of Semaphores. GreenTeaPress,
http ://greenteapress.com/semaphores/. Version 2.1.5.
[Drepper, 1999] Drepper, U. (1999). Optimizing applications with gcc & glibc.
Technical report, RedHat. http ://people.redhat.com/drepper/optimtut.ps.gz.
& %
TELECOM SudParis — Frédérique Silber-Chaussumier et Michel Simatic — Avril 2016 — module CSC4508/M2 205
Bibliographie
' $
[GNU, 2003] GNU (2003). Pages d’Info de GNU. GNU/Linux, Distribution RedHat 7.3
edition.
[HowTo, 2005] HowTo (2005). How-to linux. http ://www.traduc.org. Last accessed in
May 2011.
[Kaiser, 2006] Kaiser, C. (2006). Systèmes informatiques : les structures et paradigmes
des plates-formes informatiques.
http ://deptinfo.cnam.fr/Enseignement/CycleProbatoire/SRI/Systemes.
' $
http ://bulk.fefe.de/scalable-networking.pdf. Last accessed in May 2011.
[Lewis et al., 1995] Lewis, B., Berg, D., and Lewis, B. (1995). Threads Primer : A
Guide to Multithreaded Programming. Prentice Hall.
[linuxCenter, 2005] linuxCenter (2005). Linux center. http ://www.linux-center.org/fr.
Last accessed in May 2011.
[linuxCrossRef, 2005] linuxCrossRef (2005). Cross-referencing linux.
http ://lxr.linux.no/source. Last accessed in May 2011.
# 11 [linuxDoc, 2005] linuxDoc (2005). The Linux Documentation Project.
http ://www.tldp.org/. Last accessed in May 2011.
[linuxMag, 2005] linuxMag (2005). Linux magazine. http ://www.linux-mag.com/. Last
accessed in May 2011.
[Mitchell et al., 2001] Mitchell, M., Oldham, J., and Samuel, A. (2001).
Programmation Avancée sous Linux (Advanced Linux Programming). New Riders
Publishing, http ://www.advancedlinuxprogramming-fr.org.
[Morris, 2007] Morris, B. (2007). The Symbian OS architecture sourcebook : Design
and evolution of a mobile phone OS. Wiley.
& %
TELECOM SudParis — Frédérique Silber-Chaussumier et Michel Simatic — Avril 2016 — module CSC4508/M2 206
Bibliographie
' $
[Nichols et al., 1996] Nichols, B., Buttlar, D., and Farrell, J. (1996). Pthreads
Programming : A POSIX Standard for Better Multiprocessing. O’Reilly and
Associates.
[Rifflet, 1995] Rifflet, J.-M. (1995). La programmation sous UNIX, 3è Édition.
Ediscience International, Paris, France.
[Robbins and Robbins, 2003] Robbins, K. A. and Robbins, S. (2003). UNIX SYSTEMS
Programming : communication, Concurrency and Threads. Prentice Hall.
# 12 [Silberschatz et al., 2002] Silberschatz, A., Baer, P., and Gagne, G. (2002). Operating
System Concepts. John Wiley & Sons Inc., 6th edition.
[Stevens, 1992] Stevens, W. (1992). Advanced Programming in The UNIX
Environment. Addison-Wesley, Reading, Massachusetts, USA.
[Tannenbaum, 2001] Tannenbaum, A. (2001). Modern Operating Systems. Hardcover,
2nd edition.
[UNIX, 2003] UNIX (2003). Pages du manuel en ligne UNIX. GNU/Linux, Distribution
RedHat 7.3 edition.
[Williams, 2012] Williams, A. (2012). C++ Concurrency in action, Practical
& %
' $
Multithreading. Manning Publications, first edition.
[Zignin, 1996] Zignin, B. (1996). Techniques du multithread : du parallèlisme dans les
processus. Hermès.
# 13
& %
TELECOM SudParis — Frédérique Silber-Chaussumier et Michel Simatic — Avril 2016 — module CSC4508/M2 207
Bibliographie
TELECOM SudParis — Frédérique Silber-Chaussumier et Michel Simatic — Avril 2016 — module CSC4508/M2 208
Index
Symbols cohérence de cache, 176
_aligned_free, 45 Cohorte, see Synchronisation, Cohorte
_aligned_malloc, 42, 45 Conditions, see Moniteurs
__attribute__ Consommateurs, see Synchronisation, Produc-
__attribute__ teurs/Consommateurs
aligned, 42 Contrôle de fichier, see Fichier, Contrôle de
__attribute__ creat, 58
packed, 42 Création de fichier, see Fichier, Création de
-fpackstruct, 42 Création de processus, see Processus, Création de
A D
abort, 16 D-Bus, 91
access, 74 Défauts de page, 43
Adresse logique, 32 Déplacement dans fichier, see Fichier, Déplacement
aio_error, 65 dans
aio_read, 65 Descripteur, 61, 72
aio_return, 65 Écriture, 59
aio_write, 65 Lecture, 59
Algorithme de la seconde chance, see Seconde chance, Descripteur de segments, 35
Algorithme de la dup, 61
Alignement, 42 dup2, 61
alloca, 48 Duplication de descripteur, 61
Appel procédural entre processus, see Synchronisa-
tion, Appel procédural entre processus E
Appels systèmes, 19 EAGAIN, 65
Architecture client/serveur, 178 Écriture fichier, see Fichier, Écriture
argc, 79 Écriture sur descripteur, see Descripteur, Écriture
argv, 79 Écroulement, 33
assert, 16 EINTR, 65
atexit, 82 electric fence, 51
Attente de processus, see Processus, Attente de environ, 79
AVX, see Instructions vectorielles SSE Environnement des processus, see Processus, Envi-
ronnement des
B Envoi de signal, see Synchronisation, Envoi de signal
Belady, 37 envp, 79
Berkeley DB, see Oracle Berkeley DB epoll, 189
bless, 56 Erreur système, 16
Blocage des entrées-sorties, 65 errno, 15, 16, 20, 138, 141
brk, 49 error, 16
error_at_line, 16
C ERROR_AT_LINE, 16
cache, 23 Espace vital, 33
Mémoire, 29 event_add, 189
Cache, 40 event_dispatch, 189
d’adresses, 33 event_set, 189
cache fully-associatif, 23 EWOULDBLOCK, 65
cache hit, 23 Exclusion mutuelle, see Synchronisation, Exclusion
cache miss, 23 mutuelle
cache snooping, 176 execl, 83
calloc, 45 execle, 83
Celery, 7 execlp, 83
Clé, see IPC, Clé Exécution nouveau programme, see Processus, Exé-
clearerr, 72 cution nouveau programme
Client serveur, see Architecture client/serveur execv, 83
close, 58, 95 execve, 83
closedir, 75 execvp, 83
209
exit, 82 getc, 68
getchar, 68
F getegid, 80
fallocate, 59 getenv, 79
fclose, 68 geteuid, 80
fcntl, 62 getgid, 80
feof, 72 getpgid, 80
Fermeture fichier, see Fichier, Fermeture getpid, 80
ferror, 72 getppid, 80
fflush, 71 getrlimit, 43
fgetc, 68 getrusage, 23, 39
fgets, 68 gets, 68
Fichier getuid, 80
Conseil, 64 GNU Parallel, 7
Contrôle de, 62, 71 Google Breakpad, 4
Création de, 58
Déplacement dans, 65, 72 H
Écriture, 59 hexdump, 56
Écriture de, 69, 70 HWLoc, 3
Fermeture de, 58, 68 Hyperthreading, 169
I-nœuds, 73, 74 Hypertransport, 171
Lecture, 59
Lecture de, 68, 69
I
ICE, 91
NFS, 76
Inclusion, see Principe d’inclusion
Offset, see Fichier, Déplacement dans
Informations concernant un processus, see Processus,
Ouverture de, 58, 68
Informations concernant un
Projection en mémoire, 72
I-nœuds, see Fichier, I-nœuds
Répertoire, 75
Instruction Level Parallelism (ILP), 166
Tampon du, 71
Instructions vectorielles, 167
Verrouillage de, 62
Instructions vectorielles AVX, see Instructions vecto-
FIFO, see First In First Out
rielles SSE
File de messages, 91
Instructions vectorielles SSE, 43
fileno, 72
insure++, 50, 51
Files de messages, 91
Intel, 29, 33
Fin de processus, see Processus, Terminaison de
Interblocage, see Synchronisation,Interblocage
FINUFO, see Seconde chance, Algorithme de la
Intergiciel, 189
First In First Out, 37
Inter Process Communication, see IPC
First In Not Used First Out, see Seconde chance, Al-
interruption, 65
gorithme de la
Interruption, 21
fopen, 68
Inversion de priorité, 24
fork, 81
IPC
fprintf, 70
Clé, 90
fputc, 69
Files de messages, see Files de messages
fputs, 69
Mémoire partagée, see Mémoire, partagée
fread, 68
free, 45 K
Algorithmes, 47 Kafka, 6
fscanf, 69
fseek, 72 L
fsync, 58, 71 Least Frequently Used, 37
ftruncate, 95 Least Recently Used, 37
fwrite, 69 Lecteurs/Rédacteurs, see Synchronisation,
Lecteurs/Rédacteurs
G Lecture de fichier, see Fichier, Lecture
gdb, 4 Lecture sur descripteur, see Descripteur, Lecture
gdb, 54 Lenteur des entrées-sorties, 65
GDBM, 5, 57, 65, 72 LFU, see Least Frequently Used
210
libevent, 66, 189 O
ligne de cache, 23 O_APPEND, 58
Limite, see Voir ulimit O_ASYNC, 62
LRU, see Least Recently Used O_CREAT, 58
lseek, 65 Offset, see Fichier, Déplacement dans
lstat, 75 offsetof, 43
O_NDELAY, 58
M O_NONBLOCK, 58, 62, 65
main, 79 open, 58
majeurs opendir, 75
Défauts de page, see Défauts de page Oracle Berkeley DB, 5, 57, 65, 72
malloc, 45, 48, 49 O_RDONLY, 58
Algorithmes, 47 Ordonnancement
mallopt, 45 Politique d’, 22
Memcached, 188 Priorité
memset, 45 dynamique, 25
Mémoire, 35 statique, 22, 25
partagée, 95 Priorité d’ordonnancement, 25
memory wall, 20 standard, 25
Middleware, 189 O_RDWR, 58
mineurs O_SYNC, 58, 71
Défauts de page, see Défauts de page O_TRUNC, 58
mkdir, 75 Ouverture fichier, see Fichier, Ouverture
mkfifo, 87 O_WRONLY, 58
mknod, 87
P
mlock, 43
P, 98, 101, 108, 143
mlockall, 43
Pages
mmap, 49, 72, 95
Utilisation des, 35, 39
Moniteurs, 123
Pagination, 31, 35, 43
mpatrol, 51
PAPI, 3, 12
mprotect, 54
Passage de témoin, see Synchronisation, Passage de
mq_close, 91 témoin
mq_open, 91 perror, 16, 20
mq_receive, 91 pipe, 85
mq_send, 91 Pipeline, 164
mq_unlink, 91 Politique
mtrace, 51 d’ordonnancement, see Ordonnancement, Poli-
munlock, 43 tique d’
munlockall, 43 poll, 189
munmap, 72, 95 popen, 86
Portable Operating System Interface, see POSIX
N POSIX, 91, 130
nanomsg, 6 Sémaphores, see Sémaphores,POSIX
NATS, 6 posix_fadvise, 64
NDBM, 5, 57, 65, 72 posix_fallocate, 59
Network File System, see Fichier, NFS posix_memalign, 42, 45
Network Information Service, 76 POSIX Pthreads, 132
NFS, see Fichier, NFS Principe d’inclusion, 29
Nginx, 188 printf, 70
nice, 25 Priorité
NIS, 76 dynamique, see Ordonnancement, Prorité dy-
Non Uniform Memory Architecture, 171 namique
notify, 123 statique, see Ordonnancement, Prorité statique
notifyAll, 123 Processeur, 162
Noyau, 19 Processus
NUMA, 171 Attente de, 82
211
Création de, 81 Q
Environnement des, 79 QPI, 171
Exécution nouveau programme, 83 Quantum de temps, 22, 25
Informations concernant un, 80 Quick Path Interconnect, 171
Terminaison de, 82
Producteurs/Consommateurs, see Synchronisation, R
Producteurs/Consommateurs RabbitMQ, 6
Programmation événementielle, 189 Rational Purify, 51
Projection de fichier en mémoire, see Fichier, Projec- read, 59
tion en mémoire readddir, 75
ps, 23, 35, 39 readlink, 75
pthread_atfork, 160 readv, 59
pthread_attr_destroy, 134 realloc, 45
pthread_attr_getdetachstate, 134 Rédacteurs, see Synchronisation,
pthread_attr_getschedparam, 135 Lecteurs/Rédacteurs
pthread_attr_getschedpolicy, 135 rename, 74
pthread_attr_getstackaddr, 135 Rendez-vous entre deux processus, see Synchronisa-
pthread_attr_getstacksize, 135 tion, Rendez-vous entre deux processus
pthread_attr_init, 134 Répertoire, see Fichier, Répertoire
pthread_attr_setdetachstate, 134 Retour
pthread_attr_setschedparam, 135 d’appel système, 15
pthread_attr_setschedpolicy, 135 de fonction, 15
pthread_attr_setstackaddr, 135 return, 15
rewinddir, 75
pthread_attr_setstacksize, 135
rmdir, 75
pthread_cancel, 157
pthread_cleanup_pop, 159
S
pthread_cleanup_push, 159
sbrk, 49
pthread_cond_broadcast, 149
scanf, 69
pthread_cond_destroy, 148
SCHED_FIFO, 22
pthread_cond_init, 148 SCHED_OTHER, 22, 25
pthread_cond_signal, 149 SCHED_RR, 22
pthread_cond_timedwait, 149 sched_rr_get_interval, 22
pthread_cond_wait, 149 sched_setscheduler, 22
pthread_create, 133 sched_yield, 22
pthread_detach, 133 Seconde chance
pthread_equal, 133 Algorithme de la, 37
pthread_exit, 133 Segmentation, 34, 35
pthread_get_specific, 141 Segments, 35
pthread_join, 133 select, 66, 189
pthread_key_create, 141 Sémaphores, 98, 108, 146
pthread_key_delete, 141 POSIX, 101
pthread_mutex_destroy, 144 sem_close, 101
pthread_mutex_init, 144 sem_destroy, 101, 146
pthread_mutex_lock, 145 sem_getvalue, 101, 146
pthread_mutex_trylock, 145 sem_init, 101, 146
pthread_mutex_unlock, 145 sem_open, 101
pthread_once_t, 160 sem_post, 101, 146
pthread_self, 133 sem_trywait, 146
pthread_setcancelstate, 157 sem_unlink, 101
pthread_setcanceltype, 157 sem_wait, 101, 146
pthread_set_specific, 141 sendfile, 61
pthread_testcancel, 159 Serveur, see Architecture client/serveur
purify, 51 setvbuf, 71
putc, 69 setgid, 80
putchar, 69 setitimer, 66
puts, 69 setpgid, 80
212
setpriority, 25 U
setrlimit, 43 ulimit, 16
shm_open, 95 Unix NDBM, see NDBM
shm_unlink, 95 unlink, 74, 75
SIGALARM, 66 Utilisation des pages, 35, 39
signaction, 66
signal, 65 V
Signal, see Synchronisation, Envoi de signal V, 98, 101, 108, 143
size, 35 valgrind, 4
SMP, 170 valgrind, 40, 51
SMT, 169 Variables-condition, see Moniteurs
splint, 50, 51 Verrouillage de fichier, see Fichier, Verrouillage de
sprintf, 70 vfork, 81
SQLite, 5 vmstat, 35
sscanf, 69
SSE, see Instructions vectorielles SSE W
stat, 73 wait, 82, 123
strerror, 16, 20 waitpid, 82
superscalaire, 165 watchpoint, 54
Swap, 35 WEXITSTATUS, 82
symlink, 75 WIFEXITED, 82
Symmetric Multi-Processing, 170 WIFSIGNALED, 82
Synchronisation Wine, 51
Appel procédural entre processus, 113 Working set, 33
Cohorte, 111 write, 59
writev, 59
disque, 58
WTERMSIG, 82
Envoi de signal, 112
Exclusion mutuelle, 110 Z
Interblocage, 122 ZeroMQ, 6
Lecteurs/Rédacteurs, 119 ZeroMQ, 91
Passage de témoin, 111
Producteurs/Consommateurs, 114
Rendez-vous entre deux processus, 112
synchronized, 123
Systèmes
temps réel, 8, 9
transactionnel, 9
T
Table des pages, 32
Taille, 35
Tampon du fichier, see Fichier, Tampon du
Task-spooler, 7
Temps réel, see Systèmes temps réel, 9
Linux, 23
Terminaison de processus, see Processus, Terminai-
son de
Thrashing, 33
Thread, 126
time, 23, 39
TLB, 33
top, 23, 35, 39
Transactionnel, see Systèmes transactionnels
Translation Look-aside Buffer, 33
Tubes
nommés, 87
ordinaires, 85
213
214