À la découverte des drivers et modules sous Linux

Article pour l'Écho de Linux (Septembre 1996)

Eric Sellin (esellin@pratique.fr)


Sommaire


Introduction

Avant toute chose, je précise d'emblée que cet article est une initiation. Il pourra vous intéresser si, et seulement si, vous ne connaissez rien à la programmation du noyau de Linux.

Bien évidemment, nous ne partons pas au hasard, notre but est le suivant : créer un fichier spécial /dev/glop qui réagisse de la façon suivante :

	eureka:~$ cat /dev/glop
	A long time ago, in a galaxy far far away...
	eureka:~$ 
Very easy, isn't it ?

Rappel sur les fichiers spéciaux

Vous n'êtes pas sans savoir que, sous Unix, les périphériques sont représentés par des fichiers, appelés fichiers spéciaux et situés dans le répertoire /dev. En voici quelques exemples :
crw--w--w-   1 esellin  users      4,   0 Aug 29 18:29 /dev/console
brw-rw----   1 root     disk       3,  65 Jun 11 22:06 /dev/hdb1
brw-rw----   1 root     disk       3,  66 Jun 11 22:06 /dev/hdb2
crw-rw----   1 root     daemon     6,   0 Apr 28  1995 /dev/lp0
crw-rw----   1 root     daemon     6,   1 Apr 28  1995 /dev/lp1
brw-rw-rw-   1 root     disk      23,   0 Jul 18  1994 /dev/mcd
crw-rw-rw-   1 root     root       4,  64 Aug 29 18:46 /dev/ttyS0
crw-rw-rw-   1 root     root       4,  65 Jul 18  1994 /dev/ttyS1
Trois caractéristiques sont associées à un fichier spécial :

Notez bien que l'attribution des numéros majeur et mineur est faite arbitrairement. Une liste exhaustive de ces attributions est donnée dans le fichier Documentation/devices.txt des sources du noyau.

Pour créer un fichier spécial, on utilise, en tant que root, la commande mknod de la façon suivante :

	mknod fichier {bc} majeur mineur
Par exemple, la commande mknod /dev/glop c 52 0 va créer un fichier spécial /dev/glop, en mode caractère, de numéro majeur 52 et de numéro mineur 0.

Fort bien, me direz-vous : un fichier spécial donne accès à un périphérique. Mais que se passe-t-il quand, pour justement accéder à ce périphérique, on lit ce fichier ou on y écrit ?

Les drivers

Chaque type de périphérique (terminal, disque dur, imprimante, ...) est géré, au niveau de ses accès, par un programme particulier qu'on appelle un driver de périphérique.

À chacun de ces drivers est associé le mode et le numéro majeur du type de périphérique qu'il prend en charge. Par exemple, lorsque le driver des disques durs IDE démarre, il informe le noyau qu'il va s'occuper de tous les fichiers spéciaux en mode bloc et de numéro majeur égal à 3, puisque 3 est le numéro majeur qu'on a attribué aux disques durs IDE. Le noyau tient à jour une table des drivers et ainsi, il sait quel driver appeler lorsqu'un appel système tel que open(), read() ou write() est lancé sur un fichier spécial.

À tout moment, la commande cat /proc/devices vous donne, pour les modes caractère et bloc, les numéros majeurs actuellement reconnus par les drivers chargés en mémoire.

Ainsi, par exemple, quand on éxécute la commande cat foobar >/dev/lp1, le noyau regarde les propriétés du fichier /dev/lp1, il constate que c'est un fichier spécial en mode caractère de numéro majeur 6 et de numéro mineur 1, et il appelle le driver correspondant qui va, de son côté, envoyer le fichier foobar vers l'imprimante lp1 par une méthode qui ne regarde que lui.

Tout à l'heure, nous avons créé, grâce à la commande mknod, un fichier /dev/glop en mode caractère, de numéro majeur 52 et de numéro mineur 0. Nous allons maintenant créer un driver qui sera chargé de gérer ce nouveau périphérique qui répond au doux nom de glop.

Les modules

Pour l'instant, mes très maigres connaissances du noyau de Linux ne me permettent pas d'y intégrer un nouveau driver. Nous allons donc nous contenter d'un module chargeable. Après tout, ça nous donne une occasion d'apprendre comment ça marche.

Sous Linux, les modules sont des fichiers objets (.o) qu'on peut charger en mémoire et décharger à n'importe quel moment grâce aux commandes insmod et rmmod utilisées en tant que root. La commande lsmod, quant à elle, donne la liste des modules actuellement chargés en mémoire.

Un module doit contenir deux fonctions particulières :

Pour compiler un module, utilisez la ligne de commande suivante :

	gcc -D__KERNEL__ -DMODULE -O2 -c glop.c
Chez moi, sans le flag -O2 d'optimisation, ça ne fonctionne pas. Si quelqu'un peut m'expliquer :)

Let's go...

Le code d'un module faisant partie du noyau, quelques recommandations s'imposent :

Au cas (plus qu'improbable...) où vous ne vous conformeriez pas à ces instructions, voici ce qui pourrait vous arriver :

Unable to handle kernel paging request at virtual address c8002990
current->tss.cr3 = 00fb7000, 
*pde = 00000000
Oops: 0002
CPU:    0
EIP:    0010:[<0183602f>]
EFLAGS: 00010207
eax: 00000041   ebx: 00000fff   ecx: 0099ce58   edx: 08005000
esi: 08002990   edi: 007534c4   ebp: 00f8df90   esp: 00f8df7c
ds: 0018   es: 0018   fs: 002b   gs: 002b   ss: 0018
Process cat (pid: 1576, process nr: 29, stackpage=00f8d000)
Stack: 00000001 08002990 00001000 00b55280 00001000 08002990 00122dbc 007534c4 
       00b55280 08002990 00001000 00af8018 08002990 00001000 bffffb44 0010a5e2 
       00000003 08002990 00001000 08002990 00001000 bffffb44 ffffffda 0805002b 
Call Trace: [<00122dbc>] [<0010a5e2>] 
Code: 88 06 46 ff 05 1c 61 83 01 83 3d 1c 61 83 01 2d 75 0a c7 05 
Maintenant que vous êtes prévenu, voyons un peu comment nous allons écrire notre driver du périphérique glop.

#define __KERNEL__
#define MODULE
Ces deux #define permettent d'omettre -D__KERNEL__ et -DMODULE dans la ligne d'appel du compilateur gcc.
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/fs.h>
#include <linux/mm.h>
Quelques #include classiques...
#define GLOP_MAJOR	52
#define GLOP_ID		"glop"
Nous définissons ici le numéro majeur que nous utilisons ainsi qu'une chaîne de caractères qui apparaitra dans le fichier /proc/devices.
static char phrase[] = 
	"A long time ago, in a galaxy far far away...\n";
La phrase qui sera renvoyée lors d'une lecture du fichier spécial.
static char *ptr;
Ce pointeur indique le prochain caractère de la phrase qu'il faudra renvoyer lors de la prochaine lecture.
static int glop_read(struct inode *inode, struct file *file, char *buf, int count)
{
	int foo;
	if ( *ptr == '\0' ) {
		/* On a atteint la fin de la phrase, donc du fichier */
		return 0;
	}
	if ( (foo=verify_area(VERIFY_WRITE,buf,1)) != 0 ) {
		/* On vérifie qu'on a le droit d'écrire à cet endroit */
		return foo;
	}
	/* Ecriture effective du caractère */
	memcpy_tofs(buf,ptr++,1);
	/* Et on retourne le nombre de caractères lus = 1 */
	return 1;
}

static int glop_open(struct inode *inode, struct file *file)
{
	/* Repositionne le pointeur au début de la phrase */
	ptr = phrase;
	/* Ouverture sans problème */
	return 0;
}
Nous allons maintenant définir la structure glop_fops de type struct file_operations (cf. /usr/include/linux/fs.h). Cette structure donne la liste des fonctions à éxécuter lors d'un appel système open(), read(), write(), ... effectué sur notre fichier spécial. Dans notre cas, on n'implémente que les appels read() et open().
static struct file_operations glop_fops = {
	NULL,		/* seek() */
	glop_read,	/* read() */
	NULL,		/* write() */
	NULL,		/* readdir() */
	NULL,		/* select() */
	NULL,		/* ioctl */
	NULL,		/* mmap */
	glop_open,	/* open */
	NULL,		/* release */
	NULL,		/* fsync */
	NULL,		/* fasync */
	NULL,		/* check_media_change */
	NULL		/* revalidate */
};
Voici maintenant la fonction init_module qui est appelée au démarrage du module. Son principal but est d'initialiser le périphérique et d'installer le driver. Elle renvoie 0 si tout s'est bien passé. L'enseignement, c'est l'art de la répétition.
int init_module(void)
{

	if ( register_chrdev(GLOP_MAJOR,GLOP_ID,&glop_fops) ) {
		printk("glop: unable to get major %d\n",GLOP_MAJOR);
		return -EIO;
	}
	return 0;

}
Pour finir, voici la fonction cleanup_module qui, elle, est appelée au déchargement du module par rmmod :
void cleanup_module(void)
{
	unregister_chrdev(GLOP_MAJOR,GLOP_ID);
}
Voilà, il suffit de compiler ce petit programme, de charger le fichier objet résultant avec insmod et ensuite, vous pouvez éxécuter ceci :
	eureka:~$ cat /dev/glop
	A long time ago, in a galaxy far far away...
	eureka:~$ 

Conclusion

Bien sûr, ceci n'est que le minimum vital au sujet des drivers et des modules sous Linux. La meilleure source d'informations complémentaires reste très certainement les sources du noyau.

Je compte sur vous pour écrire très bientôt la suite de cet article.

Bonne chance.



--
Eric SELLIN -
esellin@pratique.fr
http://www.pratique.fr/~esellin
C makes it easy for you to shoot yourself in the foot. C++ makes that
harder but when you do, it blows away your whole leg -- B. Stroustrup