À la découverte des drivers et modules sous Linux
Article pour l'Écho de Linux (Septembre 1996)
Eric Sellin (esellin@pratique.fr)
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 ?
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/ttyS1Trois 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 mineurPar 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 ?
À 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.
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 :
Au cas (plus qu'improbable...) où vous ne vous conformeriez
pas à ces instructions, voici ce qui pourrait vous arriver :
Afin de simplifier l'écriture du driver, nous ne lirons
qu'un caractère par appel de cette fonction, même
si le processus appelant en demande 10 kilos. De toutes
manières, un programme bien écrit doit toujours
vérifier la valeur renvoyée par read()
qui représente le nombre de caractères
effectivement lus.
Je compte sur vous pour écrire très bientôt
la suite de cet article.
Bonne chance.
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.
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.
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 :
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;
Cette fonction sera appelée à chaque demande
de lecture du fichier spécial au travers de l'appel
système read(). En paramètre,
on reçoit :
Vous trouverez la description
complète des structures inode et
file dans le fichier /usr/include/linux/fs.h.
if ( *ptr == '\0' ) {
/* On a atteint la fin de la phrase, donc du fichier */
return 0;
}
On regarde si le pointeur n'est pas arrivé à
la fin de notre phrase. Si c'est le cas, on retourne une
valeur nulle qui marque la fin du fichier (cf.
man read).
if ( (foo=verify_area(VERIFY_WRITE,buf,1)) != 0 ) {
/* On vérifie qu'on a le droit d'écrire à cet endroit */
return foo;
}
La fonction verify_area sert ici à
vérifier que l'on a bien le droit d'écrire
dans la zone buf passée en paramètre.
/* Ecriture effective du caractère */
memcpy_tofs(buf,ptr++,1);
La fonction memcpy_tofs est un équivalent de
la fonction
memcpy qui, rappelons-le, est indisponible dans
ce contexte particulier.
On écrit un caractère et on
incrémente notre pointeur d'une position.
/* Et on retourne le nombre de caractères lus = 1 */
return 1;
Finalement, on renvoie le nombre de caractères qu'on
a lus. C'est cette valeur que le processus appelant lira au
retour de son appel à read().
}
static int glop_open(struct inode *inode, struct file *file)
{
Cette fonction sera appelée à chaque
ouverture du fichier spécial au travers de l'appel
système open().
/* Repositionne le pointeur au début de la phrase */
ptr = phrase;
On repositionne le pointeur au début de la phrase...
/* Ouverture sans problème */
return 0;
...et on annonce que tout s'est bien passé.
}
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;
}
On déclare un nouveau driver en mode caractère.
Pour cela, on utilise la fonction register_chrdev en
passant comme paramètres :
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.
--
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