[Cours système Linux – Episode 2] Les rôles du noyau

[Cours système Linux – Episode 2] Les rôles du noyau

Dans cette partie nous allons voir les nombreux rôles que je le noyau Linux, ce qui vous montrera la complexité d’un tel programme, et fera comprendre que seuls les meilleurs programmeurs du monde peuvent entreprendre une tâche aussi titanesque et complexe.

Gestion de la mémoire

Le premier rôle du noyau est de gérer la mémoire, la fameuse RAM, présente sur l’ordinateur. Pour ce faire, le noyau qui est, rappelons le, le premier programme à monter en mémoire, va se loger dans une zone à haut privilège de la mémoire physique, et va considérer que le reste de la mémoire physique est de la mémoire virtuelle.

La mémoire est découpée en pages. La taille de ces pages fait, selon l’Unix sur lequel on se trouve, généralement 4 à 8 Ko. Le lien entre la notion de mémoire virtuelle et la notion de mémoire physique se fait de la manière suivante :

Le noyau gère des processus. Les processus se sont les programmes en mémoire. Par exemple, lorsque je lance mon navigateur Internet, j’ai un programme en mémoire, donc un processus. Sur la figure ci-dessus, sont représentés deux processus X et Y. La mémoire physique de l’ordinateur, la seule qui existe vraiment, est représentée au milieu.

On voit que le noyau charge dans des pages physiques des bouts de programmes, et il reconstitue le code d’un processus donné en maintenant des tables de pages, une par processus. La table des pages du processus X dit, par exemple, que la page virtuelle 6 du processus X est contenue réellement dans la page physique 3 de la mémoire physiquement installée sur l’ordinateur. La page virtuelle 6 du processus Y est, selon la table des pages de ce processus, contenue physiquement en page physique 4. Etc.

Le concept est donc très simple, même si le noyau fait lui une gymnastique très complexe pour maintenir en cohérence cette architecture.

Pourquoi les choses se passent ainsi, et non en utilisant uniquement de vraies pages physiques ? Eh bien parce qu’il n’y a que des avantages à fonctionner comme cela.

  • Premièrement, le noyau fait ainsi croire aux processus que la machine a beaucoup plus de mémoire qu’elle n’en a en réalité. Vu d’un processus, tout se passe comme si la quantité de RAM qui lui est disponible était infinie. Il n’y a donc pas de taille maximale exécutable pour un programme
  • Ensuite, chaque processus a ainsi son propre espace d’adressage virtuel, indépendant de celui des autres processus. Un programme ne peut donc pas aller écrire dans l’espace mémoire d’un autre programme.
  • Les pages partagées par plusieurs processus ne sont chargées qu’en un seul exemplaire en mémoire physique, ce qui va dans le sens d’une optimisation de l’utilisation de la mémoire physique.
  • La mémoire physique est partagée équitablement entre tous les processus.

Comme la taille de la mémoire virtuelle (qui est infinie) est supérieure à la taille de la mémoire physique présente sur la machine, le noyau doit gérer efficacement l’utilisation de la mémoire physique. Il ne charge donc en mémoire que les pages qui sont réellement utilisées.

Si un processus tente d’accéder à une page virtuelle non chargée en mémoire physique, c’est-à-dire non référencée dans la table des pages, le micro-processeur déclenche une faute de page. Le Noyau charge alors la page virtuelle manquante dans une page physique libre. Un bout de code jamais exécuté ne sera donc jamais chargé en mémoire.

Revers de la médaille, Unix/Linux ne peut fonctionner que sur des micro-processeurs modernes équipés de MMU (Memory Management Unit). Rassurez-vous, tous les processeurs ont maintenant un MMU, c’est pourquoi on retrouve Linux partout.

Lors d’une faute de page, s’il reste une page physique libre, la page virtuelle est chargée dans cette page libre. S’il ne reste plus de page physique libre, le Noyau doit écraser une page physique existante pour la remplacer :

● Si la page choisie n’a été accédée qu’en lecture, elle est écrasée. Le processus qui l’utilise la fera recharger, s’il en a encore besoin, lors d’une prochaine faute de page
● Si la page choisie a été accédée en écriture, elle est sauvegardée dans un fichier spécial appelé Swap (ah, c’est donc ça le swap ? Eh oui, c’est tout simple).

Le noyau doit donc choisir une page à écraser. Comment fait-il ce choix ? Cela dépend de l’Unix sur lequel on est, mais généralement on va au plus simple : Chaque page comporte un âge, maintenu par le noyau. L’âge est mis à jour à chaque fois que la page est accédée. Plus une page a été accédée récemment, plus elle est jeune. Les vieilles pages sont de bonnes candidates pour l’écrasement ou le swapping. C’est ce qui est mis en œuvre dans le noyau Linux.

Pour en terminer sur l’aspect gestion de la mémoire faite par le noyau, sachez que celui-ci gère aussi des caches mémoire. Citons en deux :

  • Cache de périphériques blocs : il garde en mémoire les dernières données lues sur les périphériques de type bloc (disques durs, etc…), de sorte que, si l’on a besoin d’y ré-accéder plus tard on les lira directement en mémoire au lieu d’aller les chercher sur le disque dur, c’est beaucoup plus rapide. Sous Linux, tant qu’il reste des pages physiques non utilisées, le noyau les utilise comme cache disque.
  • Cache de swap : lorsqu’une page a été swappée puis rechargée puisqu’il est à nouveau nécessaire de la swapper, le Noyau sait grâce à ce cache qu’il peut l’écraser directement puisqu’elle est déjà dans le swap.

Vous pouvez constater que, rien que sur la gestion de la mémoire que fait le noyau, c’est déjà du lourd en termes de programmation. Voyons les autres rôles du noyau.

Gestion des processus

La ressource la plus critique sur un ordinateur est le CPU (Central Processing Unit, ou micro-processeur en français), car il y en a généralement peu sur une machine. Le cas le plus courant est même qu’il n’y a qu’un seul CPU sur la machine.

Comme Linux est conçu pour pouvoir faire tourner plusieurs processus simultanément, généralement le nombre de processus est supérieur au nombre de CPU. Le noyau doit donc scheduler.

Le CPU est alloué à chaque processus à tour de rôle, par tranche de temps. L’allocation du CPU d’un processus à l’autre est indépendante de la “volonté“ des processus. C’est le Noyau qui décide. C’est pourquoi on dit qu’Unix/Linux est un système multi-processus préemptif. Les processus n’ayant pas le CPU sont mis en attente.

L’astuce est qu’il ne faut pas qu’un processus se rende compte d’une manière ou d’une autre, qu’on lui a retiré le processeur a un moment donné. Pour cela, le noyau fait attention à bien noter l’état général d’exécution au moment où il retire le processeur à un processus. Cet état général d’exécution (contenant par exemple l’état complet du processeur, registre par registre) est sauvegardé avec le processus mis de côté. Ainsi, lorsque plus tard ce processus aura de nouveau le processeur, ce contexte général d’exécution sera restauré avant de donner la main au processus dans son exécution. Le processus ne saura alors jamais qu’il a été arrêté et mis de côté puisque tout se passe dans la continuité, comme si l’arrêt n’avait jamais eu lieu.

Vu du noyau, un processus c’est donc une structure assez élaborée dont la bonne gestion est complexe :

  • Une image à exécuter
  • Un état
  • Un identifiant (PID)
  • Un identifiant du processus parent (PPID)
  • Des droits (UID, GID, EUID, GUID)
  • Un environnement
  • Un contexte
  • Des informations temporelles
  • Une table des descripteurs de fichiers
  • Des informations relatives au scheduling

Regardons de plus près ces notions, à commencer par l’image à exécuter.

Le programme à exécuter est stocké sous forme de fichier exécutable sur un support de masse. Lorsqu’on lance ce programme, un processus contenant le programme est créé en mémoire, selon les principes de pages virtuelles / pages physiques que nous avons déjà vus, les pages étant chargées au fur et à mesure des défauts de pages.

Concernant maintenant l’état du processus : puisqu’un processus peut être à minima en cours d’exécution à un instant donné, ou mis de côté, on voit déjà qu’il peut avoir deux états possibles au moins.

En réalité les Unices (rappel : Unices = pluriel d’Unix) ont plus que deux états pour définir l’actualité d’un processus. Le classique étant les 4 états : Running, Waiting, Stopped et Zombie.

Running : le processus a actuellement un processeur et est en cours d’exécution.

Waiting : le processus n’a plus le processeur et est mis en file d’attente en attendant que son tour revienne.

Stopped : le processus a été stoppé, généralement suite à la réception d’un signal. Nous verrons ultérieurement les signaux dans le cadre de ce cours.

Zombie : le processus est mort de sa belle mort, mais son processus père n’a pas encore lu son code de sortie.

On observera des processus zombie dans ce cours quand on passera à la pratique.

Pour la petite histoire, l’état Waiting (appelé aussi “Sleeping”), peut en réalité avoir deux sous-états : waiting interruptible, qui signifie que l’attente du processus peut être interrompue par un signal, et waiting ininterruptible, qui signifie que l’attente du processus ne peut pas être interrompue (généralement lorsque le processus est en attente d’une condition sur le matériel).

Nous avons évoqué ci-dessus la notion de processus père, sur laquelle il est nécessaire de revenir pour bien comprendre.

Sous Linux, et sous Unix en général, les processus portent tous un numéro, appelé PID (Process ID). Lorsque le noyau a terminé de booter, le seul processus qu’il lance lui-même s’appelle init (/sbin/init). Init porte donc le PID de numéro 1. C’est init qui va ensuite lancer les processus de niveau 2, qui seront ses processus fils. Eux-mêmes vont lancer d’autres processus, etc.

L’ensemble des processus lancé sur une machine Unix/Linux forme donc un arbre, chaque processus ayant son propre numéro (PID) attribué par le noyau, et ayant un seul processus père, ayant lui aussi un PID, qui est donc le PPID (PID du processus père) du processus qu’on regarde.

Si vous avez bien suivi, vous avez déjà compris qu’init est le seul processus à ne pas avoir de PPID puisqu’il n’a pas de père.

L’arbre des processus reste, quoi qu’il arrive, un arbre. C’est à dire que si un processus meurt, par exemple ksh de PID 192 dans l’exemple ci-dessus, alors ses fils seront adoptés par init (le processus netscape se retrouvera directement rattaché sous init, gardera son PID à 225 mais aura un PPID de 1.

Les étudiants en informatique l’apprennent sous cette forme :

init, père de tous les processus et adopteur des orphelins

Une question que vous vous posez certainement est de savoir pourquoi c’est le noyau qui donne les PID alors même qu’on vient de dire que le noyau ne lance qu’init et qu’ensuite init et ses fils se débrouillent pour lancer toute la descendance. La réponse est simple : lancer un fils lorsqu’on est un processus, cela se fait en demandant gentillement au noyau de bien vouloir le faire, et donc cela se fait grâce à un appel système (exec pour ne pas le nommer, on le verra en exercice).

Venons en maintenant aux droits.

Chaque processus a des droits sur les fichiers (et donc sur les périphériques car ils apparaissent comme des fichiers sous Unix). Ces droits sont déterminés par son UID et son GID. Par défaut, un processus à l’UID et le GID de l’utilisateur qui l’a lancé.

Mais un processus a également un UID effectif et un GID effectif (EUID et EGID). Les ID et EID sont différents pour les exécutables munis du “Sticky bit” (bit s). Ce mécanisme permet de prendre temporairement l’identité de quelqu’un d’autre (et donc d’augmenter potentiellement les droits du processus. Nous verrons ces notions de manière concrète lorsqu’on passera à la pratique.

En ce qui concerne l’environnement d’un processus maintenant : L’environnement d’un processus est une liste de variables et leurs
valeurs, associées au processus. Ces variables, propres à chaque processus, sont destinées à être lues ou modifiées par le programme tournant dans le processus.

Les variables classiques sont HOME (permet de retrouver le home directory de l’utilisateur courant), DISPLAY (permet de retrouver le serveur X Window qui affiche la fenêtre courante), LD_LIBRARY_PATH (chemin de recherche des librairies dynamiques), PATH, etc.

Informations temporelles

Le noyau garde en mémoire le moment exact de création des processus ainsi que le temps CPU dont chacun bénéficie au cours de
sa vie (et ce en mode noyau et en mode utilisateur). Le noyau gère également des timers que les processus peuvent utiliser
via les appels systèmes pour s’envoyer des signaux à eux-mêmes par exemple.

Ces timers peuvent être single – shot ou périodiques.

Table des descripteurs de fichiers

Le noyau maintient pour chaque processus une table des descripteurs de fichiers. Chaque entrée de la table pointe soit vers rien, soit vers un fichier ouvert (en lecture, en écriture ou les deux). Lorsque la table est pleine, le processus ne peut plus ouvrir de nouveaux fichiers sans en fermer d’autres. N est fixé avant la compilation du noyau (valeur standard 4096).

Les 3 premières entrées dans cette table jouent un rôle particulier. 0 est l’entrée standard, 1 la sortie standard et 2 l’erreur standard.

Typiquement l’entrée 1 est celle qui sera utilisée si l’on fait un simple printf dans un programme en C. L’entrée 0 sera celle qui sera utilisée si l’on fait un simple gets dans un programme en C.

Bien sûr rien n’est figé. Il existe des appels systèmes pour fermer l’entrée qu’on souhaite dans la table, ou l’ouvrir sur une autre connexion, ou dupliquer une autre entrée, etc. On verra tous ces appels système en exercice.

Informations de scheduling

Le noyau schedule les processus donc il doit être capable de décider quel processus “mérite” le plus d’avoir le CPU.

Pour pouvoir le faire, il maintient un certain nombre d’indicateurs liés à chaque processus : priorité, historique du processus en matière de temps CPU, etc.

Tous les unices n’implémentent pas la même politique de scheduling mais tous maintiennent ce genre d’indicateurs.

Voyons maintenant le rôle suivant qu’assure le noyau.

Pilotage des périphériques

Le noyau est le seul programme capable d’accéder physiquement au matériel. Pour que les programmes puissent accéder au matériel, s’ils en ont les droits, il est nécessaire qu’il existe un mécanisme le permettant.

Ce mécanisme est présenté ci-dessus. Le noyau possède en son sein un pilote (un driver en anglais) qui sait comment accéder au matériel. Dans le répertoire /dev de la machine, on crée des fichiers spéciaux qui sont en fait reliés à ces pilotes de périphériques. Ecrire dans ces fichiers spéciaux revient à écrire directement dans le périphérique. Pareil pour lire.

Ainsi, jouer un son par exemple sous Unix est très simple : il suffit d’ouvrir le fichier spécial qui est relié à la carte son dans /dev, d’y envoyer le fichier son à jouer, et le son sort dans les enceintes ! (les appels systèmes dont il est fait mention dans la figure ci-dessus, n’étant que des read et des write pour écrire des données dans le fichier spécial, et ioctl pour configurer le périphérique. Plus évidemment des open et des close pour ouvrir et fermer l’accès au périphérique).

Gestion des systèmes de fichiers

En tant qu’utilisateur de votre système Unix / Linux, vous ne voulez pas vous soucier des différents systèmes de fichiers contenant les fichiers auxquels vous souhaitez accéder.

En tant que programmeur par exemple, vous souhaitez pouvoir accéder aux fichiers en envoyant un appel système open au noyau, sans vous soucier de savoir si le fichier que vous visez est sur un disque FAT, NTFS ou Ext4.

Pour que cela soit possible, le noyau virtualise complètement les systèmes de fichiers.

A l’ouverture d’un fichier par un processus (appel système open envoyé au noyau), une des cases de la table des descripteurs de fichiers du processus se retrouve connectée à un système de fichier virtuel, qui lui sait quelles connexions il doit activer parmi les méandres des drivers présents dans le noyau, et ce en fonction de la localisation exacte du fichier que vous visez. Toute la complexité de la recherche et de la gestion des accès aux fichiers se fait dans le noyau; les applications ne font qu’un simple open, et ça marche.

On comprend mieux pourquoi Linus Torvalds explique dans son bouquin qu’il a passé 3 mois à coder le seul appel système open.

Appels système

On termine par eux, mais en réalité ils sont vitaux. Comme les processus ne peuvent accéder au matériel, ni même aux mécanismes de base (comme la création de processus, les signaux, etc.), ils passent leur temps à faire des appels système au noyau.

Comme expliqué dans l’épisode 1, faire un appel système revient pour le processus à basculer en mode d’exécution noyau pour y exécuter, en bénéficiant des hauts privilèges du noyau à ce moment là, le code de l’appel système figurant dans le noyau. Ce code, écrit par les meilleurs programmeurs du monde, est sûr et fiable.

Une fois l’appel système effectué, qu’il ait réussi ou non (il peut être refusé par le code du noyau si par exemple le processus appelant n’a pas les droits pour faire ce qu’il souhaite faire), le processus repasse en mode utilisateur et se remet à exécuter le code écrit par le programmeur de l’application.

Une des raisons qui fait qu’il n’est pas souhaitable de lancer des applications en mode administrateur, c’est que dans ce mode un programme malveillant peut compiler un comportement différent d’un appel système et venir écraser le code original de l’appel système dans le noyau. Ce n’est pas à la portée du premier venu comme technique, mais on sait bien que les programmeurs de virus ne sont généralement pas connus pour être manchots. Et il est clair que si un virus parvient à ré-écrire le code d’un appel système dans le noyau (open par exemple), vous êtes mal.

Conclusion

Les puristes et autres spécialistes du noyau Linux expliqueraient que le noyau fait plein d’autres choses encore, ce qui est vrai. On n’a pas parlé des couches réseau par exemple.

Cependant, on va s’arrêter là car d’une part les fonctions principales ont été abordées, d’autres part ce balayage a le mérite de bien faire prendre conscience que la programmation d’un noyau de système d’exploitation, c’est la tâche la plus hardue qui soit en informatique, et enfin les bases de compréhension du cours pratique que nous allons abordé par la suite sont là.

Donc respect à Linus Torvalds, Alan Cox et tous les autres programmeurs géniaux du noyau Linux, c’est grâce à eux qu’on a un système dans lequel on peut s’éclater aujourd’hui.

Ok le code du noyau Linux ne fait peut-être que 2% du code total qui tourne sur un Ubuntu 18.04 quand on l’utilise. Mais ce sont les 2% les plus fondamentaux, les plus indispensables, et les plus difficiles à écrire. Sans eux, rien ne serait possible. N’en déplaise à ceux qui pensent que c’est Red hat qui a inventé Linux (celui avec lequel je me suis pris le choux à ce sujet sur le site du Télégramme se reconnaitra).