IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

ldd3

Date de publication : 19/07/2007 , Date de mise à jour : 19/07/2007


IV. Développer et faire fonctionner des modules
IV-1. Mettre en place votre système de test
IV-2. Le module Hello World
IV-3. Modules du noyau contre les applications
IV-3-1. Espace utilisateur et espace noyau
IV-3-2. Concurrence dans le noyau
IV-3-3. Le processus actuel


IV. Développer et faire fonctionner des modules

Il est presque temps de commencer à programmer. Ce chapitre introduit tous les concepts essentiels à propos des modules et de la programmation du noyau. Dans ces quelques pages, nous développons et faisons fonctionner un module complet (bien qu'assez peu utile) et nous regardons une partie du code basique partagé par tous les modules. Acquérir une telle expérience est un fondement essentiel pour tout genre de pilote modulaire. Pour éviter de décrire trop de concepts à la fois, ce chapitre parle seulement des modules, sans se référer à aucune classe de matériel spécifique.

Toutes les parties du noyau (fonctions, variables, fichiers d'en-tête et macros) introduites ici sont décrites dans une section de référence à la fin du chapitre.


IV-1. Mettre en place votre système de test

Pour commencer ce chapitre, nous présentons des exemples de modules pour établir des concepts de programmation. (Tous ces exemples sont disponibles sur le serveur FTP de O'Reilly comme expliqué dans le Chapitre 1.) Compiler, charger et modifier ces exemples sont une bonne façon d'améliorer votre compréhension du fonctionnement des pilotes et d'intéragir avec le noyau.

Les modules d'exemples devraient fonctionner avec presque n'importe quel noyau 2.6.x, y compris ceux fournis par des revendeurs de distribution. Cependant, nous vous recommandons d'obtenir un noyau non patché directement sur le réseau de miroirs kernel.org et de l'installer sur votre propre système. Les noyaux de revendeurs peuvent être lourdement patchés et diverger de la version "principale". Parfois, les patches de revendeur peuvent changer l'API des pilotes matériels du noyau. Si vous écrivez un pilote qui doit fonctionner sur une distribution particulière, vous voudrez certainement le compiler et le tester sur des noyaux correspondants. Par contre, pour apprendre à écrire des pilotes, un noyau standard reste la meilleure solution.

Indépendamment de l'origine de votre noyau, compiler des modules pour un noyau 2.6.x nécessite que vous ayez configuré et compilé l'arbre du noyau sur votre système. Cette condition est un changement par rapport aux versions précédentes du noyau, pour lesquelles un ensemble de fichiers d'en-tête à jour suffisait. Les modules 2.6 sont liés à partir des fichiers objets trouvés dans les sources de l'arborescence du noyau. Le résultat est un chargeur de modules plus robuste, mais aussi la nécessité que ces fichiers objets soient disponibles. Ainsi votre première tâche est d'obtenir une arborescence source du noyau. Cela est possible soit depuis le réseau kernel.org soit du paquet des sources du noyau de votre distributeur. Ensuite, vous compilerez un nouveau noyau et de l'installer sur votre système. Pour des raisons que nous verrons plus tard, la vie sera généralement plus simple si vous utilisez le noyau cible quand vous compilez vos modules, mais ce n'est pas indispensable.

warningVous devriez aussi réfléchir à l'environnement dans lequel vous allez expérimenter, développer et tester vos modules. Nous avons fait notre maximum pour que nos exemples de modules soient corrects et sécurisés, mais la possibilité de bugs est toujours présente. Des erreurs dans un code noyau peuvent conduire à la fermeture d'un processus utilisateur voire, occasionnellement, du système complet. Ils ne créent normalement pas de problèmes plus sérieux, comme la corruption des données de disques. Néanmoins, il est conseillé de faire vos expériences sur le noyau sur un système qui ne contient pas de données que vous ne pouvez pas vous permettre de perdre et qui ne propose pas de services essentiels. Les experts du noyau conservent typiquement un système "sacrifiable" dans le but de tester un nouveau code.
Si vous n'avez pas un système convenable avec un arbre de source du noyau configuré et compilé sur votre disque, le moment est venu de regarder un peu ça. Nous attendrons. Une fois cette tâche réalisée, vous serez prêt pour commencer à jouer avec des modules du noyau.


IV-2. Le module Hello World

De nombreux livres de programmation débutent avec un exemple de "hello world" de manière à montrer le programme le plus simple possible. Ce livre traite des modules plus que de programmes. Pour le lecteur impatient, le code suivant est un module complet "Hello world" :

#include <linux/init.h>
#include <linux/module.h>
MODULE_LICENSE("Dual BSD/GPL");

static int hello_init(void)
{
	printk(KERN_ALERT "Hello, world\n");
	return 0;
}

static void hello_exit(void)
{
	printk(KERN_ALERT "Goodbye, cruel world\n");
}

module_init(hello_init);
module_exit(hello_exit);
Ce module définit deux fonctions : l'une appelée au chargement du module dans le noyau (hello_init) et l'autre au retrait (hello_exit). Les lignes module_init et module_exit utilisent des macros spéciales du noyau pour indiquer le rôle des deux fonctions. Une autre macro spéciale (MODULE_LICENSE) est utilisée pour signaler au noyau que ce module utilise une licence libre. Sans cette déclaration, le noyau se plaindra quand le module sera chargé.

La fonction printk est définie dans le noyau Linux et est disponible pour les modules. Elle se comporte comme la fonction printf dans la librairie C standard. Le noyau fonctionne seul et a donc besoin de sa propre fonction d'affichage (sans l'aide de la librairie C). Une fois le module chargé par insmod, il pourra appeler printk. En effet, le module sera lié au noyau et pourra donc accéder à ses symboles publics (fonctions et variables détaillées dans la prochaine partie). La chaîne KERN_ALERT est la priorité du message : la priorité est juste une chaîne comme <1>, qui est préfixée à la chaîne de format de printk. Notez l'absence de virgule après KERN_ALERT : ajouter une virgule ici est une faute de frappe fréquente et ennuyante qui est malheureusement prise en compte par le compilateur. Nous avons spécifié une haute priorité dans ce module parce qu'un message avec une priorité par défaut ne se montrera pas à un endroit pratique, ceci dépendant de votre version de noyau, de votre version de klogd et de votre configuration. Vous pouvez ignorer ce problème pour le moment, nous l'expliquerons dans le chapitre 4.

Vous pouvez tester le module avec les outils insmod et rmmod comme montré ci-après. Seul le super-utilisateur peut charger et retirer un module.


% make
make[1]: Entering directory `/usr/src/linux-2.6.10'
CC [M]	/home/ldd3/src/misc-modules/hello.o
Building modules, stage 2.
MODPOST
CC	/home/ldd3/src/misc-modules/hello.mod.o
LD [M]	/home/ldd3/src/misc-modules/hello.ko
make[1]: Leaving directory `/usr/src/linux-2.6.10'
% su
root# insmod ./hello.ko
Hello, world
root# rmmod hello
Goodbye cruel world
root#
Veuillez noter encore une fois : afin que la séquence de commandes ci-dessus fonctionne, vous devez avoir une arborescence de noyau correctement configurée et compilée à un endroit où le Makefile est capable de la trouver (/usr/src/linux-2.6.10 dans l'exemple). Nous entrerons dans les détails sur la manière dont les modules sont construits dans la section "Compilation et chargement".

Selon le mécanisme d'affichage des lignes utilisé par votre système, votre sortie pourrait être différente. En particulier, l'affichage précédent a été pris d'une console texte. Si vous exécutez insmod et rmmod depuis un émulateur de terminal lancé depuis un environnement de bureau, vous ne verrez rien sur votre écran. Le message ira dans un des fichiers de log comme /var/log/messages (le nom peut varier selon les distributions). Le mécanisme utilisé pour délivrer les messages du noyau est décrit dans le chapitre 4.

Comme vous avez pu le voir, l'écriture d'un module n'est pas aussi compliquée que vous auriez pu le penser - du moins tant que le module ne doit pas faire quelque chose d'utile. La partie difficile est de comprendre le fonctionnement de votre périphérique et d'en maximiser les performances. Nous allons plus loin dans la modularisation à travers ce chapitre et laissons les problèmes spécifiques aux périphériques pour d'autres chapitres.


IV-3. Modules du noyau contre les applications

Avant de continuer, il est important de souligner les différences entre un module du noyau et une application.

La plupart des petites et moyennes applications réalisent une tâche unique du début à la fin, alors que chaque module du noyau se déclare lui-même dans le but d'exécuter des requêtes futures; sa fonction d'initialisation se finit immédiatement. Autrement dit, la tâche de la fonction d'initialisation du module est de le préparer à une utilisation ultérieure des fonctions du module; c'est comme si le module disait : "Je suis présent, voici ce que je peux faire". La fonction de fin du module (hello_exit dans l'exemple) est appelée juste avant que le module soit déchargé. Il pourrait dire au noyau : "Je ne suis plus là, ce n'est plus la peine de me demander quoi que ce soit".

Cette approche de programmation est similaire à la programmation événementielle. Toutes les applications ne sont pas événementielles, tandis que chaque module du noyau l'est. Une autre différence majeure entre les application événementielles et le code noyau se trouve dans la fonction de nettoyage : une application qui se termine peut ne pas libérer les ressources ou ne nettoie pas complètement contrairement à la fonction de fin d'un module. Cette dernière doit soigneusement défaire tout ce que la fonction d'initialisation a fait, sinon des morceaux resteront jusqu'à ce que le système soit redémarré.

Par ailleurs, la possibilité de décharger un module est une des fonctionnalités de la modularisation que vous allez le plus apprécier : elle aide à réduire les temps de développement. Vous pouvez donc tester des versions successives de votre nouveau pilote sans avoir à passer par un cycle d'arrêt/redémarrage lent à chaque fois.

En tant que programmeur, vous savez qu'une application peut appeler des fonctions qu'elle ne définit pas : l'étape d'édition des liens résoud les appels externes en utilisant la bibliothèque de fonctions appropriée. printf est une des fonctions appelables et déclarées dans la libc. D'un autre côté, un module est uniquement lié avec le noyau. Les seules fonctions qu'il peut appeler sont celles exportées par le noyau; il n'y a aucune bibliothèque à lier. La fonction printk utilisée précédemment dans hello.c par exemple, est la version de printf définie dans le noyau et exportée aux modules. Elle se comporte comme la fonction d'origine mais avec quelques différences mineures, la principale étant le manque de support des nombres à virgule flottante.

L'illustration 2-1 montre comment les appels de fonction et les pointeurs de fonction sont utilisés dans un module afin d'ajouter de nouvelles fonctionnalités au noyau. Figure 2-1. Lien d'un module avec le noyau

Étant donné qu'aucune bibliothèque n'est liée aux modules, les fichiers sources ne devraient jamais inclure les en-têtes courants. Les seules exceptions sont <stdarg.h> et certaines situations spécifiques. Uniquement les fonctions faisant partie du noyau lui-même peuvent être utilisées dans les modules. Tout ce qui est relatif au noyau est déclaré dans les en-têtes que l'on peut trouver dans l'arborescence des sources du noyau que vous avez installée et configurée. La plupart des en-têtes courants sont situés dans include/linux et include/asm, mais d'autres sous-répertoires spécifiques aux matériels peuvent avoir été créés.

Le rôle de chaque en-tête sera expliqué lorsque nous nous en servirons.

Une autre différence importante entre la programmation du noyau et la programmation des applications est la manière dont chaque environnement gère les erreurs. Une erreur de segmentation est inoffensive dans le développement d'applications et un debugger peut toujours être utilisé pour tracer l'erreur jusqu'au problème dans le code source. En revanche, une erreur du noyau tue le processus courant si ce n'est pas le système entier. Nous verrons comment tracer les erreurs du noyau dans le chapitre 4.


IV-3-1. Espace utilisateur et espace noyau

Un module est lancé dans l'espace noyau alors qu'une application est exécutée dans l'espace utilisateur. Ce concept est à la base de la théorie des systèmes d'exploitation.

Dans la pratique, le rôle des systèmes d'exploitation est de fournir des programmes avec une vue conforme du matériel de l'ordinateur. En outre, le système d'exploitation doit exécuter les programmes indépendamment et protéger les ressources contre des accès non autorisés. Cet tâche difficile est possible uniquement si le processeur impose la protection des logiciels systèmes vis-à-vis des applications.

Chaque processeur moderne est capable d'imposer ce comportement. L'approche choisie est d'implémenter différentes modalités fonctionnelles (ou niveaux) dans le processeur lui-même. Les niveaux ont différents rôles et quelques opérations sont interdites aux niveaux inférieurs. Le code d'un programme peut passer d'un niveau à un autre seulement à travers un nombre limité de portes. Les systèmes UNIX sont conçus pour tirer partie de cette fonctionnalité du matériel en utilisant deux de ces niveaux. Tous les processeurs actuels ont au moins deux niveaux de protection et quelques-uns, comme la famille des x86, ont plus de niveaux. Quand plusieurs niveaux existent, les plus haut et bas niveaux sont utilisés. Sous Unix, le noyau s'exécute dans le niveau le plus haut (aussi appelé mode superviseur) où tout est autorisé. Au contraire, les applications s'exécutent dans le niveau le plus bas (aussi appelé mode utilisateur) où le processeur régule les accès directs au matériel et les accès non autorisés à la mémoire.

Nous nous référons habituellement aux modes d'exécution comme l'espace noyau et l'espace utilisateur. Ces limites n'englobent pas seulement les différents niveaux de privilèges inhérents aux deux modes, mais aussi le fait que chaque mode peut avoir sa propre gestion de mémoire et aussi bien ses propres adresses.

Unix transfère l'exécution de l'espace utilisateur vers l'espace noyau à chaque fois qu'une application fait un appel système ou est suspendue par une interruption matérielle. Le code du noyau exécutant un appel système travaille dans le contexte d'un processus : il fonctionne au nom du processus appelant et est capable d'accéder aux données de l'espace d'adresses du processus. Par ailleurs, le code qui gère les interruptions est asynchrone en ce qui concerne les processus et n'est pas lié à un processus particulier.

Le rôle d'un module est d'étendre les fonctionnalités du noyau; le code modularisé s'exécute dans l'espace du noyau. Habituellement, un pilote réalise les deux tâches présentées précédemment : quelques fonctions dans le module sont exécutées en tant qu'appels système et quelques-unes ont la charge de la gestion des interruptions.


IV-3-2. Concurrence dans le noyau

Une différence majeure entre la programmation d'applications conventionnelles et la programmation noyau consiste en la manière de gérer la concurrence. À l'exception notable des applications multi-threadées, la plupart des applications s'exécutent de manière séquentielle (du début à la fin); il n'est pas nécessaire de se soucier de quoi que ce soit pouvant changer leur environnement. Le code du noyau ne s'exécute pas dans un monde si simple et même les modules noyaux les plus simples doivent être écrits avec l'idée que beaucoup de choses peuvent arriver simultanément.

Il y a quelques sources de concurrence dans la programmation noyau. Naturellement, les systèmes Linux lancent plusieurs processus, plus d'un pouvant essayer d'utiliser votre pilote au même moment. La plupart des périphériques sont capables d'interrompre le processeur; les gestionnaires d'interruption s'exécutent de façon asynchrone et peuvent être invoqués au même moment que votre pilote essaie de faire quelque chose d'autre. Beaucoup d'abstractions logicielles (comme les timers du noyau, introduits dans le chapitre 7) sont également lancés de manière asynchrone. Bien entendu, Linux fonctionne sur les systèmes symétriques multi-processeurs (SMP), avec pour conséquence l'exécution de votre pilote sur plus d'un processeur. Finalement, dans la branche 2.6, le code noyau a été fait de manière pré-emptive. Ce changement impose aux systèmes mono-processeur à avoir beaucoup de problèmes de concurrence, tout comme les systèmes à processeurs multiples.

En conséquence, le code du noyau Linux contient le code des pilotes et doit être "ré-entrant" - il doit être capable de s'exécuter dans plusieurs contextes à la fois. Les structures de données doivent être soigneusement créées pour conserver la séparation des multiples threads d'exécution. Le code doit faire attention d'accéder à des données partagées avec des méthodes qui préviennent la corruption des données. Les situations de concurrence sont celles où un ordre d'exécution inopportun amène à un comportement indésirable. Écrire du code qui évite ces situations et gère la concurence demande de la réflexion et peut être tortueux. La gestion appropriée de la concurrence est requise pour écrire du code noyau correct; pour cette raison, chaque extrait de code dans ce livre a été écrit avec la concurrence dans l'esprit. Les techniques utilisées sont expliquées quand nous en avons besoin; le chapitre 5 est dédié à ce problème et les primitives du noyau sont disponibles pour la gestion de la concurrence.

Une erreur commune faite par les programmeurs de pilotes est de considérer que la concurrence n'est pas un problème dans la mesure où un morceau de code particulier ne va pas "dormir" (ou bloquer). Les noyaux précédents n'étaient pas pré-emptifs et cette considération n'était pas correcte sur les systèmes multi-processeurs. Dans les séries 2.6, le code noyau ne peut (presque) jamais supposer pouvoir maintenir le processeur sur un morceau de code donné. Si vous n'écrivez pas votre code avec la concurrence dans l'esprit, il sera sujet à des échecs catastrophiques qui seront extrêmement difficiles à débugger.


IV-3-3. Le processus actuel

Bien que les modules du noyau ne s'exécutent pas séquentiellement comme les applications le font, la plupart des actions exécutées par le noyau sont réalisées au nom d'un processus spécifique. Le code noyau peut se référer au processus actuel en accédant à l'élément global current, défini dans <asm/current.h>, qui rapporte un pointeur à struct task_struct, défini dans <linux/sched.h>. Le pointeur actuel rapporte au processus s'exécutant actuellement. Pendant l'exécution d'un appel système, tel que open ou read, le processus actuel est celui qui a invoqué l'appel. Le code noyau peut utiliser des informations spécifiques au processus en utilisant current, s'il en a besoin. Un exemple de cette technique est présenté dans le chapitre 6.

Il est important de noter que current n'est pas une vraie variable globale. Le besoin de supporter les systèmes SMP a forcé les développeurs du noyau à développer un mécanisme qui trouve le processus current sur le bon processeur. Ce mécanisme doit aussi être rapide puisque les références vers current arrivent fréquemment. Le résultat est un mécanisme dépendant de l'architecture qui, habituellement, cache un pointeur vers la structure task_struct sur la pile du noyau. Les détails de cette implémentation restent cachés aux autres sous-systèmes du noyau et un pilote de périphérique peut donc juste inclure <linux/sched.h> et se rapporter au processus current. Par exemple, le code suivant affiche l'identifiant (ID) du processus et le nom de commande du processus current en accédant à certains champs de la structure task_struct :

printk(KERN_INFO "The process is \"%s\" (pid %i)\n", 
	current->comm, current->pid);
Le nom de commande stocké dans current->comm est le nom de base du programme (coupé à 15 caractères si besoin) qui est exécuté par le processus current.

 

Valid XHTML 1.1!Valid CSS!

creative commons 2. à refaire lors de la mise en ligne finale