Cours Systeme Exploitation
Cours Systeme Exploitation
D.Revuz
17 février 2005
ii
Table des matières
1 Introduction 1
1.1 Unix . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.1.1 Pourquoi unix ? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.1.2 le succès d’Unix et de linux . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.1.3 Des points forts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.1.4 Des points faibles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.2 Structure générale des systèmes d’exploitation . . . . . . . . . . . . . . . . . . . . . 2
1.2.1 Les couches fonctionnelles . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.2.2 L’architecture du système . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.2.3 L’architecture du noyau . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.3 historique . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
3 Le Buffer Cache 17
3.1 Introduction au buffer cache . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
3.1.1 Avantages et désavantages du buffer cache . . . . . . . . . . . . . . . . . . . 17
3.2 Le buffer cache, structures de données. . . . . . . . . . . . . . . . . . . . . . . . . . 17
3.2.1 La liste doublement chaı̂née des blocs libres . . . . . . . . . . . . . . . . . . 18
3.3 L’algorithme de la primitive getblk . . . . . . . . . . . . . . . . . . . . . . . . . . 18
4 La bibliothèque standard 23
4.1 Les descripteurs de fichiers. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
4.1.1 Ouverture d’un fichier . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
4.1.2 Redirection d’un descripteur : freopen . . . . . . . . . . . . . . . . . . . . 24
4.1.3 Création de fichiers temporaires . . . . . . . . . . . . . . . . . . . . . . . . . 25
4.1.4 Ecriture non formatée . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
4.1.5 Accès séquentiel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
4.1.6 Manipulation du pointeur de fichier . . . . . . . . . . . . . . . . . . . . . . 26
4.1.7 Un exemple d’accès direct sur un fichier d’entiers. . . . . . . . . . . . . . . 26
4.1.8 Les autres fonctions de déplacement du pointeur de fichier. . . . . . . . . . 26
4.2 Les tampons de fichiers de stdlib. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
4.2.1 Les modes de bufferisation par défaut. . . . . . . . . . . . . . . . . . . . . . 27
4.2.2 Manipulation des tampons de la bibliothèque standard. . . . . . . . . . . . 27
iii
iv TABLE DES MATIÈRES
6 Les processus 41
6.1 Introduction aux processus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
6.1.1 Création d’un processus - fork() . . . . . . . . . . . . . . . . . . . . . . . 41
6.2 Format d’un fichier exécutable . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
6.3 Chargement/changement d’un exécutable . . . . . . . . . . . . . . . . . . . . . . . 42
6.4 zone u et table des processus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
6.5 fork et exec (revisités) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
6.6 Le contexte d’un processus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
6.7 Commutation de mot d’état et interruptions. . . . . . . . . . . . . . . . . . . . . . 45
6.8 Les interruptions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
6.9 Le problème des cascades d’interruptions . . . . . . . . . . . . . . . . . . . . . . . . 47
6.9.1 Etats et transitions d’un processus . . . . . . . . . . . . . . . . . . . . . . . 47
6.9.2 Listes des états d’un processus . . . . . . . . . . . . . . . . . . . . . . . . . 47
6.10 Lecture du diagramme d’état. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
6.11 Un exemple d’exécution . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
6.12 La table des processus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
6.13 La zone u . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
6.14 Accès aux structures proc et user du processus courant . . . . . . . . . . . . . . . 50
6.14.1 Les informations temporelles. . . . . . . . . . . . . . . . . . . . . . . . . . . 50
6.14.2 Changement du répertoire racine pour un processus. . . . . . . . . . . . . . 51
6.14.3 Récupération du PID d’un processus . . . . . . . . . . . . . . . . . . . . . . 51
6.14.4 Positionement de l’euid, ruid et suid . . . . . . . . . . . . . . . . . . . . . . 51
6.15 Tailles limites d’un processus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
6.15.1 Manipulation de la taille d’un processus. . . . . . . . . . . . . . . . . . . . . 52
6.15.2 Manipulation de la valeur nice . . . . . . . . . . . . . . . . . . . . . . . . . 52
6.15.3 Manipulation de la valeur umask . . . . . . . . . . . . . . . . . . . . . . . . 52
6.16 L’appel système fork . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
6.17 L’appel système exec . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
8 La mémoire 61
8.0.4 les mémoires . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
8.0.5 La mémoire centrale . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
8.1 Allocation contiguë . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
8.1.1 Pas de gestion de la mémoire . . . . . . . . . . . . . . . . . . . . . . . . . . 63
8.1.2 Le moniteur résidant . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
8.1.3 Le registre barrière . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
8.1.4 Le registre base . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
8.1.5 Le swap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
8.1.6 Le coût du swap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
8.1.7 Utilisation de la taille des processus . . . . . . . . . . . . . . . . . . . . . . 65
8.1.8 Swap et exécutions concurrentes . . . . . . . . . . . . . . . . . . . . . . . . 66
8.1.9 Contraintes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
8.1.10 Deux solutions existent . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
8.1.11 Les problèmes de protection . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
8.1.12 Les registres doubles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
8.2 Ordonnancement en mémoire des processus . . . . . . . . . . . . . . . . . . . . . . 67
8.3 Allocation non-contiguë . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
8.3.1 Les pages et la pagination . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
8.3.2 Ordonnancement des processus dans une mémoire paginée . . . . . . . . . 68
8.3.3 Comment protéger la mémoire paginée . . . . . . . . . . . . . . . . . . . . . 69
8.3.4 La mémoire segmentée . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
9 La mémoire virtuelle 71
9.0.5 Les overlays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
9.0.6 Le chargement dynamique . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
9.1 Demand Paging . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
9.1.1 Efficacité . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
9.2 Les algorithmes de remplacement de page . . . . . . . . . . . . . . . . . . . . . . . 75
9.2.1 Le remplacement optimal . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
9.2.2 Le remplacement peps (FIFO) . . . . . . . . . . . . . . . . . . . . . . . . . 75
9.2.3 Moins récemment utilisée LRU. . . . . . . . . . . . . . . . . . . . . . . . . . 76
9.2.4 L’algorithme de la deuxième chance . . . . . . . . . . . . . . . . . . . . . . 76
9.2.5 Plus fréquemment utilisé MFU . . . . . . . . . . . . . . . . . . . . . . . . . 76
9.2.6 Le bit de saleté (Dirty Bit) . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
9.3 Allocation de pages aux processus . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
9.4 L’appel fork et la mémoire virtuelle . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
9.5 Projection de fichiers en mémoire . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
9.6 Les conseils et politiques de chargement des zones mmappées . . . . . . . . . . . . 80
9.7 Chargement dynamique . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
11 Les signaux 89
11.0.4 Provenance des signaux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
11.0.5 Gestion interne des signaux . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
11.0.6 L’envoi de signaux : la primitive kill . . . . . . . . . . . . . . . . . . . . . . 90
11.1 La gestion simplifiée avec la fonction signal . . . . . . . . . . . . . . . . . . . . . 91
11.1.1 Un exemple . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
11.2 Problèmes de la gestion de signaux ATT . . . . . . . . . . . . . . . . . . . . . . . . 91
11.2.1 Le signal SIGCHLD . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
11.3 Manipulation de la pile d’exécution . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
11.4 Quelques exemples d’utilisation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
11.4.1 L’appel pause . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
11.5 La norme POSIX . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
11.5.1 Le blocage des signaux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
11.5.2 sigaction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
11.5.3 L’attente d’un signal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
17 Clustering 135
17.1 Le clustering sous linux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 135
18 Bibliographie 137
18.1 Webographie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137
viii TABLE DES MATIÈRES
http://www-igm.univ-mlv.fr/~dr/NCS/
Introduction
Ceci est un polycopié de cours de licence informatique sur les systèmes d’exploitations en
général et plus spécialement sur la famille Unix.
Ce poly est le support utilisé pour les licences et pour les Apprentis ingénieurs de la filière Infor-
matique et Réseau.
1.1 Unix
1.1.1 Pourquoi unix ?
Pourquoi ce choix d’unix comme sujet d’étude pour le cours ?
– LE PRIX
– la disponibilité des sources
– L’intélligence des solutions mise en oeuvre
– de grande ressource bibliographique
– il faut mieux apprendre les conceptes fondamentaux dans un système simple et ouvert puis
passer a des systèmes propriétaires et fermés que l’inverse.
– parceque je ne vais pas changer mon cours tout de suite
1
2 CHAPITRE 1. INTRODUCTION
Il est plus facile de définir un système d’exploitation par ce qu’il fait que par ce qu’il est. J.L.
Peterson.
Un système d’exploitation est un ensemble de procédures cohérentes qui a pour but de gérer la
pénurie de ressources. J-l. Stehlé P. Hochard.
Quelques systèmes :
le batch Le traitement par lot (disparus).
interactifs Pour les utilisateurs (ce cher UNIX).
temps réels Pour manipuler des situations physiques par des périphériques (OS9 un petit frère
futé d’UNIX). L’idée est de gérer le temps vrai.En particulier de gérer des évènement
aléatoires qui neccessite l’exécution d’une action en proirité absolue.
distribués UNIX ?, les micros noyaux ? l’avenir ?
moniteurs transactionnels Ce sont des applications qui manipulent des objets à tâches mul-
tiples comme les comptes dans une banque, des réservations, etc. L’idée est de décomposer
l’activité en actions, chacune indépendantes des autres,pour ce faire elle sont écrites pour
avoir un comportement dit ”atomique” ainsi il n’y a pas de programme mais des évènement
et des actions associées. Il n’y pas dans de changement de context pour traiter une action,
c’est le système adapté pour traité de gros volumes de petites opérations.
SE orientés objets Micro Noyaux.
Utilisateurs
Système d’exploitation
Matériel
utilisateur
utilisateur
SHELL utilisateur
make
ls
awk
Hardware
cc
Kernel
vi
Applications
Applications/utilisateurs
bibliotheques
Niveau Utilisateur
Niveau Noyau
Interface appels - systeme
Sous-Systeme Sous-Systeme
de Gestion des processus
Gestion de fichiers communication
interprocessus
"scheduler"
cache
gestion memoire
caractere | bloc
Controleurs
Controle Materiel
Niveau Noyau
Niveau Materiel
Materiel
1.3 historique
Il existe un site très agréable sur l’histoire des ordinateurs :
http://www.computerhistory.org/
.
6 CHAPITRE 1. INTRODUCTION
Chapitre 2
Le système de gestion de fichier permet une manipulation simple des fichiers et gère de façon
transparente les différents problèmes d’accès aux supports de masse :
– partage : utilisation d’un même fichier/disque par plusieurs utilisateurs
– efficacité : utilisation de cache, uniformisation des accès
– droits : protection des éléments important du système et protection interutilisateurs
– alignement : transtypage entre la mémoire et les supports magnétiques
Un fichier Unix est une suite finie de bytes (octets) Matérialisée par des blocs
disques, et une inode qui contient les propriétés du fichier (mais pas son nom).
Le contenu est entièrement défini par le créateur, la gestion de l’allocation des ressources nécessaires
est a la seule responsabilité du système.
Sur Unix les fichiers ne sont pas typés du point de vue utilisateur, le concept de fichier permet
de proposer un type générique (polymorphe) aux programmeurs le système gérant la multiplicité
des formats effectifs (différenttes marques et conceptions de disques dur par exemple).
7
8 CHAPITRE 2. SYSTÈME DE GESTION DE FICHIERS
Les fichiers physiques dans le répertoire /dev (dev comme devices dispositifs matériels, les périphériques
et quelques dispositifs logiques )
– liens symboliques
– pseudo-terminaux
– sockets
– tubes nommés
Ce dernier type de fichiers spéciaux est utilisé pour servir d’interface entre disques, entre
machines et simuler : des terminaux, des lignes de communication, etc.
Cette distinction entre fichier ordinaire et spéciaux et tout simplement le fait que le système est
un utilisateur comme les autres des fichiers. Pour certains fichier le système utilise une structure
interne spéciale (d’ou le nom) qui ne doit pas être modifier sous peine de comportement indéfini.
Pour se protéger le système ne permet pas l’accès direct aux informations c’est lui qui fait toutes
les entrées sortie sur les fichiers spéciaux de façon a en assurer l’intégrité. Ceci est indépendant
du système de droits d’accès, la structure du code du noyau ne permet pas d’autres accès que les
accès ”spéciaux” 2 .
écriture était contraint, pour assurer la structure arborescente acyclique. Aujourd’hui tout les accès au répertoires
ont contraint et on a un ensemble d’appels système spécifiques pour réaliser des entrés sortie dans les reper-
toires. opendir(3), closedir(3), dirfd(3), readdir(3), rewinddir(3), scandir(3),seekdir(3), telldir(3)
approche qui permet d’être effectivement plus indépendant sur la structure interne des répertoires, avec des système
plus efficaces que les listes utilisées dans les première implémentations. Voire Reiser fs par exemple.
2 Pour plsu d’information sur le sujet aller voire dans les sources les structures de sgf et d’inode TODO : nom
de fichiers concernés .
2.4. LES INODES 9
noeud de l’arbre qui permette de lister les fichiers et les sous arbres qu’il contienti, d’autres
organisations ”plates” existe ou l’on organise les fichiers en utilisans des types et des extentions
de nom de fichier pour ”organiser”.
Les arborescences de fichiers et de catalogues, organisées comme un graphe acyclique 3 , appa-
raissent avec le projet MULTICS.
Cette organisation logique du disque a les avantages suivants :
Une racine, un accès absolu aisé (à la différence de certains système qui ont de nom-
breuses ”racines”).
Une structure dynamique.
Une grande puissance d’expression.
Un graphe acyclique.
L’organisation est arborescente avec quelques connections supplémentaires (liens multiples sur un
même fichier) qui en font un graphe. Mais ce graphe doit rester acyclique, pour les raisons sui-
vantes :
L’ensemble des algorithmes simples utilisables sur des graphe acycliques comme le parcours,
la vérification des fichiers libres, etc. deviennent beaucoup plus difficiles à écrire pour des graphes
admettant des cycles.
Des algorithmes de ramasse-miettes doivent être utilisés pour savoir si certains objets sont
utilisés on non et pour récuperer les inodes ou blocs perdus après un crash.
Tous les algorithmes de détection dans un graphe quelconque ont une complexité beaucoup
plus grande que ceux qui peuvent profiter de l’acyclicité du graphe.
Sous Unix nous sommes assurés que le graphe est acyclique car il est interdit d’avoir plusieurs
références pour un même catalogue (sauf la référence spéciale ”..” ).
sa référence dans l’arborescence (son nom), l’arborescence n’étant qu’un outil de référencement
des fichiers.
le statut de l’inode
{ locked,
waiting P
inode à écrire,
fichier à écrire,
le fichier est un point de montage
}
Et deux valeurs qui permettent de localiser l’inode sur un des disques logiques :
Numéro du disque logique
Numéro de l’inode dans le disque
cette information est inutile sur le disque (on a une bijection entre la position de l’inode sur disque
et le numéro d’inode).
On trouve aussi d’autres types d’informations comme l’accès à la table des verrous ou bien des
informations sur les disques à distance dans les points de montage.
Blocs de
Boot Bloc Boot Bloc (vide) données
Blocs de données
sur le disque
Inode
direct 0
direct 1
direct 2
direct 3
direct 4
direct 5
direct 6
direct 7
direct 8
direct 9
indirect
double
triple
18 19 20
Curseur
18 19 20
Curseur
473 vide
Curseur
Curseur
Curseur
Travail sur
l'Inode I en mémoire
allocation
d'une inode (d) allocation (d)
de l'inode I
Mais l'inode I
n'est pas libre !!
allocation (e)
d'une autre l'inode
temps
(a) I
(b)
(c) JIK
(d) JI
(e) L
temps
Bloc 109
211 208 205 202 199 112
Bloc 211
311 308 305 302 301 214
etc
Bloc 109
211 208 205 202 199 112
Bloc 211
311 308 305 302 301 214
Bloc 612
211 208 205 202 199 112
Le Buffer Cache
Ce qui est très important car les disques sont très lents relativement au CPU et un noyau qui
se chargerait de toutes les entrées/sorties serait d’une grande lenteur et l’unité de traitement ne
serait effectivement utilisée qu’à un faible pourcentage (voir Historique).
Deux idées pour réduire le nombre des accès disques :
1. bufferiser les différentes commandes d’écriture et de lecture de façon à faire un accès disque
uniquement pour une quantité de données de taille raisonnable (un bloc disque).
2. Eviter des écritures inutiles quand les données peuvent encore être changées (écriture différées).
17
18 CHAPITRE 3. LE BUFFER CACHE
Entête de Bloc
# de disque
# de bloc
statut
tête de liste b1 b2 bn
tête de liste b2 bn
L’insertion dans la liste des tampons libres se fait en fin de liste, la suppression (allocation
du tampon à un bloc donné) se fait en début de liste, ainsi le tampon alloué est le plus vieux
tampon libéré2 . Ceci permet une réponse immédiate si le bloc correspondant est réutilisé avant
que le tampon ne soit alloué à un autre bloc.
bloc# 0 mod 4 28 4 64
bloc# 1 mod 4 17 5 97
bloc# 2 mod 4 98 50 10
bloc# 3 mod 4 3 35 99
liste libres
{
if (tampon dans sa hash liste)
{
if (tampon actif )
{
[5] sleep attente de la liberation du tampon
continuer
}
[1] verrouiller le tampon
retirer le tampon de la liste des tampons libres
retourner le tampon
}
else /* n’est pas dans la hash liste */
{
if (aucun tampon libre )
{
[4] sleep attente de la liberation d’un tampon
continuer
}
retirer le tampon de la liste libre
[3] if (le tampon est a ecrire)
{
lancer la sauvegarde sur disque
continuer
}
[2] retirer le buffer de son ancienne liste
de hashage, le placer sur la nouvelle
retourner le tampon
}
}
}
20 CHAPITRE 3. LE BUFFER CACHE
bloc# 0 mod 4 28 4 64
bloc# 1 mod 4 17 5 97
bloc# 2 mod 4 98 50 10
bloc# 3 mod 4 3 35 99
liste libres
bloc# 0 mod 4 28 4 64
bloc# 1 mod 4 17 5 97 41
bloc# 2 mod 4 98 50 10
bloc# 3 mod 4 35 99
liste libres
bloc# 0 mod 4 28 4 64
bloc# 1 mod 4 17 5 97
writing
bloc# 2 mod 4 98 50 10 18
bloc# 3 mod 4 3 35 99
writing
liste libres
bloc# 0 mod 4 28 4 64
bloc# 1 mod 4 17 5 97
bloc# 2 mod 4 98 50 10
bloc# 3 mod 4 3 35 99
liste libres
bloc# 0 mod 4 28 4 64
bloc# 1 mod 4 17 5 97
bloc# 2 mod 4 98 50 10
bloc# 3 mod 4 3 35 99
liste libres
Fig. 3.8 – Scénario 5- Demande pour le bloc 17 qui est déjà utilisé.
22 CHAPITRE 3. LE BUFFER CACHE
Chapitre 4
La bibliothèque standard
#include <stdio.h>
main()
{
int i;
23
24 CHAPITRE 4. LA BIBLIOTHÈQUE STANDARD
}
}
Compilé,(a.out), cela donne les deux sorties suivantes :
$ a.out
l’entree standard est ouverte
$ a.out <&- # fermeture de l’entree standard en ksh
l’entree standard est fermee
De même printf échoue si la sortie standard est fermée.
FILE *f;
...
if ((f = fopen("toto", "r")) == NULL)
{
fprintf(stderr, "impossible d’ouvrir toto\n");
exit(1);
}
...
La fonction retourne un pointeur sur un descripteur du fichier ouvert ou NULL en cas d’échec,
(accès interdit, création impossible, etc).
#include <stdio.h>
FILE *tmpfile(void);
crée et ouvre en écriture un nouveau fichier temporaire, qui sera détruit (un unlink est réalisé
immédiatement) à la fin de l’exécution du processus, attention le descripteur est la aussi hérité
par les fils, et il faut en gérer le partage. Cette fonction utilise la fonction
Les 6 dernier caractère du chemin patron doivent être ”XXXXXX” il seront remplacé par une
chaine rendant le nom unique, ce chemin sera utilisé pour ouvrir un fichier temporaire avec l’option
création et echec sur création avec les droit 0600 ce qui permet d’éviter des troux de sécurité. La
fonction retourne le descripteur. Attention mkstemp n’assure pas que le fichier sera détruit après
utilisation comme c’etait le cas avec tmpfile , par contre il devient très difficile de réaliser une
attaque sur les fichiers temporaire créer par mkstemp.
#include <stdio.h>
int fwrite(void *add, size_t ta, size_t nbobjets, FILE *f);
Ecrit nbobjets de taille ta qui se trouvent à l’adresse add dans le fichier de descripteur f.
#include <stdio.h>
int fread(void *add, size_t ta, size_t nbobjets, FILE *f);
Lit nbobjets de taille ta dans le fichier de descripteur f et les place à partir de l’adresse add
en mémoire.
Attention : La fonction fread retourne 0 si l’on essaye de lire au delà du fichier. Pour écrire
une boucle de lecture propre on utilise la fonction feof(FILE *) :
int n[2];
while (fread(n, sizeof(int), 2, f), !feof(f))
printf("%d %d \n", n[0], n[1]);
26 CHAPITRE 4. LA BIBLIOTHÈQUE STANDARD
#include <stdio.h>
int fseek(FILE *f, long pos, int direction);
Manipulations ponctuelles
La fonction suivante permet de vider le tampon associé au FILE * f :
#include <stdio.h>
fflush(FILE *f);
En écriture force la copie du tampon associé à la structure f dans le tampon système (ne garantit
pas l’écriture en cas d’interruption du système !).
En lecture détruit le contenu du tampon, si l’on est en mode ligne uniquement jusqu’au premier
caractère ’\n’.
Attention : Il ne faut pas appeler cette fonction après l’allocation automatique réalisée par la
bibliothèque standard après le premier appel à une fonction d’entrée-sortie sur le fichier.
Il est fortement conseillé que la zone mémoire pointée par adresse soit au moins d’une taille égale
à taille.
Seul un passage au mode bufferisé en ligne ou non bufferisé peut être réalisé après l’allocation
automatique du tampon, au risque de perdre ce tampon (absence d ’appel de free). Ce qui permet
par exemple de changer le mode de bufferisation de la sortie standard après un fork. Attention
ce peut être dangereux, pour le contenu courant du tampon comme le montre l’exemple suivant.
#include <stdio.h>
main()
{
printf("BonJour ");
switch(fork())
{
case -1 :
exit(1);
case 0 :
printf("je suis le fils");
/* version 1 sans la ligne suivante version 2 avec */
setbuffer(stdout, NULL, 0);
sleep(1);
printf("Encore le fils");
break;
default :
printf("je suis le pere");
sleep(2);
}
printf("\n");
}
version 1
fork_stdlib
BonJour je suis le fils Encore le fils
BonJour je suis le pere
version 2
Encore le fils
BonJour je suis le pere
sans faire un wait). Ce mécanisme est très coûteux. Attention la commande system bloque les
signaux SIGINT et SIGQUIT, il faut analyser la valeur de retour de system de la même façons
que celle de wait. Il est conseillé de bloquer ces deux signaux avant l’appel de system .
#include <stdlib.h>
void _exit(int valeur);
#include <stdlib.h>
void exit(int valeur);
la fonction exit :
– lance les fonctions définies par atexit.
– ferme l’ensemble des descripteurs ouverts grâce à la bibliothèque standard (fopen).
– détruit les fichiers fabriqués par la primitive tmpfile
– appelle exit avec valeur.
atexit La primitive atexit permet de spécifier des fonctions à appeler en fin d’exécution, elle
sont lancées par exit dans l’ordre inverse de leur positionnement par atexit.
#include <stdlib.h>
int atexit(void (*fonction) (void ));
Exemple :
void bob(void) {printf("coucou\n");}
void bib(void) {printf("cuicui ");}
main(int argc)
{
atexit(bob);
atexit(bib);
if (argc - 1)
exit(0);
else
_exit(0);
}
$ make atexit
cc atexit.c -o atexit
$ atexit
$ atexit unargument
cuicui coucou
$
4.6. GESTION DES ERREURS 31
avec les mêmes restrictions que pour les shells sur le contenu du répertoire (impossible de
détruire un répertoire non vide).
32 CHAPITRE 4. LA BIBLIOTHÈQUE STANDARD
Chapitre 5
Les appels système d’entrées-sorties ou entrées-sorties de bas niveau sont rudimentaires mais
polymorphes, en effet c’est eux qui permettent d’écrire des programmes indépendamment des sup-
ports physiques sur lesquels se font les entrées/sorties et de pouvoir facilement changer les supports
physiques associés a une entrée-sortie.
5.1 open
#include <fcntl.h>
int open(char *ref, int mode, int perm);
33
34
données
0
1
2 1 rd 3
vers
Dans le le buffer
1 wr Dans le noyau cache et
processus
1 wr le disque
inodes en mémoire
descripteurs
1 rw 1
0
1
2 1 rd 3
vers
fd
le buffer
1 wr cache et
1 wr le disque
fd=open("toto",O_RDWR |O_CREAT,0666);
Fig. 5.2 – Avant l’ouverture, descripteurs standard ouverts, puis après l’ouverture de ”toto”.
Le paramètre permission n’a de sens qu’à la création du fichier, il permet de positionner les
valeurs du champ mode de l’inode. Les droits effectivement positionnés dépendent de la valeur
de umask, grace à la formule droits = perm & ~ umask. La valeur par défaut de umask est 066
(valeur octale).
La valeur de retour de open est le numéro dans la table de descripteurs du processus qui a été
utilisé par open. Ce numéro est appelé descripteur de l’ouverture. Ce descripteur est utilisé dans
les autres appels système pour spécifier l’ouverture de fichier sur laquelle on veut travailler1 , et -1
en cas d’échec de l’ouverture.
5.2 creat
Création d’un fichier et ouverture en écriture.
int creat(char *reference, int permissions);
5.3 read
int nbcharlus = read(int d, char *tampon, int nbalire)
descripteur entrée de la table des descripteurs correspondante au fichier dans lequel doit être
effectuée la lecture (fourni par open).
nbalire nombre de caractères à lire dans le fichier.
tampon un tableau de caractères alloué par l’utilisateur. Les caractères lus sont placés dans ce
tampon.
nbcharlus nombre de caractères effectivement lus, ou -1 en cas d’échec de l’appel système, (droits,
...), la fin de fichier est atteinte quand le nombre de caractères lus est inférieur au nombre
de caractères demandés.
Déroulement :
1. Vérification du descripteur −→ accès aux tables système.
2. Droits (mode adéquat)
3. Grâce à l’inode le système obtient les adresses du (des) bloc(s) contenant les données à lire.
Le système effectue la lecture de ces blocs.
4. Le système recopie les données du buffer cache vers le tampon de l’utilisateur.
5. Le curseur dans le fichier est remit à jour dans l’entrée de la table des fichiers ouverts.
6. Le système renvoie le nombre de caractères effectivement lus.
5.4 write
int nbcecrits = write(int desc, char *tampon, int nbaecrire);
Même déroulement que read mais avec une allocation éventuelle de bloc-disque dans le cas
d’un ajout au-delà de la fin du fichier.
Dans le cas où l’appel concerne un périphérique en mode caractère : le système active la fonction
write (réciproquement read pour une lecture) du périphérique qui utilise directement l’adresse
du tampon utilisateur.
5.5. LSEEK 37
Remarquons ici encore le polymorphisme de ces deux appels système qui permet de lire et d’écrire
sur une grande variété de périphériques en utilisant une seule syntaxe. Le code C utilisant l’ap-
pel système marchera donc indifféremment sur tous les types de périphériques qui sont définis
dans le système de fichier. Par exemple, il existe deux périphériques ”logiques” qui sont /dev/null
et /dev/zéro (que l’on ne trouve pas sur toutes les machines). Le premier est toujours vide en
lecture et les écritures n’ont aucun effet (il est donc possible de déverser n’importe quoi sur ce
périphérique). Le deuxième fournit en lecture une infinité de zéro et n’accepte pas l’écriture.
5.5 lseek
#include <fcntl.h>
off_t lseek(int d, off_t offset, int direction)
lseek permet de déplacer le curseur de fichier dans la table des fichiers ouverts du système.
offset un déplacement en octets.
d le descripteur.
direction une des trois macros L SET, L INCR, L XTND.
L SET la nouvelle position est offset sauf si offset est supérieur à la taille du fichier, auquel cas
la position est égale à la taille du fichier. Si l’offset est négatif, alors la position est zéro.
L INCR la position courante est incrémentée de offset place (même contrainte sur la position
maximum et la position minimum).
L XTND Déplacement par rapport à la fin du fichier, cette option permet d’augmenter la taille
du fichier (ne pas créer de fichiers virtuellement gros avec ce mécanisme, ils posent des
problèmes de sauvegarde).
La valeur de retour de lseek est la nouvelle position du curseur dans le fichier ou -1 si l’appel
a échoué.
tempout = open("sortie_temporaire",1);
oldout = dup(1);
close(1);
newout = dup(tempout); /* renvoie 1 */
write(1,"xxxx",4); /* ecriture dans le fichier temporaire */
38 CHAPITRE 5. APPELS SYSTÈME DU SYSTÈME DE GESTION DE FICHIER
close(tempout);
close(1);
newout = dup(oldout);
close(oldout);
Il est aussi possible de choisir le descripteur cible avec
int ok = dup2(int source, int destination);
Recopie du descripteur source dans l’entrée destination de la table des descripteurs. Si
destination désigne le descripteur d’un fichier ouvert, celui-ci est préalablement fermé avant
duplication. Si destination n’est pas un numéro de descripteur valide, il y a une erreur, retour
-1.
5.7 close
Fermeture d’un fichier.
int ok = close(descripteur);
5.8 fcntl
L’appel système fnctl permet de manipuler les ouverture de fichier après l’ouverture, bien
sur il n’est pas possible de changer le mode d’ouverture (lecture/écriture/lecture-écriture) après
l’ouverture.
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
int fcntl(int desc, int commande);
int fcntl(int desc, int commande, long arg);
int fcntl(int desc, int commande, struct flock *verrou);
L’appel système fnctl permet de positionner des verrous de fichier voire le chapitre 12. L’appel
système fnctl permet la manipulation de certains des drapeaux d’ouverture :
O APPEND
5.8. FCNTL 39
dup2(fd,1);
descripteurs
2 rw 1
0
1
2 1 rd 2
fd
1 wr
close(fd);
1 rw 1
0
1
2 1 rd 2
1 wr
O NONBLOCK
O ASYNC
O DIRECT
L’appel système fnctl permet gerer les signaux associés aux entrée asyncrones.
40 CHAPITRE 5. APPELS SYSTÈME DU SYSTÈME DE GESTION DE FICHIER
Chapitre 6
Les processus
le noyau
P1 P5
table des processus
P1
les processus
P5
41
42 CHAPITRE 6. LES PROCESSUS
int fork(void);
Tous les processus sauf le processus d’identification 0, sont créés par un appel à fork.
Le processus qui appelle le fork est appelé processus père.
Le nouveau processus est appelé processus fils.
Le processus de PID=1 de nom init est l’ancêtre de tous les autres processus (le processus 0
ne réalisant plus de fork()), c’est lui qui accueille tous les processus orphelins de père (ceci a fin
de collecter les information à la mort de chaque processus).
– Eventuellement d’autres sections : Table des symboles pour le débugeur, Images, ICONS,
Table des chaı̂nes, etc.
Pour plus d’informations se reporter au manuel a.out.h sur la machine.
Pile
Données non-initialisées } initialisée à zéro
Données initialisée par exec
lu par exec
Texte
Adresse Basse =0
Le code du programme gère les extensions de pile (appel ou retour de fonction), c’est le noyau qui
alloue l’espace nécessaire à ces extensions. Sur certains systèmes on trouve une fonction alloca()
qui permet de faire des demandes de mémoire sur la pile.
Un processus UNIX pouvant s’exécuter en deux modes (noyau, utilisateur), une pile privée sera
utilisée dans chaque mode.
La pile noyau sera vide quand le processus est en mode utilisateur.
Le tas est une zone où est réalisée l’allocation dynamique avec les fonctions Xalloc().
Les structures de régions de la table des régions contiennent des informations sur le type, les
droits d’accès et la localisation (adresses en mémoire ou adresses sur disque) de la région.
Seule la zone u du processus courant est manipulable par le noyau, les autres sont inacces-
sibles. L’adresse de la zone u est placée dans le mot d’état du processus.
Mémoire Centrale
Fig. 6.3 – Table des régions, table des régions par processus
copie
copiée
Table des processus
fork()
père = Mémoire Centrale
fils =
ancienne
entrée
exec()
Mémoire Centrale
pouvoir exécuter un nouveau processus, il faut pouvoir sauvegarder le contexte d’unité centrale
du processus courant (mot d’état), puis charger le nouveau mot d’état du processus à exécuter.
Cette opération délicate réalisée de façon matérielle est appelée commutation de mot d’état.
Elle doit se faire de façon non interruptible ! Cette ”Super instruction” utilise 2 adresses qui sont
respectivement :
l’adresse de sauvegarde du mot d’état
l’adresse de lecture du nouveau mot d’état
Le compteur ordinal faisant partie du mot d’état, ce changement provoque l’exécution dans le
nouveau processus.
C’est le nouveau processus qui devra réaliser la sauvegarde du contexte global. En général c’est
le noyau qui réalise cette sauvegarde, le noyau n’ayant pas un contexte du même type.
Le processus interrompu pourra ainsi reprendre exactement où il avait abandonné.
Les fonctions setjmp/longjmp permettent de sauvegarder et de réinitialiser le contexte d’unité
central du processus courant, en particulier le pointeur de pile.
Sauvegarde
du contexte
du Processus 1
traitement
Chargement
du contexte
Acquittement
du processus 2
Commutations d’état
avancer.
48 CHAPITRE 6. LES PROCESSUS
Exécution Examiner
en mode utilisateur et traiter
1 les signaux
Appel system
interruption
Retour au
Exécution mode utilisateur
en mode noyau
gestion 2
interruption préemption
exit 7 Préempté
zombie
ordonancement
9 du processus
sleep
Examiner
les signaux
3 Prêt et en mémoire
wakeup
Endormi 4 mémoire
en mémoire suffisante
swapout swapout Création
swapin fork
8
centrale
swap mémoire
insuffisante
wakeup
Endormi 6 5 Prêt
en zone de swap en zone de swap
9. zombie le processus vient de réaliser un exit, il apparaı̂t uniquement dans la table des
processus où il est conservé le temps pour son processus père de récupèrer le code de retour
et d’autres informations de gestion (coût de l’exécution sous forme de temps, et d’utilisation
des ressources ).
L’état zombie est l’état final des processus, les processus restent dans cet état jusqu’à ce que leur
père lise leur valeur de retour (exit status).
divers des compteurs utilisés pour la comptabilité (pour faire payer le temps CPU utilisé) et
que l’on peut manipuler par la commande alarm, des données utilisées par l’implémentation
effective du système, etc.
6.13 La zone u
La zone u de type struct user définie dans <sys/user.h> est la zone utilisée quand un pro-
cessus s’exécute que ce soit en mode noyau ou mode utilisateur. Une unique zone u est accessible
à la fois : celle de l’unique processus en cours d’exécution (dans un des états 1 ou 2).
Contenu de la zone u :
pointeur sur la structure de processus de la table des processus.
uid réel et effectif de l’utilisateur qui détermine les divers privilèges donnés au processus, tels
que les droits d’accès à un fichier, les changements de priorité, etc.
Compteurs des temps (users et system) consommés par le processus
Masque de signaux Sur système V sous BSD dans la structure proc
Terminal terminal de contrôle du processus si celui-ci existe.
erreur stockage de la dernière erreur rencontrée pendant un appel système.
retour stockage de valeur de retour du dernier appel système.
E/S les structures associées aux entrées-sorties, les paramètres utilisés par la bibliothèque stan-
dard, adresses des buffers, tailles et adresses de zones à copier, etc.
”.” et ”/” le répertoire courant et la racine courante (c.f. chroot())
la table des descripteurs position variable d’un implémentation à l’autre.
limites de la taille des fichiers de la mémoire utilisable etc 41 (c.f. ulimit en Bourne shell et limit
en Csh ).
umask masque de création de fichiers.
times remplit la structure pointée par buffer avec des informations sur le temps machine uti-
lisé dans les état 1 et 2.
La structure? :
struct tms {
clock_t tms_utime; /* user time */
clock_t tms_stime; /* system time */
6.14. ACCÈS AUX STRUCTURES PROC ET USER DU PROCESSUS COURANT 51
contient des temps indiqués en microsecondes 10-6 secondes, la précision de l’horloge est par
defaut sur les HP9000 700/800 de 10 microsecondes.
permet de définir un nouveau point de départ pour les références absolues (commençant par
/). La référence .. de ce répertoire racine est associée à lui-même, il n’est donc pas possible de
sortir du sous-arbre défini par chroot. Cet appel est utilisé pour rsh et ftp, et les comptes pour
invités.
Les appels suivants permettent de changer le répertoire de travail de référence “.” et donc
l’interprétation des références relatives :
L’appel getpid() retourne le PID du processus courant, getppid le PID du processus père,
getpgrp le PID du groupe du processus courant, getpgrp2 le PID du groupe du processus pid (si
pid=0 alors équivalent à getpgrp).
#include <unistd.h>
int setuid(uid_t uid);
int setgid(gid_t gid);
Fonctionnement :
si euid == 0 (euid de root) les trois uid sont positionnés à la valeur de uid
sinon si uid est égal à ruid ou suid alors euid devient uid. ruid et suid ne changent pas. sinon rien !
pas de changements.
Syntaxe identique pour setgid et gid.
La commande setreuid() permet de changer le propiétaire réel du processus, elle est utilisé
pendant le login, seul le super utilisateur peut l’exécuter avec succès.
52 CHAPITRE 6. LES PROCESSUS
Les deux appels permettent de changer la taille du processus. L’adresse manipulée par les deux
appels est la première adresse qui est en dehors du processus. Ainsi on réalise des augmentations
de la taille du processus avec des appels à sbrk et on utilise les adresses retournées par sbrk pour
les appels à brk pour réduire la taille du processus. On utilisera de préférence pour les appels
à sbrk des valeurs de incr qui sont des multiples de la taille de page. Le système réalisant des
déplacement du point de rupture par nombre entier de pages (ce qui est logique dans un système de
mémoire paginé). A ne pas utiliser en conjonction avec les fonctions d’allocation standard malloc,
calloc, realloc, free.
#include <unistd.h>
int nice(int valeur);
La commande shell renice priorite -p pid -g pgrp -u user permet de changer le nice
d’un processus actif.
#include <sys/stat.h>
mode_t umask(mode_t mask);
int execve(const char *file, char * const argv[], char * const envp[]);
int execlp( const char *file,const char *arg0, ... , NULL );
int execvp(const char *file, char * const argv[]);
Informations conservées par le processus : PID PPID PGID ruid suid (pour l’euid cf le
setuidbit de chmod ), nice, groupe d’accès, catalogue courant, catalogue “/”, terminal de
contrôle, utilisation et limites des ressources (temps machine, mémoire, etc), umask, masques
des signaux, signaux en attente, table des descripteurs de fichiers, verrous, session.
Quand le processus exécute dans le nouvel exécutable la fonction :
main(int argc, char **argv,char **envp)
argv et env sont ceux qui ont été utilisés dans l’appel de execve.
La sélection dans le temps des processus pouvant accèder à une ressource est un problème dit
d’ordonnancement. Nous présentons ici :
– le cas général
– les besoins et les problèmes
et nous décrirons des solutions que l’on trouve sous UNIX pour différents problèmes d’ordonnan-
cement.
Les algorithmes d’ordonnancement réalisent la sélection parmi les processus actifs de celui
qui va obtenir l’utilisation d’une ressource, que ce soit l’unité centrale, ou bien un périphérique
d’entrée-sortie.
Pour l’unité centrale notre but est de maximiser débit et taux utile de l’unité centrale :
le débit est le nombre moyen de processus exécutés en un temps donné.
le taux utile est la proportion de temps réellement utilisée pour exécuter des processus utilisa-
teurs.
Un exemple :
Soient 2 processus A et B de même comportement 30 périodes de deux seconde :
1 seconde d’activité
1 seconde d’inactivité
AIAIAIAIAIAIAIAIAIAIAIAIAIAIAIAIAIAIAI
Si l’on exécute les deux processus consécutivement on obtient un débit de 1 processus par
minute, et un taux utile de 50%. Si l’on entrelace les périodes actives et inactives des deux processus
on obtient un débit de 2 processus par minute et un taux d’utilisation de 100%.
Pour une autre ressource d’autres critères seront utilisés.
55
56 CHAPITRE 7. L’ORDONNANCEMENT DES PROCESSUS
200
150
100
50
0
1 4 8 12 16 20 24
7.1.1 Famine
Notre première tâche est d’affecter une ressource (l’UC par exemple) à un unique processus à
la fois (exclusion mutuelle) et s’assurer de l’absence de famine.
famine : un processus peut se voir refuser l’accès à une ressource pendant un temps indéterminé,
il est dit alors que le processus est en famine.
Un système qui ne crée pas de cas de famine : fournira toujours la ressource demandée par un
processus, au bout d’un temps fini.
Si on prend le cas des périphériques (tels que les disques) l’ordonnancement peut se faire de
façon simple avec par exemple une file d’attente (FIFO).
Pour l’unité centrale on va devoir utiliser des structures de données plus complexes car nous
allons avoir besoin de gérer des priorités. C’est par exemple, autoriser l’existence de processus qui
évitent la file d’attente. La structure de données utilisée peut parfaitement être une file, une liste,
un arbre ou un tas, ceci en fonction de l’élément-clef de notre algorithme de sélection (âge, priorité
simple, priorité à plusieurs niveaux, etc).
Cette structure de données doit nous permettre d’accéder à tous les processus prêts (éligibles).
Si on utilise trop de temps (1 milli-seconde) pour sélectionner cet élu, le taux utile décroı̂t très
rapidement (ici on perd 9% du temps d’unité centrale).
Par contre l’ordonnancement à long terme peut être plus long car il a lieu moins souvent (toutes
les secondes par exemple). La conception de l’ordonnanceur à long terme est faite dans l’optique
d’obtenir un ordonnanceur à court terme rapide.
solution −→ préemption.
La préemption est la possibilité qu’a le système de reprendre une ressource à un processus sans
que celui-ci ait libéré cette ressource.
Ceci est impossible sur bon nombre de ressources. Lesquelles ?
Une autre possibilité est de partager les quantums de temps sur les différentes queues.
Il est aussi possible de réaliser différents algorithmes de scheduling sur les différentes queues :
– Round Robin sur les processus interactifs
– FCFS sur les gros calculs en tâche de fond.
swapper
non
Interne au Noyau
interruptibles Disk I/O
Buffer
Attentes de :
Inode
interruptibles tty input
tty output
enfants
priorité limite
niveau 0
niveau 1
Utilisateurs
niveau N
Dans les listes internes au noyau, de simples files d’attente sont utilisées avec la possibilité de
doubler les processus endormis de la même liste (en effet seul le processus réveillé par la fin de son
entrée/sortie est éligible).
Pour les processus utilisateurs, la même règle est utilisée mais avec préemption et la règle du
tourniquet.
C’est à dire, on calcul une priorité de base qui est utilisée pour placer le processus dans la
bonne file d’attente.
Un processus qui utilise l’unité centrale voit augmenter sa priorité.
Un processus qui libère l’unité centrale pour demander une entrée/sortie ne voit pas sa priorité
changer.
Un processus qui utilise tout sont quantum de temps est préempté et placé dans une nouvelle file
d’attente.
consommation d’unité central du processus. Pour que cette valeur ne devienne pas trop pénalisante
sur le long terme (comme pour un shell) elle est atténuée toute les secondes grâce à la formule
suivante :
2 × load
P cpu = × P cpu + P nice
2 × load + 1
la valeur de load (la charge) est calculée sur une moyenne du nombre de processus actifs
pendant une minute.
Pour ne pas utiliser trop de ressources, les processus qui sont en sommeil (sleep) voient leur
P cpu recalculé uniquement à la fin de leur période de sommeil grâce à la formule :
µ ¶ sleep time
2 × load
P cpu = × P cpu
2 × load + 1
la variable sleep time étant initialisée à zéro puis incrémentée une fois par seconde.
Le choix de l’ordre de ces priorités est très important, en effet un mauvais choix peut entraı̂ner
une diminution importante des performances du système.
Il vaut mieux que les processus en attente d’un disque soient plus prioritaires que les processus
en attente d’un buffer, car les premiers risquent fort de libérer un buffer après leur accès disque
(de plus il est possible que ce soit exactement le buffer attendu par le deuxième processus). Si la
priorité était inverse, il deviendrait possible d’avoir un interblocage ou une attente très longue si
le système est bloqué par ailleurs.
De la même façons, le swappeur doit être le plus prioritaire et non interruptible −→ Si un
processus est plus prioritaire que le swappeur et qu’il doit être swappé en mémoire ...
En Demand-Paging le swappeur est aussi le processus qui réalise les chargements de page, ce
processus doit être le plus prioritaire.
Chapitre 8
La mémoire
La mémoire est un tableau à une dimension de mots machines (ou d’octets), chacun ayant une
adresse propre. Les échanges avec l’extérieur se font en général par des lectures ou des écritures à
des adresses spécifiques.
Le système Unix est multi-tâche,ceci pour maximiser l’utilisation du cpu. Cette technique
pose comme condition obligatoire que la mémoire centrale soit utilisée et/ou partagée entre les
différentes tâches.
Les solutions de gestion de la mémoire sont très dépendantes du matériel et ont mis longtemps
à évoluer vers les solutions actuelles. Nous allons voir plusieurs approches qui peuvent servir dans
des situations particulières .
La mémoire est le point central dans un système d’exploitation, c’est à travers elle que l’unité
centrale communique avec l’extérieur.
Mémoire
Registres volatile
Coût par bit croissant
Mémoire cache vitesse d’accès croissant
Mémoire centrale
capacité de stockage
Disques
décroissante
Mémoire
Bandes magnétiques permanente
61
62 CHAPITRE 8. LA MÉMOIRE
MEMOIRE
CENTRALE 106-107 10-7 1
64Kilos octets
utilisateurs
FFFF
le noyau
Registre (moniteur)
Barrière
un programme
utilisateur
FFFF
Registre
Barrière
Intéruption
Registre
Base
1400
unité mémoire
+
centrale 0346 1746
le noyau
(moniteur)
Registre
Barrière
un programme
utilisateur
FFFF
Moniteur
Barrière
1 swap out P1
Zone
utilisateur
P2
2 swap in
Pour continuer à fournir cette possibilité le registre barrière est transformé en registre de base
(relocation) . A chaque utilisation d’une adresse logique du programme, on ajoute à cette adresse
la valeur du registre de base pour trouver l’adresse physique. L’utilisateur ne connaı̂t plus les
adresses physiques. Il travaille uniquement avec des adresses logiques (xdb).
Le moniteur a évidemment une valeur nulle pour son registre de base et donc peut adresser
toute la mémoire. Le changement de la valeur du registre de base se fait de façon protégée en
mode moniteur.
Ces deux systèmes de protection de la mémoire sont clairement mono-processus. Seul le mo-
niteur peut être protégé par ces mécanismes, il n’est pas possible de protéger les processus entre
eux.
8.1.5 Le swap
Il est possible avec les registres barrière ou les registres de base d’écrire des systèmes temps
partagé, en utilisant le mécanisme de swap (échange).
Swapper, c’est échanger le contenu de la mémoire centrale avec le contenu d’une mémoire
secondaire. Par extension swapper devient l’action de déplacer une zone mémoire de la mémoire
vers le support de swap (en général un disque) ou réciproquement du périphérique de swap vers
la mémoire.
Le système va réaliser cet échange à chaque changement de contexte. Les systèmes de swap
utilisent une mémoire secondaire qui est en général un disque mais on peut utiliser d’autre supports
secondaires plus lents ou plus rapides comme des bandes ou mémoires secondaires (non accessibles
par l’unité de traitement).
Bas Haut
Interruption Interruption
de taille effective d’un processus, ce qui permet d’améliorer le débit mais cela impose que toutes
les augmentations ou réductions de taille d’un processus utilisateur soient réalisée par un appel
système (sbrk) afin que le noyau connaisse à tout moment la taille réelle de chaque processus.
8.1.9 Contraintes
Le swap introduit d’autres contraintes : un processus doit être en préempté actif pour être
swappé, c’est à dire n’être en attente d’aucune entrée-sortie. En effet, si P1 demande une E/S et
pendant cette demande il y a échange de P1 et P2, alors la lecture demandée par P1 a lieu dans
les données de P2.
Deux registres de relocation Base et Limit, on travaille avec des adresses logiques Limit donne
la valeur maximale d’une adresse logique et Base donne la position en mémoire de l’adresse logique
8.2. ORDONNANCEMENT EN MÉMOIRE DES PROCESSUS 67
limite Base
unité non
A > limite + mémoire
centrale
oui
Interruption
1500k 1500k
p4 p4
1900k
1900k
zéro.
0 0
Moniteur Moniteur
40k 40k
P5 P5
90k 90k
100k
P4
P4
160k
170k P3
200k 190k
P3 Zone contiguë
230k de 66k
256k 256k
0 0 0 0 0
Moniteur Moniteur Moniteur Moniteur Moniteur
40k 40k 40k 40k 40k
P1 P1 P1 P5
90k
100k 100k 100k 100k 100k
P4 P4 P4
P2
170k 170k 170k
Adresse Adresse
logique physique
unité p d f d mémoire
centrale
0
1 page 0
page 0 0 1 2
page 1 1 4 3 page 2
page 2 2 3 4 page 1
page 3 3 7 5
6
mémoire Table des pages 7 page 3
logique
mémoire
physique
Un avantage des pages est une plus grande simplicité du partage de la mémoire entre différents
processus. En particulier quand plusieurs processus partagent le même code. La page qui contient
du code utilisé par les processus sera partageable et protégée en écriture.
Sous Unix le compilateur produit automatiquement des programmes dont la partie code est
partageable.
Adresse
logique Adresse
l b physique
unité p d b d mémoire
centrale
oui
d<l
non
interruption erreur d’adresse
La mémoire virtuelle
Les méthodes de gestion mémoire que nous venons de voir ont toutes un défaut majeur qui est
de garder l’ensemble du processus en mémoire, ce qui donne :
– un coût en swap important
– Impossibilité de créer de très gros processus.
Les méthodes de mémoire virtuelle permettent d’exécuter un programme qui ne tient pas entièrement
en mémoire centrale !
Nous avons commencé par présenter des algorithmes de gestion de la mémoire qui utilisent le
concept de base suivant :
l’ensemble de l’espace logique adressable d’un processus doit être en mémoire pour pouvoir exécuter
le processus.
Cette restriction semble à la fois raisonnable et nécessaire, mais aussi très dommageable car
cela limite la taille des processus à la taille de la mémoire physique.
71
72 CHAPITRE 9. LA MÉMOIRE VIRTUELLE
Les overlay nécessitent quelques adaptations de l’éditeur de liens et des mécanismes de reloca-
tion.
Nous partons d’un système de swap où la mémoire est découpée en pages. Comme pour le
swap, quand un programme doit être exécuté nous le chargeons en mémoire (swap in) mais au lieu
de faire un swap complet, on utilise un ”swappeur paresseux” (lazy swapper).
Un swappeur paresseux charge une page uniquement si elle est nécessaire.
Que ce passe-t-il quand le programme essaie d’accéder à une page qui est hors mémoire ?
– le matériel va traduire l’adresse logique en une adresse physique grâce à la table des pages.
– tant que les pages demandées sont en mémoire, le programme tourne normalement, sinon si
la page est contenue dans l’espace des adresses logiques mais n’est pas chargée, il y a une
page fault.
En général, une erreur d’adresse est dûe à une tentative d’accès à une adresse extérieure
(invalide). Dans ce cas, le programme doit être interrompu, c’est le comportement normal d’un
système de swap.
Mais il est possible avec un swappeur paresseux que la page existe mais ne soit pas en mémoire
centrale, d’où les étapes suivantes dans ce cas :
On peut faire démarrer un processus sans aucune page en mémoire. La première Page Fault
aurait lieu à la lecture de la première instruction (l’instruction n’étant pas en mémoire).
Il faut réaliser une forme spéciale de sauvegarde de contexte, il faut garder une image de l’état
du processus qui vient d’effectuer une Page Fault mais de plus il faudra redémarrer (réexécuter)
l’instruction qui a placé le processus dans cet état, en effet il est possible que l’instruction ne se
soit pas terminé par manque de données.
Le système d’exploitation a ici un rôle important, c’est lui qui va réaliser le chargement de la
page manquante puis relancer le processus et l’instruction.
Les circuits nécessaires à la méthode de Demande Paging sont les mêmes que ceux que l’on
utilise pour un système de swap paginé, c’est-à-dire une mémoire secondaire et un gestionnaire de
pages (table des pages).
Par contre, la partie logicielle est beaucoup plus importante.
Enfin il faut que les instructions soient interruptibles, ce qui n’est pas toujours le cas sur
tous les processeurs et ce qui est fondamental, comme nous allons le voir sur des exemples :
add A,B in C
9.1. DEMAND PAGING 73
3 la page existe
en zone de swap
noyau
2 interruption
1 référence
load M
6 relancer
l’instruction table des
pages
disque
de
swap
5 mise à jours
de la table des
page du P. 4 swap in
de la page
mémoire fautive ...
9.1.1 Efficacité
Efficacité des performances de Demand Paging :
– gérer l’interruption
– swapper la page demandée
– relancer le processus
Ce qui coûte le plus cher est la recherche de la page sur le disque et son transfert en mémoire, ce
qui prend de l’ordre de 1 à 10 millisecondes.
Ce qui nous donne en prenant une vitesse d’accès mémoire de 1 microseconde et un temps de
gestion de page de 5 millisecondes un
Une erreur de page toutes les mille pages nous donne un temps effectif onze fois plus long que
l’accès standard.
Il faut réduire à moins d’une erreur de page tout les 100000 accès pour obtenir une dégradation
inférieure à 10
On comprend bien que les choix à faire sur des pages qu’il faut placer en mémoire sont donc
très importants.
Ces choix deviennent encore plus importants quand l’on a de nombreux utilisateurs et qu’il y a
sur-allocation de la mémoire, exécution concurrente de 6 processus de la taille supérieure ou égale
à la mémoire physique !
Si l’on suppose de plus que nos 6 programmes utilisent dans une petite séquence d’instructions
toutes les pages de leur mémoire logique, nous nous trouvons alors dans une situation de pénurie
de pages libres.
Maintenant il nous faut sélectionner une victime, c’est-à-dire, une des pages occupées de la
mémoire centrale qui sera swappée sur disque et remplacée par la page demandée.
9.2. LES ALGORITHMES DE REMPLACEMENT DE PAGE 75
Remarquons que dans ce cas-là notre temps de transfert est doublé, comme il faut à la fois lire
une page et sauvegarder une page sur disque (le temps de transfert disque est ce qui est le plus
coûteux dans la gestion d’une erreur de page).
Il est possible de réaliser des systèmes de demand segments, mais le lecteur avisé remarquera
rapidement les problèmes posés par la taille variable des segments.
On recherche l’algorithme qui réduit au mieux la probabilité d’occurrence d’une erreur de page.
Un algorithme est évalué en prenant une chaı̂ne de numéros de page et en comptant le nombre de
fautes de page qui ont lieu au cours de cette suite d’accès, et cela en fonction du nombre de pages
de mémoire centrale dont il dispose.
Pour illustrer les algorithmes de remplacement, nous utiliserons la suite de pages suivante :
7,0,1,2,0,3,0,4,2,3,0,3,2,1,2,0,1,7,0,1
et 3 pages en mémoire centrale.
Quand une victime doit être sélectionnée c’est la page la plus ancienne qui est sélectionnée.
7,0,1,2,0,3,0,4,2,3,0,3,2,1,2,0,1,7,0,1
7XX/70X/701/201-201/231/230/430/420/423/
023-023-023/013/012-012-012/712/702/701
7xx 70x 701 201 - 203 - 403 402 432 032 - - 132 - 102 - 107 -
On peut voir ceci comme une queue circulaire, où l’on avance sur les pages qui ont le bit à 1
(en le positionnant à zéro) jusqu’à ce que l’on trouve une page avec le bit d’utilisation à zéro.
Le partage ”équitable” : m pages de mémoire physique, n processus, m/n pages par processus
! On retrouve ici un problème proche de la fragmentation interne, un grand nombre de pages est
donné à un processus qui en utilise effectivement peu.
Si la mémoire est libre et assez grande, les deux processus sont grossièrement aussi rapides,
par contre si on lance dix exemplaires du premier, le temps d’attente est juste multiplié par 10.
Pour le deuxième, le temps d’attente est au moins multiplié par 100 (je n’ai pas attendu la fin de
l’exécution).
Mais avec le système de demand-paging, on peut introduire une nouvelle notion qui est la
”copie sur écriture” (copy on write). On ajoute à la structure de page de la table des pages des
indicateurs de ”copie sur écriture”. L’idée est de réaliser la copie de la page uniquement dans le
cas où l’un des processus qui peuvent y accèder réalise une écriture. Dans ce cas-là, la page est
recopiée avant l’écriture et le processus écrivain possède alors sa propre page.
L’intérêt de ce mécanisme est surtout visible dans le cas très fréquent où le fork est immédiatement
suivi par un exec. En effet, ce dernier va réaliser une libération de toutes les pages, il est donc
inutile de les recopier juste avant cette libération.
Le système BSD a introduit la première version de cette idée en partant de l’appel système
vfork() qui lui permet le partage totale de toutes les pages entre le processus père et le processus
fils sans aucune copie. L’intérêt est de pouvoir réaliser rapidement un execve sans avoir à recopier
l’espace d’adressage du processus père.
#include <sys/mman.h>
#include <sys/types.h>
L’adresse adr indique où doit être placé le fichier, cette adresse doit être une adresse de début
de page (un multiple de sysconf( SC PAGE SIZE)), si le paramètre est NULL alors le système
sélectionne l’adresse de placement qui est retournée par la fonction. L’intervalle de position
[offset, offset+len]
du fichier desc est placé en mémoire.
prot indique les protections d’accès sous HP-UX les protections suivantes sont disponible :
9.5. PROJECTION DE FICHIERS EN MÉMOIRE 79
--- PROT_NONE
r-- PROT_READ
r-x PROT_READ|PROT_EXECUTE
rw PROT_READ|PROT_WRITE
rwx PROT_READ|PROT_WRITE|PROT_EXECUTE
options indique si l’on veut que les écritures réalisées dans les pages contenant la projec-
tion soient partagées (MAP SHARED), ou au contraire qu’une copie sur écriture soit réalisée
(MAP PRIVATE).
La fonction munmap permet de libérer la zone mémoire d’adresse adr et de longueur len.
Pour une autre forme de mémoire partagée, voir le chapitre sur les IPC (sur le web).
exit(0) ;
Attention, quand vous utilisez mmap c’est de la mémoire, mais c’est a vous de gérer l’allignement.
Exemple :
char *p = map(...);
int *q = p+1; // warning
*q = 1 ; // problème d’allignement
#include <sys/mman.h>
int madvise(void *start, size_t length, int advice);
#include <stdio.h>
Le fichier main qui va charger la libraire puis appeler une fonction de cette libraire.
#include <stdio.h>
#include <dlfcn.h>
Les tubes sont un mécanisme de communication qui permet de réaliser des communications
entre processus sous forme d’un flot continu d’octets. Les tubes sont un des éléments de l’agrément
d’utilisation d’UNIX. C’est ce mécanisme qui permet l’approche filtre de la conception sous UNIX.
Mécanisme de communication lié au système de gestion de fichier, les tubes nommés ou non
sont des paires d’entrées de la table des fichiers ouverts, associées à une inode en mémoire gérée
par un driver spécifique. Une entrée est utilisée par les processus qui écrivent dans le tube, une
entrée pour les lecteurs du tube.
L’opération de lecture y est destructive !
L’ordre des caractères en entrée est conservé en sortie (premier entré premier
sorti).
Un tube a une capacité finie : en général le nombre d’adresses directes des inodes
du SGF (ce qui peut varier de 5 à 80 Ko).
#include <unistd.h>
int pipe(int p[2]);
On ne peut pas manipuler les descripteurs de tubes avec les fonctions et primitives : lseek,
ioctl, tcsetattr et tcgetattr, comme il n’y a pas de périphérique associé au tube (tout est
83
84 CHAPITRE 10. TUBES ET TUBES NOMMÉS
processus A
pipe(p)
Dans le noyau
descripteurs
inodes en mémoire
p[0]
2 0
1 rd
p[1]
1 wr
processus A ouvertures
fork() inodes
de fichiers
p[0]
2 rd 2 0
p[1]
processus B 2 wr
fils de A p[0]
p[1]
fait en mémoire).
Héritage d’un tube dans la figure 10.2 : le processus B hérite des descripteurs ouverts par son
père A et donc, ici, du tube.
Dans la Figure 10.3, les descripteurs associés aux tubes sont placés comme descripteurs 0 et
1 des processus A et B, c’est à dire la sortie de A et l’entrée de B. Les autres descripteurs sont
fermés pour assurer l’unicité du nombre de lecteurs et d’écrivains dans le tube.
10.3. LECTURE DANS UN TUBE 85
processus A ouvertures
dup2(1,p[1]) 1
close (p[0])
close (p[1])
1 rd
processus B 0
fils de A 1 wr
dup2(0,p[0])
close (p[0])
close (p[1])
Fig. 10.3 – Redirection de la sortie standard de A dans le tube et de l’entrée standard de B dans
le tube, et fermeture des descripteurs inutiles
Comportement de l’appel :
Ainsi un unique processus ne peut ouvrire à la fois en lecture et écriture un tube nommé.
Les signaux
signal est un numéro compris entre 1 et NSIG (défini dans <signal.h>) et pid le numéro du
processus.
Le processus visé reçoit le signal sous forme d’un drapeau positionné dans son bloc de contrôle.
Le processus est interrompu et réalise éventuellement un traitement de ce signal.
On peut considérer les signaux comme des interruptions logicielles, ils interrompent le flot nor-
mal d’un processus mais ne sont pas traités de façon synchrone comme les interruptions matérielles.
{
bit pendant;
89
90 CHAPITRE 11. LES SIGNAUX
void (*traitement)(int);
}
Remarque : comme pendant est un unique bit, si un processus reçoit plusieurs fois le même
signal avant de le prendre en compte, alors il n’y a pas mémorisation des réceptions successives,
un seul traitement sera donc réalisé.
Comme nous l’avons vu dans le graphe d’état des processus, la prise en compte des signaux
se fait au passage de l’état actif noyau à l’état actif utilisateur. Pourquoi la prise en compte de
signaux se fait-elle uniquement à ce moment là ?
Parce que
Une sauvegarde de la pile utilisateur et du contexte a été effectuée quand le processus est passé en
mode noyau. Il n’est pas nécessaire de faire un nouveau changement de contexte. Il est facile pour
traiter le signal de réaliser immédiatement une nouvelle augmentation de pile pour le traitement
du signal, de plus la pile noyau est vide (remarque : en POSIX, il devient possible de créer une
pile spéciale pour les fonctions de traitement de signaux).
L’appel à la fonction de traitement est réalisé de façon à ce qu’au retour de la fonction, le
processus continue son exécution normalement en poursuivant ce qui était en cours de réalisation
avant la réception du signal. Si l’on veut que le processus se poursuive dans un autre contexte (de
pile), il doit gérer lui-même la restauration de ce contexte.
La primitive longjmp peut permettre de réaliser des changements de contexte interne au pro-
cessus, grâce à un désempilement brutal.
Pendant ce changement d’état, la table de gestion des signaux du processus est testée pour la
présence d’un signal reçu mais non traité (c’est un simple vecteur de bit pour le bit pendant, et
donc testable en une seule instruction, ceci doit être fait rapidement comme le test de réception
d’un signal est souvent réalisé).
Si un signal a été reçu ( et qu’il n’est pas masqué), alors la fonction de traitement associée est
réalisée. Le masquage permet au processus de temporiser la mise en øeuvre du traitement.
Remarquez que l’on peut réécrire kill(0, signal) par kill(-getpid(), signal). Rappel :
les PID sont toujours positifs.
11.1. LA GESTION SIMPLIFIÉE AVEC LA FONCTION SIGNAL 91
11.1.1 Un exemple
Exemple pour rendre un programme insensible à la frappe du caractère de contrôle intr sur le
terminal de contrôle du processus.
void got_the_blody_signal(int n) {
signal(SIGINT, got_the_blody_signal);
printf(" gotcha!! your (%d) signal is useless \n");
}
main() {
signal(SIGINT, got_the_blody_signal);
printf(" kill me now !! \n");
for(;;);
}
signal(SIGINT, SIG_IGN);
traitement() {
printf("PID %d en a capture un \n", getpid());
92 CHAPITRE 11. LES SIGNAUX
main() {
int ppid;
signal(SIGINT,traitement);
if (fork()==0)
{/* attendre que pere ait realise son nice() */
sleep(5);
ppid = getppid(); /* numero de pere */
for(;;)
if (kill(ppid,SIGINT) == -1)
exit();
}
/* pere ralenti pour un conflit plus sur */
nice(10);
for(;;) pause(); <- reception du premier signal
/* pause c’est mieux qu’une attente active */
}
Le traitement normal est lié à la primitive wait qui permet de récupérer la valeur de retour
(exit status) d’un processus fils. En effet, la primitive wait est bloquante et c’est la réception du
signal qui va réveiller le processus, et permettre la fin de l’exécution de la primitive wait.
Un des problèmes de la gestion de signaux System V est le fait que le signal SIGCHLD est
reçu (raised) au moment de la pose d’une fonction de traitement.
#include <stdio.h>
#include <unistd.h> /* ancienne norme */
#include <signal.h>
main() {
if (fork()) { exit(0); /* creation d’un zombi */ }
signal(SIGCHLD, hand);
printf("ce printf n’est pas execute\n");
}
Sur les HP, un message d’erreur vous informe que la pile est pleine : stack growth failure.
Deuxième exemple :
#include <signal.h>
#include <sys/wait.h>
}
printf(" wait handler pid: %d status %d \n", pid, status);
return;
}
main() {
signal(SIGCHLD,hand); /* installation du handler */
if (fork() == 0)
{ /* dans le fils */
sleep(5);
exit(2);
}
/* dans le pere */
if ((pid = wait(&status)) == -1) /* attente de terminaison du fils */
{
perror("wait main ");
return ;
}
printf(" wait main pid: %d status %d \n", pid, status);
}
résultat :
Entree dans le handler
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME COMD
1 S 121 6792 6667 0 158 20 81ac180 6 49f5fc ttys1 0:00 sigchld
1 S 121 6667 6666 0 168 20 81ac700 128 7ffe6000 ttys1 0:00 tcsh
1 Z 121 6793 6792 0 178 20 81bda80 0 ttys1 0:00 sigchld
1 S 121 6794 6792 0 158 20 81ac140 78 4a4774 ttys1 0:00 sh
1 R 121 6795 6794 4 179 20 81bd000 43 ttys1 0:00 ps
wait handler pid: 6793 status 512 (2 * 256)
wait main: Interrupted system call
A la mort du fils, Le père reçoit le signal SIGCHLD (alors qu’il était dans le wait du main),
puis le handler est executé, et ps affiche bien le fils zombi. Ensuite c’est le wait du handler qui
prend en compte la terminaison du fils. Au retour du handler, l’appel a wait du main retourne -1,
puisqu’il avait été interrompu par SIGCHLD.
main()
{
int mask ;
struct sigvec s1,s2;
s1.sv_handler = gots1;
s1.sv_mask = sigmask(SIGUSR1);
sigvec(SIGUSR1, &s1, NULL);
s2.sv_handler = gots2;
s2.sv_mask = sigmask(SIGUSR2);
sigvec(SIGUSR2, &s2, NULL);
raise(SIGUSR1);
}
Nous donne les affichages suivant :
sans masquage de SIGUSR2: got s2(31) got s1(30)
avec masquage de SIGUSR2: got s1(30) got s2(31)
Sous BSD, pas de fonction de manipulation propre des groupes de signaux (on regroupe les
signaux par des conjonctions de masques).
Le problème de ”l’interruption” des appels système par les signaux est corrigé par la fonction :
int siginterrupt(int sig, int flag);
le drapeau flag prend comme valeur 0 ou 1, ce qui signifie que les appels systèmes interrompus
par un signal seront :
soit relancés avec les mêmes paramètres.
soit retourneront la valeur -1, et dans ce cas la valeur de errno est positionnée à EINTR.
Certaines fonctions comme readdir utilisent des variables statiques, ces fonctions sont dites
non réentrantes. Il faut éviter d’appeler ce type de fonctions dans un handler de signal, dans le cas
où l’on fait déjà appel à la fonction dans le reste du processus. De la même façon la variable errno
est unique. Si celle-ci est positionnée dans le main mais qu’un signal arrive avant son utilisation,
une primitive appelée dans le handler peut en changer la valeur ! (ce problème de réentrance sera
vu plus en détail avec les processus multi-activités).
pause(void);
cette primitive est le standard UNIX d’attente de la réception d’un signal quelconque, BSD
propose la primitive suivante :
sigpause(int sigmask)
qui permet l’attente d’un groupe spécifique de signaux, attention les signaux du masque sont
débloqués (c.f. sigprocmask).
#include <signal.h>
int sigprocmask(int op, const sigset_t *nouv, sigset_t *anc);
L’opération op :
SIG SETMASK affectation du nouveau masque, recupération de la valeur de l’ancien masque.
SIG BLOCK union des deux ensembles nouv et anc
SIG UNBLOCK soustraction anc - nouv
On peut savoir si un signal est pendant et donc bloqué grâce à la fonction :
retourne -1 en cas d’échec et 0 sinon et l’ensemble des signaux pendants est stocké à l’adresse
ens.
11.5. LA NORME POSIX 97
11.5.2 sigaction
La structure sigaction décrit le comportement utilisé pour le traitement d’un signal :
struct sigaction {
void (*sa_handler) ();
sigset_t sa_mask;
int sa_flags;}
#include <signal.h>
int sigaction(int sig,
const struct sigaction *paction,
struct sigaction *paction_precedente);
Cette fonction réalise soit une demande d’information. Si le pointeur paction est null, on ob-
tient la structure sigaction courante. Sinon c’est une demande de modification du comportement.
Mécanismes de contrôle d’accès concurrents à un fichier, les verrous sont d’une grande utilité
dans les applications de gestion et dans l’élaboration de bases de données partagées.
Les verrous sont rattachés aux inœuds. Ainsi toutes les ouvertures d’un même fichier, et à fortiori
tous les descripteurs sur ces ouvertures, ”voient” le verrou.
La protection réalisée par le verrou a donc lieu sur le fichier physique.
Un verrou est la propriété d’un seul processus, et seul le processus propriétaire du verrou peut le
modifier ou l’enlever, attention le verrou ne protège pas contre les accès du processus propriétaire
(attention à une situation multi-thread).
La portée : Ensemble des positions du fichier auxquelles le verrou s’applique. Cet ensemble
est un intervalle, soit une portion du fichier
[position1, position2]
soit jusqu’à la fin du fichier
[position1, fin de fichier[
dans ce dernier cas si le fichier augmente, le verrou protège les nouvelles positions.
F RDLCK partagé, plusieurs verrous de ce type peuvent avoir des portées non disjointes, par
exemple les verrous [80, 150] et [100, 123]
F WRLCK exclusif, pas de cohabitation possible avec un autre verrou quelque soit son type.
Dans le premier mode advisory (consultatif), la présence d’un verrou n’est testée qu’à la pose
d’un verrou, la pose sera refusée s’il existe un verrou de portée non disjointe et que l’un des deux
verrous est exclusif.
1 En effet le mode impératif n’est pas POSIX, et donc par défaut n’est pas mise en oeuvre sur les disque sous
linux.
99
100 CHAPITRE 12. LES VERROUS DE FICHIERS
Dans le second mode mandatory, la présence de verrous est testée pour la pose mais aussi pour
les appels systèmes read et write.
Dans le mode consultatif, les verrous n’ont d’effet que sur les processus jouant effectivement
le jeu, c’est-à-dire, posant des verrous sur les zones du fichiers sur lesquels ils veulent réaliser une
lecture (verrou partagé) ou une écriture (verrou exclusif).
Dans le mode impératif, les verrous ont un impact sur les lectures/écritures de tous les proces-
sus :
– sur les verrous de type partagé (F RDLCK), toute tentative d’écriture (appel système write)
par un autre processus est bloquée ;
– sur les verrous de type exclusif (F WRLCK), toute tentative de lecture ou d’écriture (read
et write) par un autre processus est bloquée.
Pour rendre l’utilisation impérative il faut sous linux monter le disque avec l’option -o mand.
Puis il faut utiliser la commande chmod pour positionner le SETGID bit, soit chmod g+s fichier
en shell soit la même chose en C : si l’on a le descripteur d d’une ouverture sur le fichier
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
...
struct stat buf
fstat(d, &buf);
fchmod(d,buf.st_mode | S_ISGID);
struct flock {
short l_type; /* F_RDLCK, F_WRLCK,F_UNLCK */
short l_whence; /* SEEK_SET,SEEK_CUR,SEEK_END */
off_t l_start; /* position relative a l_whence */
off_t l_len; /* longueur de l’intervalle */
pid_t l_pid; /* PID du processus propriétaire */
};
le champ l type
F RDLCK verrou partagé
F WRLCK verrou exclusif
F UNLCK déverrouillage
Les manipulations de verrous se font avec la primitive fcntl, c’est-à-dire par le biais d’un
descripteur. Pour poser un verrou partagé, ce descripteur doit pointer sur une ouverture en lecture.
De même, il faut un descripteur sur une ouverture en écriture pour un verrou de type exclusif
.
Pour décrire la portée du verrou que l’on veut poser, on utilise la même syntaxe que pour la
primitive lseek, le début de l’intervalle est whence+l start :
l whence = SEEK SET −→ whence = 0
l whence = SEEK CUR −→ whence = offset courrant
l whence = SEEK END −→ whence = taille du fichier.
12.4. UTILISATION DE FCNTL POUR MANIPULER LES VERROUS 101
La longueur du verrou est définie par le champ l len. Si cette valeur est nulle, le verrou va
jusqu’à la fin du fichier (même si le processus change cette fin). Remarque : il est possible de poser
un verrou dont la portée est supérieure à la taille du fichier.
Le champ l pid contient le pid du processus propriétaire du verrou, ce champ est rempli par
fcntl dans le cas d’un appel consultatif (F GETLK).
13.1 exemples
13.1.1 Les méfaits des accès concurrents
L’exemple le plus simple est une variable entière partagée par deux processus ou threads, ou
bien manipulé par une fonction asyncrone comme un handler de signal. Supposont que l’on définise
deux fonctions de manipulation de la variable :
int getValue();
void setValue(int );
int tmp1=getValue();
| int tmp2= getValue();
setValue(tmp1+1);
| setValue(tmp2+1);
#include <stdio.h>
#include <signal.h>
int nbi; // nbre d’incrementation accès atomique
int partage; // nbr d’incrémentation accès non atomique
103
104 CHAPITRE 13. ALGORITHMES DISTRIBUÉS & INTERBLOCAGES
void handler(int s)
{
int tmp=getValue();
setValue(tmp+1);
nbi++;
}
int
main(int c, char *argv[])
{
struct sigaction sig;
long diff = 0;
sig.sa_handler= handler;
sig.sa_flags = 0;
for(;;)
{
int tmp=getValue();
setValue(tmp+1);
nbi++;
if ((partage != nbi) && (diff!= nbi-partage))
{ diff = nbi-partage; fprintf(stderr,"%d\n", nbi-partage); }
}
}
Solutions :
1. regarder avant de traverser
2. si deux personnes arrivent en même temps sur chaque rive,
si elles avancent en même temps −→ interblocage
si elles attendent en même temps −→ interblocage
3. Un remède : un côté prioritaire −→ famine. En effet si le coté OUEST est prioritaire et qu’un
flot continu de personnes arrive de ce côté, les personnes à l’EST sont bloquées indéfiniment.
4. Une solution : alterner les priorités.
Pour des ressources système comme les fichiers, le partage n’est pas géré par le SGF. Il faut
donc un mécanisme de partage : les verrous, qui permettent un partage dynamique et partiel (por-
tions de fichiers). Pour un partage entre utilisateurs, on utilise plutôt des outils comme SCCS, RCS.
13.2. MODE D’UTILISATION DES RESSOURCES PAR UN PROCESSUS. 105
Exemple :
Exclusion mutuelle les ressources ne sont pas partageables, un seul processus à la fois peut
utiliser la ressource.
Possession & attente il doit exister un processus qui utilise une ressource et qui est en attente
sur une requête.
Sans préemption les ressources ne sont pas préemptibles c’est-à-dire que les libérations sont
faites volontairement par les processus. On ne peut pas forcer un processus à rendre une
ressource. (Contre exemple : le CPU sous Unix est préemptible)
Attente circulaire il doit exister un ensemble de processus Pi tel que Pi attend une ressource
possédée par Pi+1 .
Les quatre conditions sont nécessaires pour qu’une situation d’interblocage ait lieu.
Exercice : montrer que pour les verrous, les quatre conditions tiennent.
Exercice : montrer que si l’une des condition n’est pas vérifiée alors il ne peut y avoir d’interblocage.
Sécurité et Sûreté de
fonctionnement
Comme pour une habitation il faut que votre système offre deux chose importante, d’une part
que la vie dans l’habitation soit sur, pas de risque pour les utilisateurs ni pour les éléments matériel.
Feux , indondation, enfermement, tremblement de terre etc. C’est la sûreté de fonctionnement.
¯
D’autre par l’habitation est protégée ainsi que ses habitants contre des attaques plus ou moins
malveillantes. La porte du jardin est fermée pour éviter que des animaux détériore le jardin. L’ha-
bitation est munie de systéme de vérouillage pour se proteger contre un cambriollage. C’est la
sécurité.
¯
La sûreté de fonctionnement est un élément statégique qui doit être gérer par la direction
informatique en fonction de contraintes opérationnelles. La sécurité est le problème de tout le
monde. Pour que la sécurité fonctionne, il faut que toutes les personnes ayant un accès à une
ressource soient conscient du degré de sécurité associé à la ressource. La stratégie de sécurité doit
bien sur être définie par la direction informatique mais c’est un travail collectif (installation d’une
sérure n’a pas d’effet si tout le monde laisse la porte ouverte).
Il peut s’agir :
– d’erreurs de programmation (d’un utilisateur, ou du système lui-même) qui se propagent au
système (du fait de contrôles insuffisants ou mal effectués).
– d’un mauvais fonctionnement du matériel.
– enfin, d’un opérateur, concepteur ou réalisateur malveillant ou peu scrupuleux (quand il
s’agit d’informations financières !).
Le recensement des opérations frauduleuses aux Etats-Unis au cours d’une année a donné 339 cas
de fraude, pour un coût d’un milliard de francs.
La protection des sites a également un coût très important (temps et complexité), d’où des
systèmes de protection qui résultaient d’un compromis coût/efficacité.
Le coût en ressources de la protection étant resté stationnaire, les systèmes et les machines
actuelles plus rapides ont rendu ce coût moins prohibitif.
L’idée d’un système de protection est de traiter les différents types de problèmes de manière
générale et unitaire.
107
108 CHAPITRE 14. SÉCURITÉ ET SÛRETÉ DE FONCTIONNEMENT
Heureusement, si ces dispositifs permettent d’augmenter les performances du logiciel, dans des
domaines comme celui de la fiabilité ou de la résistance aux erreurs, leur coût relatif diminue. Si,
de plus, ces dispositifs permettent une gestion des ressources partagées plus facile et plus sûre, ils
peuvent devenir compétitifs d’un point de vue commercial.
Il est difficile de définir précisément ce que l’on entend par protection d’un système d’exploita-
tion (et d’information en général), tant les facteurs qui peuvent influer sur cette notion (humains,
sociaux, économiques), sont nombreux. On peut dire cependant que la protection se rapporte à
tout ce par quoi l’information peut être modifiée, divulguée ou détruite. Dans certains cas, la
gestion du trafic aérien par exemple, elle peut être la garantie des performances du système. La
confidentialité d’enregistrements financiers, médicaux ou personnels relève aussi de la protection,
comme le fait qu’un processus utilisateur ne puisse être exécuté en mode système. La protection
exige enfin la correction des processus système.
– pérennité du système
– confidentialité des données (système, utilisateur, etc.)
– correction du système
A l’opposé, nous ne parlerons pas de :
– protection physique de l’ordinateur (feu, vol, coupures, etc.)
– malveillance ou incompétence de l’opérateur (il est éventuellement possible de limiter soi-
gneusement les privilèges du super-utilisateur afin de préserver le système).
Le degré de protection du système dépend de deux facteurs :
– le degré de protection des informations qu’il manipule
– le degré de confiance en ses logiciels, en particulier le système d’exploitation.
Un logiciel est fiable quand il satisfait correctement ses spécifications et quand, de plus, il est ca-
pable de résister à un environnement imprévu (données erronées, pannes, etc.), soit en corrigeant
l’anomalie, soit en la signalant, mais en évitant que les erreurs ne se propagent et ne contaminent
le système tout entier.
La protection, l’intégrité et l’authenticité des données qui transitent dans un système d’informa-
tion sont réalisées par les systèmes cryptographiques (ATHENA et Kerberos au MIT).
Le confinement des erreurs est obtenu en contrôlant les accès aux entités du système d’exploita-
tion, par les domaines de protection.
Objets
Fichier Segment Segment Processus Editeur
1 1 2 2
Sujets
Lire Executer Lire Entrer
Processus 1 Ecrire
Lire Entrer
Processus 2
Ecrire
Lire
Processus 3 Entrer
Ecrire
Entrer
Executer
Fig. 14.2 – Matrice d’accès
Deux niveaux :
– un niveau logique (soft), celui du modèle de protection, ensemble de règles qui définissent
quels accès (aux ressources) sont autorisés et quels accès sont interdits. Ces règles sont
définies soit à la conception du système, soit par les utilisateurs.
– un niveau matériel qui permet d’appliquer le modèle réellement. C’est le rôle des mécanismes
de protection.
Le premier doit être dynamique. Par contre, le deuxième doit être stable pour faciliter l’implémentation,
le contrôle et la fiabillisation.
Les deux doivent de surcroı̂t être indépendants du modèle pour offrir un vaste ensemble de règles
possibles.
Un exemple de protection simple est celui des répertoires sous unix, pour éviter qu’ils soit corom-
pus (ou que l’arborescence soit corompue), il sont identifiés comme des fichiers spéciaux et il n’est
possible d’y accèder que par le truchement d’appels systèmes spécifiques. Biensur il est toujours
possible de les manipuler si l’on peut accèder en mode raw au disque dur mais cette option est
réservée au super utilisateur.
Le modèle doit fixer à tout instant les droits d’accès dont dispose chaque processus. Cet en-
semble de droits est le domaine de protection du processus. Voire un exemple de matrice d’accès
dans la figure 14.2
qui compose un processus ne mette pas en danger des ressources non utilisées. Par exemple : un
module de lecture de données, un module de calcul, un module d’impression. On va donc exécuter
chaque module dans un domaine de protection le plus réduit possible.
C’est le principe du moindre privilège : un programme ne peut endommager un objet auquel
il n’a pas accès !
Pour mettre en place ces domaines dynamiques, une possibilité est de changer les droits d’accès
du processus au cours de son exécution. Une autre possibilité est d’ajouter aux objets le type
”domaine” et de contrôler les accès à la matrice. L’édition de cases de la matrice devient une
opération protégée.
14.4 Le confinement
Le problème ici est tout simplement le fait que le programme ne manipule pas de données de
l’utilisateur mais simplement enregistre ses paramètres d’appels (les utilisateurs à qui vous envoyez
du courrier par exemple). Le problème du confinement est donc de vous protéger contre ce type
d’extraction d’informations (ce qui peut par exemple être utilisé en bourse pour connaitre votre
comportement d’achat).
Lire Entrer
Domaine 2
Ecrire
Lire
Domaine 3 Entrer Entrer Entrer
Processus i Domaine i
Objet
111
112 CHAPITRE 14. SÉCURITÉ ET SÛRETÉ DE FONCTIONNEMENT
Editeur Executer
Proc Lire,Executer Code Procedure
Fic1 Lire
Fichier
Fic2 Lire,Ecrire
Fichier
Proc Entrer
Objet
C-liste1 Entrer
.
.
.
O Lire,Ecrire Objet O
Objet
O’ Lire,revoquer Copie
O’ Lire O’
Copies O Lire,Ecrire
O’ Lire
O’ Lire
O Lire,Ecrire Objet O
Objet
O’ Lire,revoquer Copie
O’ Lire O’
Copies
O’ Lire
Revocation
O’ Lire
qui donne sur le fichier proj les droits de lecture et d’écriture à l’utilisateur dr du groupe
staff et à l’utilisateur zipstein quelque soit son groupe et qui refuse cet accès aux utilisateurs
du groupe licence.
qui donne le droit d’accès total à l’utilisateur binome (quelque soit son groupe), permet le
parcours du répertoire aux membres du groupe propriétaire et refuse l’accès à tous les autres uti-
lisateurs.
int setacl(
const char *path,
size t nentries,
const struct acl entry *acl
);
int fsetacl(
int fildes,
size t nentries,
const struct acl entry *acl
116 CHAPITRE 14. SÉCURITÉ ET SÛRETÉ DE FONCTIONNEMENT
);
Un bon exercice : récrire lsacl de façon qu’il fonctionne d’une manière similaire à /bin/ls.
Utilisation de la commande script pour montrer le comportement des acl.
119
120 CHAPITRE 15. MULTIPLEXER DES ENTRÉES-SORTIES
– Les descripteurs que nous voulons scruter. (l’indice du plus grand descripteur qui nous
intéresse dans la table des descripteurs du processus)
– Les conditions de réveil sur chaque descripteur (en attente de lecture, écriture, évènement ?)
– Combien de temps nous voulons attendre.
La fonction retourne pour chaque descripteur s’il est prêt en lecture, écriture, ou si l’évènement
a eu lieu, et aussi le nombre de descripteur prêts. Cette information nous permet ensuite d’appeler
read ou write sur le(s) bon(s) descripteur(s).
#include <sys/types.h>
#include <sys/time.h>
#include <unistd.h>
Paramétrage du délai :
struct timeval {
long tv_sec;
long tv_usec;
};
Les trois pointeurs (readfds, writefds, et exceptfds) sur des ensembles de descripteurs sont
utilisés pour indiquer en entrée les situations qui nous intéressent. C’est à priori (cela peut varier
avec l’implémentation) des tableaux de bits avec un bit pour chaque descripteur du tableau de
descripteurs du processus. L’entier maxfd est la position du dernier bit significatif de ce tableau
de bits.
Les seules façons de manipuler ces ensembles de descripteurs sont :
– Allocation :fd\_set *fd=(fd\_set*)malloc(sizeof(fd\_set));
– Création
– Affectation
– Utilisation d’une des quatre macros suivantes :
Un descripteur est considéré comme prêt en lecture si un appel read dessus ne sera pas blo-
quant. De même, un descripteur est considéré comme prêt en écriture si un appel write ne sera pas
bloquant. Les exceptions / évènements sont définis pour les lignes de communication qui acceptent
les messages hors bande comme les sockets en mode datagramme.
15.2. LES OUTILS DE SÉLECTION 121
struct pollfd {
int fd;
short events;
short revents;
};
Ici on spécifie la liste de descripteurs (dans un tableau) et ce que l’on veut gèter sur chacun
d’eux.
La valeur de retour est -1 en cas d’erreur, 0 si le temps d’attente timeout est écoulé, ou un
entier positif indiquant le nombre de descripteurs pour lesquels la valeur du champ revents a été
modifiée.
Les évènements sont ici :
Pour les évènements de events :
POLLIN Données non prioritaire peuvent être lues.
POLLPRI Données prioritaire peuvent être lues.
POLLOUT Données non prioritaire peuvent être écrites, les messages de haute priorité peuvent
toujours êtres écrits.
Pour les revents (valeurs de retour de la primitive poll) :
POLLIN,POLLPRI les données sont là.
POLLOUT l’écriture est possible
POLLERR Une erreur a eu lieu.
POLLHUP La ligne a été coupée.
POLLNVAL Descripteur invalide.
Le mode de blocage de la primitive poll dépend du paramètre timeout
timeout == INFTIM Bloquant, INFTIM est défini dans stropts.h.
timeout == 0 Non bloquant.
timeout > 0 Semi bloquant, attente de timeout micro secondes.
Un Exemple Attente de données sur ifd1 et ifd2, de place pour écrire sur ofd, avec un
délai maximum de 10 seconds :
#include <poll.h>
struct pollfd fds[3] ;
int ifd1, ifd2, ofd, count ;
fds[0].fd = ifd1 ;
fds[0].events = POLLIN ;
fds[1].fd = ifd2 ;
fds[1].events = POLLIN ;
fds[2].fd = ofd ;
122 CHAPITRE 15. MULTIPLEXER DES ENTRÉES-SORTIES
fds[2].events = POLLOUT ;
count = poll(fds, 3, 10000) ;
if (count == -1) {
perror("poll failed") ;
exit(1) ;
}
if (count==0)
printf("Rien \n") ;
if (fds[0].revents & POLLIN)
printf("Données a lire sur ifd%d\n", fds[0].fd) ;
if (fds[1].revents & POLLIN)
printf("Données a lire sur ifd%d\n", fds[1].fd) ;
if (fds[2].revents & POLLOUT)
printf("De la place sur fd%d\n", fds[2].fd) ;
1. Il faut verifier la présence d’epoll sur votre système, ouvrer le périphérique /dev/epoll en
mode O RDWR, sinon retour a select et poll kdpfd = open("/dev/epoll",O_RDWR);
2. Définiser le nombre maximal maxfd de descipteurs scrutables #include <linux/eventpoll.h> \ldots ioctl(epol
3. Allouer un segment de mémoire partagé avec char *map = (char *)mmap(NULL, EP MAP SIZE(maxfds,
PROT READ | PROT WRITE, MAP PRIVATE, epoll fd, 0))
4. Maintenant vous pouvez ajouter des descipteurs
struct pollfd pfd;
pfd.fd = fd;
pfd.events = POLLIN | POLLOUT | POLLERR | POLLHUP;
pfd.revents = 0;
if (write(kdpfd, &pfd, sizeof(pfd)) != sizeof(pfd)) {
/* gestion d’erreur */
}
5. Récupere les évenements
struct pollfd *pfds;
struct evpoll evp;
for (;;) {
evp.ep_timeout = STD_SCHED_TIMEOUT;
evp.ep_resoff = 0;
struct iovec {
void *iov_base ;
int iov_len;
};
La programmation par thread (actvité) est naturelle pour gérer des phénomènes asyncrones.
Les entrées utilisateur dans les interfaces graphiques (souris, clavier) sont plus facile a gérer si l’on
peut séparer l’activité principale du logiciel de la gestion des commandes utilisateur. Les entrées
sorties multiples voir le chapitre 15 correspondant, sont gérées plus simplement en utilisant des
threads.
Les actvitiés sont une nouvelle façon de voire les processus dans un système. L’idée est de
séparer en deux le concept de processus. La première partie est l’environement d’exécution, on y
retrouve une très grande partie des éléments constitutifs d’un processus en particulier les infor-
mations sur le propriétaire, la position dans l’arborescence le masque de création de fichier etc.
La deuxième partie est l’activité, c’est la partie dynamique, elle contient une pile, un context
processeurs (pointeur d’instruction etc), et des données d’ordonancement.
16.0.1 Description
Un processus est composé des parties suivantes : du code, des données, une pile, des descripteurs
de fichiers, des tables de signaux. Du point de vue du noyau, transférer l’exécution à un autre
processus revient à rediriger les bons pointeurs et recharger les registres du processeur de la pile.
Les divers threads d’un même processus peuvent partager certaines parties : le code, les données,
les descripteurs de fichiers, les tables de signaux. En fait, ils ont au minimum leur propre pile, et
partagent le reste.
125
126 CHAPITRE 16. LES THREADS POSIX
Données de la Tache
répertoire de travail
umask
Descripteurs
Accès aux tables de pages
* programme
* données globales
* pile globale
Propriétaires
Scheduling
Données de Registres
laThread 1 Pile noyau
Scheduling
Données de Registres
laThread 2 Pile noyau
Scheduling
Données de Registres
laThread 3 Pile noyau
•••
Fig. 16.1 – Organisation mémoire, partage des fonctions entre le processus et les activités
127
conservées après le fork() et dont le contenu ne varie pas. Ainsi si une activité a pris le sémaphore
avant le fork(), si l’activité principale cherche à prendre ce sémaphore après le fork() elle sera
indéfiniment bloquée.
Après un exec, le processus ne contient plus que la thread qui a exécuté l’une des six commandes
exec. Pas de problème avec les sémaphores comme l’espace d’adressage a changé.
16.0.3 clone
Sous linux (et rarement sous les systèmes Unix) il existe un appel système un peut spécial. Cet
appel système réalise un dédoublement du processus comme fork d’ou son nom de clone. Cet
appel système permet de préciser exactement ce que l’on entend partager entre le processus père
et le processus fils.
Eléments partageables :
ppid Création d’un frère au lieux d’un fils.
FS Partage de la structure d’information liée au système de fichier (“.”, “/”, umask),
FILES Partage de la table des descripteurs,
SIGHAND Partage de la table des gestionnaires de Signaux, mais pas des masques de signaux,
PTRACE Partage du “crochet” (hook) de debug voire l’appel ptrace.
VFORK Partage du processeur ! le processus père est bloqué tantque le fils n’a pas exécuté soit
exit soit execve, c’est à dire qu’il s’est détaché de tout les élément partageable du processus
père (sauf les FILEs),
VM Partage de la mémoire virtuelle, en particulier les allocations et désallocations par mmap et
munmap sont visibles par les deux proccessus.
pid Les deux processus ont le même numéro.
THREAD Partage du groupe de thread, les deux processus sont ou ne sont pas dans le même
groupe de threads.
où
objet désigne si il est présent le type de l’objet auquel la fonction s’applique. Les valeurs possibles
de objet peuvent être
cond pour une variable de condition
mutex pour un sémaphore d’exclusion mutuelle
opération désigne l’opération a réaliser, par exemple create, exit ou init
le suffixe np indique, si il est présent, qu’il s’agit d’une fontion non portable, c’est-à-dire Hors
Norme.
avec objet prenant comme valeur cond, mutex ou rien pour une thread.
128 CHAPITRE 16. LES THREADS POSIX
Terminaison
a) les appels UNIX exit et donc exit terminent toutes les threads du processus.
b) Terminaison d’une thread
int pthread_exit (int *p_status);
p status code retour de la thread, comme dans les processus UNIX la thread est zombifiée
pour attendre la lecture du code de retour par une autre thread. A l’inverse des processus, comme
il peut y avoir plusieurs threads qui attendent, la thread zombie n’est pas libérée par la lecture du
p status, il faut pour cela utiliser une commande spéciale qui permettra de libérer effectivement
l’espace mémoire utilisé par la thread.
Cette destruction est explicitement demandée par la commande
int pthread_detach (pthread_t *p_tid);
Si un tel appel a lieu alors que l’activité est en cours d’exécution, cela indique seulement qu’à
l’exécution de pthread_exit les ressources seront restituées.
16.1 Synchronisation
Trois mécanismes de synchronisation inter-activités :
– la primitive join
– les sémaphores d’exclusion mutuelle
– les conditions (évènements)
16.1. SYNCHRONISATION 129
access
open
Lecture
incorrecte
permet à une activité de réaliser une opération P sur le sémaphore. Si le sémaphore est déjà
utilisé, l’activité est bloquée jusqu’à la réalisation de l’opération V (par une autre activité) qui
libèrera le sémaphore.
Opération P non bloquante :
Opération V :
Un appel à la fonction
pthread_mutex_unlock(pthread_mutex_t *pmutex);
Trois étapes
1. libération sur sémaphore *p mutex
2. activité mise en sommeil sur l’évènement
3. réception de l’évènement, récupération du sémaphore
La condition est indépendante de l’événement et n’est pas nécessairement valide à la réception
(cf. exemple).
Exemple, le programme suivant :
pthread_mutex_t m;
pthread_cond_t cond;
int condition = 0;
pthread_mutex_unlock(m);
pthread_mutex_lock(print);
printf(" Condition realisee\n");
pthread_mutex_unlock(print);
}
main()
{
pthread_t lathread;
Un autre exemple d’utilisation de condition avec deux threads qui utilisent deux tampons pour
réaliser la commande cp, avec une activité responsable de la lecture et l’autre de l’écriture. Les
conditions permettent de synchroniser les deux threads. Ici nous utilisons la syntaxe NeXT/MACH.
#include <sdtio.h>
#include <fcntl.h>
#import <mach/cthreads.h>
char buff1[BUFSIZ];
int nb_lu1;
int etat1 = BUFFER_A_LIRE;
int ds, dd; /* descripteurs source et destination */
}
}
ecrire()
{
for(;;)
{ /* ecriture du buffer 1 */
mutex_lock(lock1);
while (etat1 == BUFFER_A_LIRE)
condition_wait(cond1, lock1);
if (nb_lu1 == 0)
{
mutex_unlock(lock1);
exit(0);
}
write(dd, buff1, nb_lu1);
mutex_unlock(lock1);
etat1 = BUFFER_A_LIRE;
condition_signal(cond1);
}
}
main()
{
ds = open(argv[1], O_RDONLY);
dd = open(argv[2], O_WRONLY|O_TRUNC|O_CREAT, 0666);
lock1 = mutex_alloc();
cond1 = condition_alloc();
cthread_fork((cthread_fn_t)lire, (any_t)0);
ecrire(); /* la thread principale realise les ecritures */
}
#include <pthread.h>
pthread_attr_setsched(pthread_attr_t *attr, int politique);
SCHED RR Round Robin. La thread la plus prioritaire s’exécute jusqu’à ce qu’elle bloque. Les
threads de même priorité maximum sont organisées avec le principe du tourniquet, c’est-à-
dire qu’il existe un quantum de temps au bout duquel le cpu est préempté pour une autre
thread (voire Chapitre 6 sur les Processus).
SCHED OTHER Comportement par défaut. Tous les threads sont dans le même touniquet, il
n’y a pas de niveau de priorité, ceci permet l’absence de famine. Mais les threads avec une
politique SCHED FIFO ou SCHED RR peuvent placer les threads SCHED OTHER en situation de
famine.
SCHED FG NP (option DCE non portable) Même politique que SCHED OTHER mais l’ordon-
nanceur peut faire évoluer les priorités des threads pour assurer l’équité.
SCHED BG NP (option DCE non portable) Même politique que SCHED FG NP, mais les threads
avec une politique SCHED FIFO ou SCHED RR peuvent placer les threads SCHED BG NP en si-
tuation de famine.
pthread_attr_setprio(pthread_attr_t *attr, int prio);
La priorité varie dans un intervalle défini par la politique :
PRI OTHER MIN <= prio <= PRI OTHER MAX
PRI FIFO MIN <= prio <= PRI FIFO MAX
PRI RR MIN <= prio <= PRI RR MAX
PRI FG MIN NP <= prio <= PRI FG MAX NP
PRI BG MIN NP <= prio <= PRI BG MAX NP
Ces deux fonctions retournent 0 en cas de succès et -1 sinon. La valeur de errno indiquant si
l’erreur est une question de paramètres ou de permission.
Les deux fonctions que l’on peut appeler sur une pthread pour changer sa priorité ou sa
politique sont :
pthread_setprio(pthread_t *unepthread, int prio);
pthread_setsched(pthread_t *unepthread, int politique, int prio);
Il est possible de connaı̂tre la priorité ou la politique d’une pthread ou d’un objet pthread attr
avec :
pthread_attr_getprio(pthread_attr_t *attr,int prio);
pthread_attr_getsched(pthread_attr_t *attr,int politique);
pthread_getprio(pthread_t *unepthread, int prio);
pthread_getsched(pthread_t *unepthread, int politique);
#include <pthread.h>
int pthread_getspecific (pthread_key_t *p_clé, void **pvaleur);
permet la lecture de la valeur qui est copié à l’adresse pvaleur retourne 0 ou -1 selon que
l’appel à réussi ou non. La fonction
#include <pthread.h>
int pthread_setspecific (pthread_key_t *p_clé, void *valeur);
permet l’écriture à l’emplacement spécifié de valeur retourne 0 ou -1 selon que l’appel a réussit
ou non.
Clustering
Le clustering sont des techniques liés a l’utilisation de grappes d’ordinateurs utilisé comme
un super ordinateur avec plusieurs processeurs. L’objectif est le RAIP : Redundant Array of
Inexpensive PROCESSOR.
la station de trvail individuelle vielli a grande vitesse, une facon de recycler ceux qui sont en
peu trop vieux (mais pas encore trops vieux) est de les rassembler dans votre premier cluster. De
ces PC nous allons tirer un super calculateur, bien sur il est toujours plus rapide d’achetter un
G5 si on en a les moyens, pour les mainframes (je l’affirme vous n’en avez pas les moyens ou alors
vous en avez deja plusieurs...)1 .
Il existe différentes techniques de clustering pour différent objectifs :
Tolérence au pannes Google, le cluster est organisé pour assurer la redondance des unité de
calcul pour assurer la continuité de service.
Super calculateur Earth Simulator,Plusieurs processeurs travaillant en même temps permet
d’optenir un super calculateur à peu de frais, comme les processeurs qui compose chaque
noeud sont bon marché. IL faut que le problème s’y prète c’est le cas des calculs météorologiques
et des calculs de simulation à grande échelle.
Monté en charge Google, Le problème pour une application n’est pas toujours un problème
de puissance de calcul parfois ce qui posse problème c’est la quantité d’entrées sorties
qui faut assurer. Vous pouvez d’ailleurs facilement tester cette propriété en réalisant un
petit programme qui sans saturer l’unité central sature complètement les entrées sorties
while :;do; { cp grosfichier /tmp/$PID } &; done
Ainsi le clustering a essentiellement pour objectif d’utiliser le dicton ”l’union fait la force” pour
résoudre une difficulté de calcul.
1 C’est evident si vous avez les moyens d’acheter un mainframe vous avez les moyens d’achetter un super-cluster
haut de gamme.
135
136 CHAPITRE 17. CLUSTERING
Chapitre 18
Bibliographie
M. Bach. The design of the UNIX operating system. 1986. Prentice-Hall, Englewood Cliffs,
N.J. ISBN 0-13-201757-1
W.R. Stevens, UNIX Network Programming. 1990 Prentice-Hall, Englewood Cliffs, N.J.
18.1 Webographie
Vous trouverez a l’url suivant une webographie : www-igm.univ-mlv.fr/ dr/Cours.html
137
Index
138
INDEX 139
static, 104
stdio.h, 31
stdlib
atexit, 38
clearerr, 39
exit, 38
fclose, 36
feof, 33, 39
fflush, 36
fopen, 32
fread, 33
freopen, 32, 35
fseek, 34
ftell, 35
fwrite, 33
mkdir, 39
perror, 39
printf, 31
remove, 37
rename, 37
rewind, 35
rmdir, 39
scanf, 31
setbuf, 37
setbuffer, 37
setlignebuf, 37
setvbuf, 36
stderr, 31
stdin, 31
stdout, 31
system, 38
tmpfile, 33
tmpnam, 33
Xalloc, 51
super bloc, 10
susp, 104, 113
swap, 79, 85
synchronisation, 101
Système de Gestion de Fichiers, 7
tas, 50
terminal de contrôle, 104
termios, 106
tubes, 97
tubes nommés, 100
Worst-fit, 81