Chapitre 12 Principes de base des systèmes Unix
12.1 Survol du système
Le système UNIX fut développé à Bell laboratories (research)
de 1970 à 1980 par Ken Thompson et Dennis Ritchie. Il s'est
rapidement répandu dans le milieu de la recherche, et plusieurs
variantes du système original ont vu le jour (versions SYSTEM V
d'AT&T, BSD à Berkeley, ...). Il triomphe aujourd'hui auprès
du grand public avec les différentes versions de LINUX1, dont le
créateur original est Linus B. Torvalds et qui ont transformé les
PC de machines bureautiques certes sophistiquées en véritables
stations de travail.
Les raisons du succès d'UNIX sont multiples. La principale est sans
doute sa clarté et sa simplicité. Il a été également le
premier système non propriétaire qui a bien isolé la partie
logicielle de la partie matérielle de tout système
d'exploitation. Écrit dans un langage de haut niveau (le
langage C, l'un des pères de Java), il est très facile à
porter sur les nouvelles architectures de machines. Cela représente
un intérêt non négligeable : les premiers systèmes
d'exploitation étaient écrits en langage machine, ce qui ne
plaidait pas pour la portabilité des dits-systèmes, et donc avait
un coût de portage colossal.
Quelle pourrait être la devise d'UNIX ? Sans doute Programmez,
nous faisons le reste. Un ordinateur est une machine complexe, qui
doit gérer des dispositifs physiques (la mémoire, les
périphériques commes les disques, imprimantes, modem, etc.) et
s'interfacer au réseau (Internet). L'utilisateur n'a pas besoin de
savoir comment est stocké un fichier sur le disque, il veut juste
considérer que c'est un espace mémoire dans lequel il peut ranger
ses données à sa convenance. Que le système se débrouille pour
assurer la cohérence du fichier, quand bien même il serait
physiquement éparpillé en plusieurs morceaux.
Dans le même ordre d'idées, l'utilisateur veut faire tourner des
programmes, mais il n'a cure de savoir comment le système gère la
mémoire de l'ordinateur et comment le programme tourne. Ceci est
d'autant plus vrai qu'UNIX est un système multi-utilisateur et
multitâches. On ne veut pas savoir comment les partages de
ressources sont effectués entre différents utilisateurs. Chacun
doit pouvoir vivre sa vie sans réveiller l'autre.
Toutes ces contraintes sont gérées par le système, ce qui ne veut pas
dire que cela soit facile à mettre en oeuvre. Nous
verrons plus loin comment le système décide ce que fait
le processeur à un instant donné.
Le système UNIX est constitué de deux couches bien
séparées : le noyau permet au système d'intéragir avec
la partie matérielle de la machine, la couche supérieure contient
les programmes des utilisateurs. Tout programme qui tourne
invoque des primitives de communication avec le noyau, appelées appels systèmes. Les plus courants sont ceux qui font les
entrées-sorties et les opérations sur les fichiers.
La majeure partie des langages de haut niveau
(Java, C) possèdent des interfaces qui appellent directement le
noyau, mais cela est transparent pour le programmeur moyen.

Le noyau s'occupe du contrôle de l'exécution inviduelle aussi bien
que globale des programmes. Un programme qui s'exécute est appelé
processus. Le noyau permet la création, la suspension, la
fin d'un processus. Il gère de manière équitable l'exécution
de tous les processus présents. Il leur alloue et gère la place
mémoire nécessaire à leur exécution.
12.2 Le système de fichiers
Le système de fichiers d'UNIX est arborescent, l'ancêtre de tous
les chemins possibles étant la racine (notée /). On dispose de
répertoires, qui contiennent eux-mêmes des répertoires et des
fichiers. C'est là la seule vision que veut avoir un utilisateur de
l'organisation des fichiers. Aucun fichier UNIX n'est typé. Pour
le système, tous les fichiers (y compris les répertoires)
contiennent des suites d'octets, charge au système lui-même et aux
programmes à interpréter correctement leur contenu.
De même, un fichier n'a pas de taille limitée a
priori. Ces deux caractéristiques ne sont pas partagées par grand
nombre de systèmes... Un des autres traits d'UNIX est de traiter
facilement les périphériques comme des fichiers. Par exemple :
unix% cat musique > /dev/audio
permet d'écouter le fichier musique sur son ordinateur (de
façon primitive...). C'est le système qui se débrouille pour
associer le nom spécial /dev/audio/ aux hauts-parleurs de
l'ordinateur (s'ils existent).
En interne, un fichier est décrit par un i-noeud (inode
pour index node en anglais), qui contient toutes les informations
nécessaires à sa localisation sur le disque. Un fichier est vu
comme une suite d'octets, rassemblés en blocs. Ces blocs n'ont pas
vocation à être contiguës : les fichiers peuvent croître de
façon dynamique et une bonne gestion de l'espace disque peut
amener à aller chercher de la place partout sur le disque. Il faut
donc gérer une structure de données permettant de récupérer
ses petits dans le bon ordre. La structure la plus adaptée est celle
d'une table contenant des numéros de blocs. Celle-ci est
découpée en trois : la zone des blocs directs contient des
numéros de blocs contenant vraiment des données ; la zone simple indirection fait référence à une adresse où on trouve
une table de numéros de blocs directs ; la zone double
indirection contient des références à des tables de
références qui font référence à des blocs (cf. figure
12.1), et enfin à la zone triple indirection. La
structure de données correspondante est appelée B-arbre et permet
de gérer correctement les disques actuels.

Figure 12.1 : Structure bloc d'un fichier.
Le noyau ne manipule jamais les fichiers directement sur disque. Il
travaille avec des tampons intermédiaires qui permettent de limiter
l'accès à la mémoire disque. Cela améliore les performances
des entrées/sorties et protège les fichiers.
12.3 Les processus
12.3.1 Comment traquer les processus
Un processus est un programme qui s'exécute. Comme UNIX est
un système multi-tâches, multi-utilisateurs, il y a toujours un
grand nombre de tels processus qui vivent à un instant donné.
Les différents processus sont stockés dans une table et
repérés par leur numéro d'ordre. À tout instant, on peut
consulter la liste des processus appartenant à un utilisateur par la
commande ps (pour process status) :
unix% ps
PID TTY TIME CMD
646 pts/1 00:00:00 bash
740 pts/1 00:00:00 ps
La première colonne donne le numéro du processus dans la table, la
deuxième le terminal (on dit tty) associé au processus, le temps CPU
utilisé est donné dans la troisième, et finalement la
quatrième donne la commande qui a lancé le processus.
On peut également obtenir la liste de tous les processus en
rajoutant des options à ps avec pour résultat le contenu de
la table 12.1.

Table 12.1 : Résultat de la commande
ps ax.
On voit apparaître une colonne STAT qui indique l'état de chaque processus. Dans cet exemple, la commande
ps ax est la seule à être active (d'où le R dans la
troisième colonne). Même quand on ne fait rien (ici, le seul
processus d'un utilisateur est l'édition d'un fichier par la commande
emacs), il existe plein de processus vivants. Notons parmi
ceux-là tous les programmes se terminant par un d comme
/usr/sbin/ssfd ou lpd, qui sont des programmes
systèmes spéciaux (appelés démons) attendant qu'on les
réveille pour exécuter une tâche comme se connecter ou imprimer.
On constate que la numérotation des processus n'est pas
continue. Au lancement du système, le processus 0 est
lancé, il crée le processus 1 qui prend la
main. Celui-ci crée d'autres processus fils qui héritent de
certaines propriétés de leur père. Certains processus sont
créés, exécutent leur tâche et meurent, le numéro qui leur
était attribué disparait. On peut voir l'arbre généalogique
des processus qui tournent à l'aide de la commande pstree
(voir figure 12.2).

Table 12.2 : Résultat de la commande
pstree.
Les ? dans la deuxième colonne indiquent l'existence de
processus qui ne sont affectés à aucun TTY.
12.3.2 Fabrication et gestion
Un processus consiste en une suite d'octets que le processeur
interprète comme des instructions machine, ainsi que des données
et une pile. Un processus exécute les instructions qui lui sont
propres dans un ordre bien défini, et il ne peut exécuter les
instructions appartenant à d'autres processus. Chaque processus
s'exécute dans une zone de la mémoire qui lui est réservée et
protégée des autres processus. Il peut lire ou écrire les
données dans sa zone mémoire propre, sans pouvoir interférer
avec celles des autres processus. Notons que cette propriété est
caractéristique d'UNIX, et qu'elle est trop souvent absente de
certains systèmes grand public.
Les compilateurs génèrent les instructions machine
d'un programme en supposant que la mémoire utilisable commence à
l'adresse 0. Charge au système à transformer les
adresses virtuelles du programme en adresses
physiques. L'avantage d'une telle approche est que le code
généré ne dépend jamais de l'endroit dans la mémoire où il
va vraiment être exécuté. Cela permet aussi d'exécuter le
même programme simultanément : un processus nouveau est créé
à chaque fois et une nouvelle zone de mémoire physique lui est
alloué. Que diraient les utilisateurs si l'éditeur emacs ne
pouvait être utilisé que par un seul d'entre eux à la fois !
Le contexte d'un processus est la donnée de son code, les
valeurs des variables, les structures de données, etc. qui lui sont
associées, ainsi que son état. À un instant donné, un
processus peut être dans quatre états possibles :
-
en cours d'exécution en mode utilisateur ;
- en cours d'exécution en mode noyau ;
- pas en exécution, mais prêt à l'être ;
- endormi, quand il ne peut plus continuer à s'exécuter
(parce qu'il est en attente d'une entrée sortie par exemple).
Un processeur ne peut traiter qu'un seul processus à la fois. Le
processus peut être en mode utilisateur : dans ce cas, il peut
accéder aux données utilisateur, mais pas aux données du
noyau. En mode noyau, le processus peut accéder également aux
données du noyau. Un processus passe continuellement d'un état à
l'autre au cours de sa vie, selon des règles précises,
simplifiées ici dans le diagramme de transition d'états décrits à
la figure 12.2.

Figure 12.2 : Diagramme de transition d'états.
Il ne peut y avoir changement de contexte qu'en passant de
l'état 2 (mode noyau) à l'état 4 (endormi). Dans ce cas, le
système sauvegarde le contexte du processus avant d'en charger un
autre. Le système pourra reprendre l'exécution du premier
processus plus tard.
Un processus peut changer d'état de son propre chef (parce qu'il
s'endort) ou parce que le noyau vient de recevoir une interruption qui doit être traitée, par exemple parce qu'un
périphérique a demandé à ce qu'on s'occupe de lui. Notons
toutefois que le noyau ne laisse pas tomber un processus quand il
exécute une partie sensible du code qui pourrait corrompre des
données du processus.
12.3.3 L'ordonnancement des tâches
Le processeur ne peut traiter qu'un processus à chaque fois. Il
donne l'impression d'être multi-tâches en accordant à chacun
des processus un peu de temps de traitement à tour de rôle.
Le système doit donc gérer l'accès équitable de chaque processus
au processeur. Le principe général est d'allouer à chaque
processus un quantum de temps pendant lequel il peut s'exécuter dans
le processeur. À la fin de ce quantum, le système le préempte et
réévalue la priorité de tous les processus au moyen d'une
file de priorité. Le processus avec la plus haute priorité dans
l'état ``prêt à s'exécuter, chargé en mémoire'' est alors
introduit dans le processeur. La priorité d'un processus est
d'autant plus basse qu'il a récemment utilisé le processeur.
Le temps est mesuré par une horloge matérielle, qui interrompt
périodiquement le processeur (générant une interruption). À
chaque top d'horloge, le noyau peut ainsi réordonner les priorités
des différents processus, ce qui permet de ne pas laisser le
monopole du processeur à un seul processus.
La politique exacte d'ordonnancement des tâches dépend du
système et est trop complexe pour être décrite ici. Nous
renvoyons aux références pour cela.
Notons pour finir que l'utilisateur peut changer la priorité d'un
processus à l'aide de la commande nice. Si le programme
toto n'est pas extremement prioritaire, on peut le lancer sur la
machine par :
unix% nice ./toto &
La plus basse priorité est 19 :
unix% nice -19 ./toto &
et la plus haute (utilisable seulement par le super-utilisateur) est
-20. Dans le premier cas, le programme ne tourne que si
personne d'autre ne fait tourner de programme ; dans le second, le
programme devient super-prioritaire, même devant les appels
systèmes les plus courants (souris, etc.).
12.3.4 La gestion mémoire
Le système doit partager non seulement le temps entre les processus,
mais aussi la mémoire. La mémoire centrale des ordinateurs actuels
est de l'ordre de quelques centaines de méga octets, mais cela ne
suffit généralement pas à un grand nombre d'utilisateurs. Chaque
processus consomme de la mémoire. De manière simplifiée, le
système gère le problème de la façon suivante.
Tant que la mémoire centrale peut contenir toutes les demandes, tout
va bien. Quand la mémoire approche de la saturation, le système
définit des priorités d'accès à la mémoire, qu'on ne peut
séparer des priorités d'exécution. Si un processus est en mode
d'exécution, il est logique qu'il ait priorité dans l'accès à
la mémoire. Inversement, un processus endormi n'a pas besoin d'occuper
de la mémoire centrale, et il peut être relégué dans une autre
zone mémoire, généralement un disque. On dit que le processus a
été swappé2.
Encore plus précisément, chaque processus opère sur de la
mémoire virtuelle, c'est-à-dire qu'il fait comme s'il avait toute
la mémoire à lui tout seul. Le système s'occupe de faire
coincider cette mémoire virtuelle avec la mémoire physique. Il
fait même mieux : il peut se contenter de charger en mémoire la
partie de celle-ci dont le processus a vraiment besoin à un instant
donné (mécanisme de pagination).
12.3.5 Le mystère du démarrage
Allumer une machine ressemble au Big bang : l'instant d'avant,
il n'y a rien, l'instant d'après, l'ordinateur vit et est prêt à
travailler.
La première tâche à accomplir est de faire charger en mémoire
la procédure de mise en route (on dit plus souvent boot) du
système. On touche là à un
problème de type poule et oeuf : comment le système peut-il se
charger lui-même ? Dans les temps anciens, il s'agissait d'une
procédure quasi manuelle de reboot, analogue au démarrage des
voitures à la manivelle. De nos jours, le processeur a peine
réveillé va lire une mémoire ROM contenant les
instructions de boot. Après quelques tests, le système
récupère sur disque le noyau (fichier /vmunix ou ...). Le
programme de boot transfert alors la main au noyau qui commence à
s'exécuter, en construisant le processus 03,
qui crée le processus 1 (init) et se transforme en le
processus kswapd.
12.4 Gestion des flux
Un programme UNIX typique prend un ou plusieurs flux en entrée,
opère un traitement dessus et ressort un ou plusieurs
flux. L'exemple le plus simple est celui d'un programme s'exécutant
sur un terminal : il attend en entrée des caractères tapés par
l'utilisateur, interprète ces caractères d'une certaine manière
et ressort un résultat sur le même terminal. Par exemple, le
programme mail permet d'envoyer un email à la main :
unix% mail moimeme
Subject: essai
144
.
Un exemple plus élaboré, qui reprend le même principe, est le
suivant. Plutôt que lire les commandes sur le clavier, le programme
va lire les caractères sur son entrée standard, ici un fichier
contenant le nombre 144 :
unix% mail moimeme < cmds
Notez la différence avec la commande
unix% mail cmds
qui transforme cmds en un argument passé au programme et qui
provoque une erreur (à moins qu'un utilisateur s'appelle cmds...).
Dès qu'un programme s'exécute, il lui est associé trois flux :
le premier est le flux d'entrée, le deuxième le flux de sortie, le
troisième est un flux destiné à recueillir les erreurs
d'exécution. Ainsi, on peut rediriger la sortie standard d'un
programme dans un fichier en utilisant :
unix% java carre < cmds > resultat
En sh ou en bash, la sortie d'erreur est récupérée
comme suit :
unix% mail < cmds > resultat 2> erreurs
Pour le moment, nous nous sommes contentés de gérer les flux de
façon simple, en les fabriquant à l'aide du contenu de
fichiers. On peut également prendre un flux sortant d'un
programme pour qu'il serve d'entrée à un autre programme. On peut
lister les fichiers d'un répertoire et leur taille à l'aide de la
commande ls -s :
unix% ls -s > fic
unix% cat fic
total 210
138 dps
1 poly.tex
51 unix.tex
20 unixsys.tex
Une autre commande d'UNIX bien commode est celle permettant de trier
un fichier à l'aide de plusieurs critères. Par exemple,
sort +0n fic permet de trier les lignes de fic suivant
la première colonne :
unix% sort +0n fic
total 210
1 poly.tex
20 unixsys.tex
51 unix.tex
138 dps
Pour obtenir ce résultat, on a utilisé un fichier intermédiaire,
alors qu'on aurait pu procéder en une seule fois à l'aide de :
unix% ls -s | sort +0n
Le pipe (tube) permet ainsi de mettre en communication la sortie
standard de ls -s avec l'entrée standard de sort. On
peut bien sûr empiler les tubes, et mélanger à volonté
>, < et |.
On a ainsi isolé un des points importants de la philosophie
d'UNIX : on construit des primitives puissantes, et on les
assemble à la façon d'un mécano pour obtenir des opérations
plus complexes. On n'insistera jamais assez sur l'importance de
certaines primitives, comme cat, echo, etc.
12.5 Protéger l'utilisateur
Nous avons déjà mentionné que l'espace de travail de l'utilisateur
était protégé d'interférences extérieures.
En Unix, il existe également des moyens pour préserver la vie privée des
utilisateurs. Certes moins puissants que dans Multics, les droits
d'accès aux fichiers permettent d'empêcher certains acteurs de lire ou
modifier les fichiers d'un utilisateur (à l'exception notable de
root).
Chaque utilisateur appartient à un groupe défini dans le fichier
/etc/group. On distingue trois niveaux d'acteurs:
l'utilisateur, son groupe, et les autres. Quand l'utilisateur crée un
fichier, celui-ci se voit attribuer des droits par défaut, que l'on
peut observer à l'aide de la commande ls -lg, comme dans l'exemple:
-rw-r--r-- 1 morain users 3697 Apr 7 18:08 poly.tex
Le nom du fichier est poly.tex. Immédiatement à sa gauche, on
trouve la date de dernière modification. À gauche encore, sa taille en
octets. On voit alors que ce fichier a été créé par l'utilisateur
morain qui appartient au groupe users. Regardons
maintenant le premier champ, dont la logique veut qu'il soit lu en
quatre morceaux:
- rw- r-- r--
Le premier - indique qu'il s'agit là d'un fichier, par
opposition à un répertoire, repéré par un d. Le premier paquet
de trois symboles correspond à l'utilisateur, le deuxième au groupe de
l'utilisateur, le troisième aux autres. À l'intérieur d'un paquet, un
symbole peut avoir une valeur -, auquel cas la propriété est
refusée. Le premier symbole peut valoir r, c'est la permission
en lecture; le deuxième symbole w correspond à la permission
d'écrire et le dernier à celle de pouvoir exécuter le fichier. On peut
changer ces permissions par la commande chmod. Par exemple, la
commande
unix% chmod o-r poly.tex
empêche les autres de lire le fichier:
unix% ls -lg poly.tex
-rw-r----- 1 morain users 3697 Apr 7 18:08 poly.tex
Notons pour finir que les droits de création des fichiers sont régis
par défaut par la variable d'environnement UMASK qui est
positionnée au lancement de l'environnement de l'utilisateur. Très
souvent, pour éviter les ennuis, celle-ci a une valeur qualifiée de
parano pour protéger les nouveaux arrivants. C'est le cas à
l'École.
- 1
- La
première version (0.01) a été diffusée en août 1991.
- 2
- C'est du franglais, mais le terme est
tellement standard...
- 3
- Dans Linux,
c'est un peu différent : des processus spéciaux sont créés en
parallèle au lancement, mais l'idée est grosso-modo la même.