⚙️ Sur les OS et les noyaux

| ~ 8 mins | 1561 mots

Introduction

J’utilise Gnu/Linux tous les jours, j’ai eu autrefois cours sur les systèmes d’exploitation, fait un peu de VHDL et d’assembleur… Et pourtant le tutoriel Operating System in 1,000 Lines 🇬🇧 m’a appris, réappris et fait mieux comprendre ce qu’est réellement un système d’exploitation et quel est le rôle concrêt d’un noyau (aka kernel).

Cela a aussi abouti à pas mal de questionnement sur le fonctionnement de tout cela auquel j’ai recherché des réponses.

J’ai donc décidé de re-clarifier pour moi et pour d’éventuels lecteurs, quelques points en essayant de rester à la fois simple et clair.

L’espace de mémoire physique présenté au processeur est “virtuel”.

Un noyau doit décider comment il organise la mémoire. Le processus de boot lui donne accès à un espace mémoire dite “physique”. Pour être un plus concrêt, un noyau à d’une façon ou d’une autre une “carte” de l’organisation de la mémoire, par exemple via un linker script.

Mais qu’est-ce qui se cache derrière l’adresse 0x00000 ou 0x30000 ? la RAM ? un contrôleur de la carte mère quelconque ? le GPU ? Autre-chose ?

En réalité, l’allocation de tel ou tel parti de cet espace à tel ou tel mémoire est définie pas une spécification (lié à l’architecture du processeur et le processeur en lui-même). Il y a ainsi un bon nombre de “trou” dans cet adressage. L’adresse 0x02323 pointe peut-être vers rien du tout.

Donc l’espace de mémoire “physique” est, lui-même, une représentation « virtuelle » de différentes mémoires accessibles, non pas forcément dans le sens ou l’on pourrait le changer dynamiquement (il n’est pas très clair pour moi ce qui est physiquement fixé ou non là-dedans), mais plutôt dans le sens ou plusieurs mémoires bien différentes se retrouve accessibles depuis un même espace d’adressage de façon transparente pour le noyau (qui a intérêt à savoir réellement où se trouve chaque chose bien évidemment).

Il existe deux grandes méthodes pour organiser l’adressage l’espace mémoire classique (RAM) et l’espace IO :

On pourrait penser que les addresses des IO sont fixés, mais cela ne semble pas si simple. Il existe des interactions possibles pour allouer la mémoire nécessaire.

Un exemple concrêt de l’interaction possible est la technologie rezisable BAR pour les GPU qui permet de négocier la taille allouer pour l’accès au GPU, autrement la taille sera limité et il faut donc un mécanisme pour passer à la donnée suivante et ainsi des cycles cpu perdus.

Dans le cas de périphériques tel qu’un disque dur ou autres, un principe qu’on retrouve beaucoup est :

Adressage Physique

Les processus sont déjà une première sorte de “conteneur”.

Si vous connaissez les conteneurs et leur logique d’isolation, pensez que les processus sont déjà un premier mécanisme d’isolation du système :

Pagination

syscall

Ainsi les processus ne peuvent pas intéragir sur d’autre processus par défault et doivent demander l’accès pour bon nombre de tâches. Un certain nombre de mécanismes existent néanmoins (en passant obligatoirement par des syscall) pour au final permettre à ces processus de pouvoir communiquer entre eux, ce sont les IPC(Inter processus communication)… mais ce sera un sujet pour un autre article 😉.

Le noyau définis des concepts basiques de l’informatique

Les concepts tels que la pile, le tas, les processus sont tellement habituel pour un informaticien lambda qu’ils apparaissent comme naturel, il est donc un peu troublant, de se retrouver à devoir les implémenter, concrètement, simplement et de se demander à quoi pourrait ressembler un autre paradigme de fonctionnement de l’OS sans ces éléments ?

Ainsi dans le tutoriel, on implémente des choses telles que :

La structure d’un processus dans le tutoriel est très simple :

struct process {
    int pid; // 0 if it's an idle process
    int state; // PROC_UNUSED, PROC_RUNNABLE, PROC_EXITED
    vaddr_t sp; // kernel stack pointer
    uint32_t *page_table; // points to first level page table
    uint8_t stack[8192]; // kernel stack
};

Bien évidemment, dans le cas d’un noyau vraiment utilisé, les choses sont plus complexe. Mais il est amusant de voir à quel point tout cela est à la fois simple et réellement compliqué.

Un autre point intéressant a noté de voir qu’une partie de la mécanique tiens dans le code du kernel et une autre dans le rôle et l’implémentation même des instructions CPU. Ainsi, on a :

Ces éléments sont un peu complexe à comprendre, notamment, car il nécessite de se plonger dans les méandres de la documentation d’architecture CPU, mais ce qu’il faut en déduire c’est que le CPU possède divers mécanismes pour simplifier la vie des concepteurs de système d’exploitation.

Programmer un pilote (driver) n’est (presque) qu’implémenter une spécification d’API

Dans la partie sur l’espace d’adresse physique, je parlais d’espace de configuration disponible via MMIO pour les IO. Mais il ne s’agit pas de remplis ces espaces de données au hasard.

Les appareils d’entrée/sortie une toute une spécification, je dirais même une API que le noyau doit respecter pour utiliser l’appareil, c’est (presque) aussi simple que ça un pilote.

Le tutoriel pour sa part nous donne pour mission d’implémenter un disque virtuel via Virtio et il est relativement amusant de voir qu’il s’agit pour une bonne grosse part de l’application stricte d’une spécification.

Les interruptions CPU sont essentielles au fonctionnement

Les interruptions ?

Les interruptions CPU sont un mécanisme, à l’image des Signaux POSIX (SIGINT, SIGKILL, etc.) qui permet au CPU d’exécuter un code spécifique (configurable) quand le CPU est confronté à des événements tels que :

Tout comme les Signaux POSIX, il est possible de se “brancher dessus”.

C’est ce que fait le noyau :

vie d’un processus

Un processus passe généralement souvent dans le mode noyau à intervalles réguliers, ne serait qu’à cause des appels systèmes.

Les interruptions sont la base d’un os préemptif

Dans le tutoriel, l’os se veut coopératif, le processus en cours indique de lui-même quand il redonne la main au noyau pour une nouvelle tâche.

Dans les systèmes d’exploitation récents, le mécanisme est préemptif, c’est-à-dire que le noyau peut arrêter un processus en cours de route.

Seulement voilà, comment faire cela ?

La solution ce sont les interruptions. Tout le mécanisme de préemption fonctionne dessus. En trouvant différente manière de profiter d’interruption qui interromprait le processus, on revient au mode noyau et on peut alors, décider de ne plus re-exécuter le code de ce processus.

Dans les faits, il y a toute une mécanique très complexe d’ordonnancement dans linux là-dessus pour organiser au mieux le temps alloué aux différents processus et leur ordre.

Quant aux interruptions, une astuce pour éviter qu’un processus reste éternellement en fonctionnement en mode utilisateur si aucune interruption n’est provoqué, est tout simplement l’existence d’interruption à intervalles réguliers, configurable via l’option de Compilation CONFIG_HZ.

Pour conclure

Le développement de Kernel/Noyau ne semble pas pour moi, j’ai l’impression qu’une partie notable du travail consiste à implémenter des spécifications complexes (jeux d’instruction CPU, spécification de pilote) dans le contexte d’un langage relativement bas niveaux, c’est une discipline très exigeante à mille lieues de la fluidité du développement python auquel j’ai l’habitude.

Néanmoins, mieux comprendre et démythifié les bases sur lequel son propre code fonctionne permet d’avoir une capacité supplémentaire d’action en cas de problèmes.

Je ne peux donc que vous conseiller de faire ce tutoriel. Le tutoriel s’avère plus ou moins rude selon vos connaissances ainsi je vous conseillerais de ne pas forcément chercher à comprendre le moindre détail, mais d’avancer tranquillement dessus (et ne pas hésiter le copier/coller… vous n’avez pas tous prévu de développer des OS, non ?).

Bonne création de noyau à tous 😀.