Vue d'ensemble de l'architecture de l'espace noyau

L'architecture du noyau de Manux est assez différente de celles des noyaux habituels. Voyons cela plus en détail.

Un noyau modulaire monospace

Le noyau est décrit comme ayant une architecture "modulaire monospace". Dans la mesure où cette désignation a été créée spécialement pour lui, il semble utile de préciser sa signification.

Le noyau Linux est monolithique modulaire, c'est-à-dire qu'il est composé d'un monolithe central auquel s'ajoutent des modules éventuels chargés dynamiquement (du moins, dans la configuration la plus courante; il peut aussi être compilé de façon purement monolithique), l'ensemble s'exécutant dans un espace d'adressage unifié.

Le noyau de Manux s'exécute lui aussi dans un espace d'adressage unifié. Toutefois, il est intégralement découpé en modules, et dépourvu de tout monolithe central. De plus, actuellement, aucun module n'est amovible.

Pour être exact, les éléments du noyau se répartissent en trois catégories :

L'amorce est le permier élément chargé. Il s'agit techniquement d'un exécutable ELF au format multiboot, auquel le chargeur de démarrage transfère le contrôle du processeur juste après avoir fini son travail. Du point de vue du noyau, il s'agit de la portion de code chargée d'effectuer l'initialisation du matériel et des modules.

Les divers modules permanents sont les composants réels du noyau. Ils se répartissent les différentes tâches, et communiquent par appel de fonction direct.

Enfin, les éphémodules sont des éléments chargés d'effectuer une tâche ponctuelle bien précise avant d'être déchargés. Il peut s'agir de vérifier l'absence de corruption de la mémoire d'un module, de tester un module au début de son dévelopement, de réorganiser la mémoire pour l'adapter à l'injection d'une nouvelle version d'un module, etc... Le noyau comporte toujours un et un seul éphémodule en mémoire. Initialement, l'éphémodule chargé est "blanc", qui ne fait rien.

Le découpage en module apporte les avantages suivants :

L'architecture des modules

Les modules sont composés de deux parties : le code et les données. Le code est situé dans un binaire au format ELF, lié statiquement, et codé par la méthode des refs sans defs - c'est-à-dire que ses données sont référencées sans jamais être définies. Théoriquement, une telle approche devrait entraîner un code incompilable. En fait, l'adresse des données est injectée lors de l'édition de liens (fichier scripts_ld/<module>.lds), ce qui fonctionne très bien, mais implique que les modules ne peuvent s'exécuter avant qu'un code extérieur (l'amorce) n'ait alloué et initialisé leurs structures de données.

Quant aux données, elles sont composées de deux portions contigües : le bordereau et le tas. Le tas est la zone de mémoire dans laquelle les allocations dynamiques ont lieu, par la fonction Alloue_mem(). Le bordereau est la structure mémoire contenant ou référençant l'ensemble des données du module.

A titre d'exemple, voici la structure du bordereau du module des tubes :

struct bordereau_tubes {
        struct en_tete_bordereau etb;
        struct tube tubes[NB_MAX_TUBES];
} __attribute__((packed));

L'en-tête du bordereau contient les informations requises pour réaliser une allocation dynamique dans le tas, et les structures tube contiennent les données requises pour gérer les différents tubes.

Les avantages de cette architecture sont les suivants :

Conventions de codage associées

Dans le code (et les données) du noyau, les conventions de codages suivantes sont appliquées :

(Cette liste n'est pas exhaustive; elle ne recense que les éléments intéressant l'architecture du noyau.)

Intéret de ces conventions

Les fonctions déclarées "exported" sont les interfaces des modules. C'est à travers elles que les modules communiquent. Evidemment, il y a un problème : lorsque gcc compile leur code, il leur attribue des adresses arbitraires au sein du module. Bien sûr, on pourrait à l'édition de liens informer chaque module des adresses des interfaces des autres modules, mais une telle opération rendrait la compilation d'un module dépendante de celle des autres, et poserait des difficultés très élevées lors des patchages dynamiques.

Pour résoudre ce problème, une petite planche de code est placée au début du module, chargée d'appeler les diverses interfaces depuis une position connue (il s'agit du fichier inclus/liens/<module>.s , voyez le code source pour plus de détails). La position de chacun des appels de cette planche est une constante facilement calculable, ce qui permet de rendre chaque module indépendant de la version des autres.

A titre d'exemple, voici un désassemblage du début du module gmm :

Disassembly of section code:

f14000d4 <code>:
	...
f1400100:	e9 fb 04 00 00       	jmp    0xf1400600
f1400105:	90                   	nop
f1400106:	0f 0b                	ud2    
f1400108:	e9 83 05 00 00       	jmp    0xf1400690
f140010d:	90                   	nop
f140010e:	0f 0b                	ud2    
f1400110:	e9 db 05 00 00       	jmp    0xf14006f0
f1400115:	90                   	nop
f1400116:	0f 0b                	ud2    
	...
f1400120:	55                   	push   %ebp
f1400121:	57                   	push   %edi
f1400122:	bf 01 00 00 00       	mov    $0x1,%edi
f1400127:	56                   	push   %esi
f1400128:	53                   	push   %ebx

La planche de code initial s'étend du début du module jusqu'en 0xf1400117 inclus. Elle est suivie d'un trou de 9 octets introduit par l'éditeur de liens, puis par le code du module tel que compilé par gcc. Au point 0xf1400100 se trouve l'interface Alloue(), qui est l'interface 0 de ce module; elle transfère le contrôle à la fonction alloue() du code C, située ici en 0xf1400600. Deux autres interfaces suivent (Libere() et Realloue()), après quoi s'étend le binaire correspondant au fichier noyau/gmm/main.c .

Lors d'une recompilation, les adresses des fonctions du code C changent, mais pas celui des planches initiales. De la sorte, la compilation de chaque module ne dépend pas de celle des autres, et il est possible de modifier dynamiquement chaque module séparément.

Accessoirement, la convention de codage facilite l'identification de la nature des divers éléments par le programmeur. Exemple tiré du module p3p :

        Inodes[j] = Alloue_mem(sizeof(struct inode_p3p), 0);
        verifie((Inodes[j] != NULL), RETOURNE, NULL, 0);

        Num_inode_courante = ((j+1) % P3P_NB_MAX_INODES);

Ici, nous voyons que Inodes est une structure du bordereau de ce module, j une variable locale, Alloue_mem() une fonction fournie par un autre module (en fait, c'est une macro cachant la fonction Alloue(), mais qu'importe); verifie() est une fonction locale (à nouveau, c'est une macro, mais elle n'appelle pas en soi de fonction externe); NULL, RETOURNE et P3P_NB_MAX_INODES sont des définitions du préprocesseur, et Num_inode_courante est une variable stockée dans le bordereau.

Index de la documentation
Page principale