Programmation modules sous Linux  (partie 2)

Suivant
Pilote caractère
I . Programmation modules
1 . Programmation en mode noyau ou en mode utilisateur
2 . Débogage en mode noyau
3 . Chargement et déchargement
4 . Description et modinfo
5 . Passage de paramètres
6 . Edition de liens
7 . Appel de fonctions
8 . Compteur d'utilisation
9 . Dépendances statique et dynamique
10 . Prorocessus en cours
11 . Makefile
12 . Bibliographie

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) ;


2 . Débogage en mode noyau

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);


4 . Description et modinfo

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");
}


5 . Passage de paramètres

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


6 . Edition des liens

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


7 . Appel de fonction

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).


8 . Compteur d'utilisation

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.


10 . Processus en cours

Le module peut connaître le processus qui le pilote en accèdant à l'élément global current, un pointeur sur struct task_struct qui, pour la version 2.4 du noyau, est déclaré dans <asm/current.h>, inclus dans <linux/sched.h>. Le pointeur current désigne le processus utilisateur en cours (c'est-à-dire celui qui a émis l'appel système, tel que open ou read).

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);


11 . Makefile

Le Makefile suivant est un exemple minimal montrant la construction d'un module comportant deux fichiers sources :

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


12 . Bibliographie

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