Précédent Remonter Suivant

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

Précédent Remonter Suivant