Programmation
modules sous Linux (partie
2) |
Suivant Pilote caractère |
I . Programmation modules |
1 . Programmation en mode noyau ou en mode utilisateur
Rappels
Espace
noyau :
Tout est permis (même le pire), donc tout est à craindre.
Accès direct (mémoire, E/S périphériques matériels,
...)
Espace
utilisateur :
Tout est protégé (exécution en mode protégé),
donc les possibilités sont plus restreintes.
Conseils
"Toujours essayer de déporter la complexité en dehors du noyau"
Conséquences
:
le noyau doit rester le plus petit possible,
les appels système doivent rester
en nombre limité,
la complexité réside dans
la bibliothèque C (libC), ou dans les applications utilisateurs.
Pilote
de périphérique ou fonctionnalité en mode noyau :
accès direct à la mémoire
et aux périphériques,
rapidité (moins de couches logicielles).
Application
en mode utilisateur :
déporte la complexité en dehors
du noyau,
exécution en mode protégé,
indépendance vis-à-vis des
interfaces, ports...
Interaction
Trois
types de transitions utilisateur/noyau :
appels systèmes (points d’entrées
dans le noyau limités environ 190),
interruptions gérés par les
gestionnaires d’interruptions du noyau,
exceptions (ex : accès mémoire
illégal, instruction illégale...).
Chaque
processus utilisateur se décompose donc en deux parties :
une partie noyau qui gère les appels
système (ex : open(),read()
...),
une partie utilisateur qui fait le reste
(ex : gestion, algorithme...).
Contraintes
Seuls outils de compilation supportés : gcc
et GNU make.
Version minimum des outils dans Documentation/Changes
;
Utiliser l’optimisation -O (ou -O2, 2 étant le niveau d'optimisation,
il est conseillé de ne pas dépasser cette valeur) de gcc pour
que les fonctions inline des en-têtes (headers) soient interprétées
;
Pas de bibliothèque C (libC) pour la programmation noyau ;
Fonctions utiles dans le répertoire lib/
des sources du noyau ;
Normes de codage décrites dans Documentation/CodingStyle
;
Il est conseillé de découper les gros fichiers C en plusieurs
fichiers aux fonctions bien définies (couches logicielles) ;
Les fichiers objet (.o) peuvent ensuite être réunis en un seul
module à l’aide de ld,
l’éditeur de liens (linker) ;
Le plusieurs techniques de débogage sont possibles :
En
mode console avec printk()
et dmesg pour visualiser ;
Avec
kgdb (il faut
le patch kgdb pour le noyau (kgdb.sourceforge.net)
; Nécessite une deuxième machine reliée à la cible
par câble série, et l'utilisation de GDB-client pour déboguer
la cible. Sinon, c'est une solution relativement facile à mettre en oeuvre
et efficace (permet le débogage en mode source).
Avec
kdb, un débogueur
noyau embarqué, développé par SGI (oss.sgi.com) ; Il comprend
un ensemble de commandes utilisateurs permettant de déboguer le noyau
et les pilotes de périphériques en temps réel, et un débogueur
noyau en mode assembleur (pas de mode source). Ce débogage complexe est
plus difficile à exploiter.
Le
profiling offre la possibilité d’analyser le temps
passé dans chaque fonction du noyau (paramètre de boot : profile=n).
On peut lire les informations binaires, dans /proc/profile,
grâce à l’utilitaire readprofile.
Cela permet de déceler des dysfonctionnements structurels, et/ou des
parties d’un driver à optimiser.
par
requêtes (vers un fichier virtuel de l'interface /proc).
avec
GDB en lecture seule sur /proc/kcore
;
débogage
classique dans l'espace utilisateur pour les parties indépendantes du
matériel (analyse des sorties de l’application avec strace
)
Pour envoyer des messages sur la console, on utilisera la fonction du noyau printk() (la fonction printf() de la libc n'étant pas disponible en mode noyau) ;
Prototype : int
printk (const char *fmt, ...) ;
Exemple : printk("<1>
Hello world !\n");
Plusieurs niveaux de débogage sont définis dans <linux/kernel.h>
#define KERN_EMERG
"<0>" /* system is unusable */
#define KERN_ALERT "<1>" /* action must be taken immediately
*/
#define KERN_CRIT "<2>" /* critical conditions */
#define KERN_ERR "<3>" /* error conditions */
#define KERN_WARNING "<4>" /* warning conditions */
#define KERN_NOTICE "<5>" /* normal but significant condition
*/
#define KERN_INFO "<6>" /* informational */
#define KERN_DEBUG "<7>" /* debug-level messages */
Les messages sont logués par le daemon syslogd.
La commande dmesg
affiche et contrôle le tampon circulaire utilisé par printk()
3 . Chargement et déchargement
Un module possède un point d'entrée et un point de sortie qui
sont automatiquement appelés au chargement et déchargement de
celui-ci.
point
d'entrée :
int init_module(void)
point
de sortie : void
cleanup_module()
Remarque : un module ne possède donc pas de fonction main()
!
La fonction init_module() doit contenir tout le code nécessaire à l’initialisation du module (ex : allocations, initialisations matérielles, réservation des ressources...) ;
Au contraire, la fonction de libération cleanup_module() doit défaire tout ce qui a été fait à l’initialisation (ex : libération de mémoire, désactivation du matériel, libération des ressources...) ;
Source d'un module :
#define MODULE
#include <linux/module.h>
int init_module(void)
{
printk("<1>Hello world !\n");
return 0;
}
void cleanup_module(void)
{
printk("<1>Goodbye, cruel world !\n");
}
Compilation (fichier objet .o) :
gcc -c -I/usr/src/linux/include
tp1_exo1.c
Test chargement et déchargement :
$ insmod tp1_exo1.o
$ lsmod
$ rmmod tp1_exo1
$ dmesg
A partir du noyau 2.3.13, les fonctions init_module()
et cleanup_module() ont été
remplacés par module_init()
et module_exit()
:
#define MODULE
#include <linux/module.h>
#include <linux/init.h>
static int
__init mon_init_module(void)
{
printk("<1>Hello world !\n"); return 0;
}
static void
__exit mon_cleanup_module(void)
{
printk("<1>Goodbye, cruel world !\n");
}
module_init(mon_init_module);
module_exit(mon_cleanup_module);
Le module peut être décrit par des macros disponible dans linux/module.h
:
MODULE_AUTHOR(nom) : place le
nom de l'auteur dans le fichier objet
MODULE_DESCRIPTION(desc) : place
une description du module dans le fichier objet
MODULE_SUPPORTED_DEVICE(dev)
: place une entrée indiquant le périphérique pris en charge
par ce module
MODULE_LICENSE(type) : indique
le type de licence du module
On peut obtenir ces informations en utilisant la commande modinfo nom_module
#define MODULE
#include <linux/module.h>
MODULE_AUTHOR("tv");
MODULE_DESCRIPTION("mon premier module");
MODULE_SUPPORTED_DEVICE("aucun");
MODULE_LICENSE("GPL");
int init_module(void)
{
printk("<1>Hello world !\n");
return 0;
}
void cleanup_module(void)
{
printk("<1>Goodbye, cruel world !\n");
}
Lors du chargement d'un module, il est possible de lui passer des paramètres.
Pour cela, on utilise deux macros définis dans linux/module.h
:
MODULE_PARM(nom, type)
MODULE_PARM_DESC(desc)
Remarques : cinq types de paramètres sont actuellement pris en charge
: b (octet), h (entier court, 2 octets), i (entier, 4 octets), l (entier long)
et s (chaînes de caractères).
#define MODULE
#include <linux/module.h>
static int param=0;
MODULE_PARM(param, "i");
MODULE_PARM_DESC(param, "un parametre de ce module");
int init_module(void)
{
printk("<1>parametre = %d\n", param);
return 0;
}
void cleanup_module(void)
{
printk("<1>module decharge avec succes\n");
}
Test : $ insmod param=2
L'édition des liens va permettre :
regrouper plusieurs fichiers
.o (découpage fonctionnel)
retirer l'extension .o
(si on le désire)
//fichier
: tp1_exo4.c
#define MODULE
#include <linux/module.h>
void detect(void);//prototype
int init_module(void)
{
printk("<1>tp4 charge.\n");
detect();
return 0;
}
void cleanup_module(void)
{
printk("<1>tp4 decharge.\n");
}
//fichier
: tp1_exo4ext.c
#define MODULE
#include <linux/module.h>
void detect(void)
{
printk("<1>je suis la fonction detect() !\n");
}
Après compilation de chaque fichier, réaliser l'édition
des liens suivante :
$ ld -r tp1_exo4.o
tp1_exo4ext.o -o tp1_exo4
Test : $ insmod ./tp1_exo4
Un module peut appeler une fonction si le module, qui la posséde, l'exporte et si celui-ci est préalablement chargé. Par défaut, lors d'un chargement avec insmod, les symboles sont exportés (voir plus loin pour l'utilisation des symboles).
//fichier
: tp1_exo5a.c
#define MODULE
#include <linux/module.h>
void detect(void);
int init_module(void)
{
printk("<1>tp5a charge.\n");
printk("<1>Appel la fonction : detect()\n");
detect();
return 0;
}
void cleanup_module(void)
{
printk("<1>tp5a decharge.\n");
}
//fichier
: tp1_exo5b.c
#define MODULE
#include <linux/module.h>
void detect(void)
{
printk("<1>je suis la fonction detect() !\n");
}
int init_module(void)
{
printk("<1>tp5b charge.\n");
return 0;
}
void cleanup_module(void)
{
printk("<1>tp5b decharge.\n");
}
Il faut donc charger d'abord tp1_exo5b.o puis tp1_exo5a.o. Le déchargement se fera dans l'ordre inverse du chargement (tp1_exo5a.o puis tp1_exo5b.o).
Le système conserve un compteur d'utilisation pour
chaque module de façon à déterminer si celui-ci peut être
retiré en toute sécurité. Dans les noyaux actuels, le système
conserve automatiquement la trace du compteur d'utilisation. Cependant, il est
encore possible d'ajuster manuellement le compteur grâce à 3 macros
définis dans linux/module.h
:
MOD_INC_USE_COUNT
: incrémente le compteur pour le module en cours
MOD_DEC_USE_COUNT
: décrémente le compteur pour le module en cours
MOD_IN_USE
: vrai lorsque le compteur est différent de zéro ou valeur du
compteur
#define MODULE
#include <linux/module.h>
void test(void)
{
if(MOD_IN_USE) printk("<1>Le module est en cours d'utilisation
!\n");
else printk("<1>Le module n'est pas utilise !\n");
}
int init_module(void)
{
MOD_INC_USE_COUNT;
printk("<1>Module charge ...\n");
test();
return 0;
}
void cleanup_module(void)
{
printk("<1>Module decharge ...\n");
MOD_DEC_USE_COUNT;//tester en mettant cette ligne en commentaire,
dans ce cas le module ne pourra plus être déchargé!!!
}
9 . Dépendances statiques et dynamiques
Dépendance
statique
Les symboles exportés par le noyau sont disponibles dans /proc/ksyms (utilisés par insmod pour l’édition de liens dynamique).
La commande ksyms (kernel/ksyms.c) permet d’afficher plus finement le contenu de /proc/ksyms.
Les modules peuvent exporter des symboles supplémentaires grâce à la macro, défini dans <linux/module.h>, EXPORT_SYMBOL(symbol) ou EXPORT_SYMBOL_NOVERS(symbol), si EXPORT_SYMTAB est définie (#define ou -D dans gcc).
La macro EXPORT_NO_SYMBOLS permet de n’exporter aucun symbole.
La commande nm permet de visualiser les symboles stockés dans un module (ou autre). Dans la sortie de nm, T signifie Texte, D signifie Données et U signifie Non défini (undefined). Un symbole non défini est un symbole dont le fichier fait appel mais qu'il ne déclare pas.
Les modules peuvent utilisés des symboles exportés par d'autres modules et on peut empiler de nouveaux modules sur d'autres modules.Quand on utilise des modules empilés, il est utile de connaître la commande modprobe. modprobe permet le chargement et le déchargement d'un module et de ses dépendances. Ainsi, une seule commande modprobe peut parfois remplacer plusieurs appels à insmod. Par contre, modbrope n'examine que l'arborescence des modules installés (/lib/modules/). Donc, on utilisera toujours insmod pour charger des modules du répertoire courant.
Une fois les modules compilées, il faut les copier dans l'arborescence des modules installés. Pour le TP, on les copiera dans /lib/modules/$(uname -r)/kernel/drivers/char/. Puis, on utilise la commande depmod -a qui permet de construire le fichier modules.dep qui contient les dépendance entre modules.
Chargement d'un module et de ses dépendances : $
modprobe mon_module
Déchargement d'un module et de ses dépendances : $
modprobe -r mon_module
Soit le module tp2_exo2a suivant qui exporte la fonction tp_detect()
:
#include <linux/config.h>
#if defined(CONFIG_MODVERSIONS)
#define MODVERSIONS
#endif
#ifdef MODVERSIONS
#include <linux/modversions.h>
#endif
#define EXPORT_SYMTAB
#define MODULE
#include <linux/module.h>
MODULE_LICENSE("GPL");
void tp_detect(void)
{
printk("<1>je suis la fonction tp_detect() !\n");
}
int init_module(void)
{
printk("<1>tp2exo2a charge ...\n");
return 0;
}
void cleanup_module(void)
{
printk("<1>tp2exo2a decharge ...\n");
}
EXPORT_SYMBOL (tp_detect);
On écrit une deuxième module qui va utilisé le symbole tp_detect :
#include <linux/config.h>
#if defined(CONFIG_MODVERSIONS)
#define MODVERSIONS
#endif
#ifdef MODVERSIONS
#include <linux/modversions.h>
#endif
#define MODULE
#include <linux/module.h>
MODULE_LICENSE("GPL");
EXPORT_NO_SYMBOLS;
void test(void) { tp_detect(); }
int init_module(void)
{
printk("<1>tp2exo2b charge ...\n");
test();
return 0;
}
void cleanup_module(void)
{
printk("<1>tp2exo2b decharge
...\n");
}
Les test sont nombreux pour bien comprendre le mécanisme des dépendances :
1) visualiser les symboles exportés en utilisant la commande nm
2) charger le module tp2exo2a.o, puis visualiser les symboles exportés dans le noyau en utilisant la commande ksyms | grep tp2exo
3) charger le module tp2exo2b.o et visualiser le fonctionement en utilisant la commande dmesg
4) décharger les deux modules dans le bon ordre !
5) copier les deux modules dans l'arborescence des modules /lib/modules/ puis faire un depmod -a
6) visualiser le fichier nouvellement créé modules.dep et noter les ajouts pour les deux modules
7) tester les dépendances en faisant un modprobe.
Dépendance
dynamique
Tout code dans l'espace noyau peut demander le chargement de modules. Pour cela, on utilise la fonction request_module() qui demande à kmod de charger un module.
La fonction request_module() est synchrone : attente que la tentative de chargement du module soit terminée. request_module() utilise modprobe pour le chargement du module demandé. Un retour sans erreur (valeur de retour égale à zéro) de request_module() ne garantit pas que la fonctionnalité recherchée soit maintenant disponible. La valeur de retour n'indique que le succès de l'exécution de modprobe, mais ne réflète pas le status de modprobe. Généralement, on fera 2 fois le test de présence de la fonctionnalité recherchée.
Remarque : Le chargement d'un module dans le noyau soulève d'évidentes questions de sécurité puisque le code chargé tourne avec le plus haut niveau de privilèges possible. D'autre part, une vulnérabilité (liée à request_module() et modprobe) ayant été découverte tardivement dans le noyau 2.4.0-test pousse à déconseiller l'utilisation de cette fonction.
#include <linux/config.h>
#if defined(CONFIG_MODVERSIONS)
#define MODVERSIONS
#endif
#ifdef MODVERSIONS
#include <linux/modversions.h>
#endif
#define EXPORT_SYMTAB
#define MODULE
#include <linux/module.h>
MODULE_LICENSE("GPL");
void tp_detect(void)
{
printk("<1>je suis la fonction tp_detect() !\n");
}
void charge_module(void)
{
int res;
printk("<1>demande de chargement de tp2exo3b ...\n");
res = request_module("tp2exo3b");
printk("<1>resultat : %i\n", res);
}
int init_module(void)
{
printk("<1>tp2exo3a charge ...\n");
charge_module();
return 0;
}
void cleanup_module(void)
{
printk("<1>tp2exo3a decharge ...\n");
}
EXPORT_SYMBOL (tp_detect);
#include <linux/config.h>
#if defined(CONFIG_MODVERSIONS)
#define MODVERSIONS
#endif
#ifdef MODVERSIONS
#include <linux/modversions.h>
#endif
#define MODULE
#include <linux/module.h>
MODULE_AUTHOR("tv");
MODULE_DESCRIPTION("un module");
MODULE_SUPPORTED_DEVICE("aucun");
MODULE_LICENSE("GPL");
EXPORT_NO_SYMBOLS;
int init_module(void)
{
printk("<1>tp2exo3b charge ...\n");
printk("<1>tp2exo3b appelle la fonction tp_detect() ...\n");
tp_detect();
return 0;
}
void cleanup_module(void)
{
printk("<1>tp2exo3b decharge ...\n");
}
Copier les fichiers dans l'arborescence des modules /lib/modules/...
Pour tester, on fera seulement un insmod
tp2exo3a.o,
qui s'occupera du chargement de tp2exo3b.
Exemple : printk("Le processus est \"%s\" (pid %i)\n", current->comm, current->pid);
Dans le cas d'un module minimal, le processus en cours sera donc insmod.
Remarque : Généralement, l'utilisation de current
se fait dans l'implémentation des fonctions open(),
close(), ioctl(), etc ... D'autre part il est possible d'émettre
un signal à partir d'un module. Pour cela, on utilise les fonctions kill_pg()
et kill_proc()
:
/* on emet le signal SIGIO au processus
concerne */
kill_proc(current->pid, SIGIO, 1);
KERNELDIR = /usr/src/linux
include $(KERNELDIR)/.config
CC = gcc
CFLAGS = -D__KERNEL__ -DMODULE -I$(KERNELDIR)/include -O2 -Wall
INSTALLDIR = /lib/modules/$(shell uname -r)/kernel/misc
MODNAME = sample
ifdef CONFIG_MODVERSIONS
CFLAGS += -DMODVERSIONS -include $(KERNELDIR)/include/linux/modversions.h
endif
all: $(MODNAME)
$(MODNAME): sample_init.o sample_clean.o
$(LD) -r $^ -o $@
sample_init.o: sample_init.c
$(CC) $(CFLAGS) -c $^
sample_clean.o: sample_clean.c
$(CC) $(CFLAGS) -c $^
install:
install -d $(INSTALLDIR)
install $(MODNAME) $(INSTALLDIR)
#optionnel : reconstruit les dependances des modules
depmod -a
clean:
rm -f $(MODNAME) *.o *~ core
Compilation : $ make ou $ make all
Installation : $ make install
Documents : Le répertoire Documentation/ des sources
du noyau
Livres :
"Pilotes de périphériques Linux 2.4 (2° édition)"
et "Le noyau linux" chez O'Reilly
"Programmation Linux 2.0" de R.Card chez Eyrolles
"Programmation système en C sous Linux" de C.Blaess chez Eyrolles
Liens :
Les archives du noyau linux : www.kernel.org
La LDP du noyau Linux : www.linuxdoc.org
FAQ du noyau Linux : www.tux.org
Recherche : www.google.com
Divers : www.tvtsii.net
Cours :
Alcôve – Noyau Linux et pilotes de périphériques
: www.courseforge.org
Atrid - Drivers et modules sous Linux : www.atrid.fr
Précédent Le bus PCI |
Sommaire |
Suivant Pilote caractère |
© 2002 for www.tvtsii.net by tv |