Retrouvez cet article dans : Linux Magazine 105
Que ne voila un titre prometteur, mais point de trépignements, ami lecteur, nous ne poserons ici que les fondements de l’édifice. Ce que je vous propose en toute humilité, c’est de vous familiariser avec le monde merveilleux des modules noyau. Au travers d’un exemple basique, nous apprendrons à écrire notre premier pilote et titillerons son device associé.
1. Utilisation basique
Avant de se lancer corps et âme dans l’écriture d’un module d’exemple, je vous propose de nous familiariser avec l’utilisation même des modules noyau. En effet, si ces derniers sont incontournables et très communément utilisés dans le monde GNU/Linux et FreeBSD, ils sont étrangement très peu populaires dans le Landerneau NetBSD. Non qu’ils ne soient pas supportés, loin de là, ou que leur programmation soit d’une complexité insurmontable, mais finalement très peu de parties du noyau trouvent leur version modulaire. Un simple petit tour dans le répertoire /usr/lkm suffit pour se convaincre du peu d’intérêt porté à la modularisation :
$ ls /usr/lkm/ adosfs.o dummy_pci.o kernfs.o procfs.o bsdcomp.o exec_freebsd_aout.o lfs.o ptyfs.o cd9660.o exec_freebsd_elf.o mfs.o smbfs.o coda.o exec_linux_elf.o msdosfs.o tap.o coda5.o exec_pecoff.o ntfs.o tmpfs.o compat_freebsd.o exec_svr4_elf.o nullfs.o udf.o compat_linux.o ext2fs.o overlay.o umapfs.o compat_pecoff.o fdesc.o pf.o union.o compat_svr4.o filecorefs.o portal.o vnd.o deflate.o if_ipl.o powernow.o wi_pcmcia.o
Shocking, isn’t it ?
Préalablement à toute manipulation, nous devons vérifier que notre noyau possède bien le support des modules chargeables.
Évidemment, pour poursuivre notre périple, il sera indispensable de posséder les sources du système. Plutôt que de faire dans la finesse, je vous propose de télécharger l’intégralité de l’arbre des sources, ce qui vous permettra de vous plonger encore un peu plus dans l’étude du code source d’un OS du bien. En avant :
# cd /usr # cvs -d anoncvs@anoncvs.fr.netbsd.org:/cvsroot co -rnetbsd-4-0 src
Comme le laisse supposer la dernière commande, nous travaillerons sur la version 4.0 de NetBSD, dernière en date. L’article s’appliquera toutefois, à quelques modifications près (que nous mentionnerons), à n’importe quelle version de NetBSD supérieure ou égale à 2.0.
Une fois l’arbre téléchargé, nous nous rendons dans le répertoire contenant les configurations et nous vérifions l’existence de l’option LKM
$ grep LKM /usr/src/sys/arch/<architecture>/conf/MON_NOYAU options LKM # loadable kernel modules
où <architecture> correspond au type de CPU, par exemple i386.
Si une telle ligne n’apparaissait pas, il serait indispensable à la bonne poursuite des opérations que vous l’ajoutiez à votre configuration, puis que vous recompiliez, puis installiez votre noyau NetBSD (http://www.netbsd.org/docs/guide/en/chap-kernel.html).
À présent que nous savons que notre noyau supporte les LKM, nous allons voir quels sont les outils mis à disposition par notre système pour les manipuler. Un simple man lkm nous informe que ces utilitaires sont au nombre de trois :
modload(8)pour charger un module ;modstat(8)fournit des informations sur les modules chargés ;modunload(8)pour décharger un module.
Pour illustrer l’utilisation de ces exécutables, prenons comme cobaye le module pf.o, le célèbre pare-feu issu des magiciens d’OpenBSD, candidat idéal, puisque désactivé par défaut, du noyau GENERIC, mais pourtant essentiel à la protection d’une machine :
root@obana:~$ pfctl -sm pfctl: /dev/pf: Device not configured root@obana:~$ modload /usr/lkm/pf.o Module loaded as ID 0 root@obana:~$ modstat Type Id Offset Loadaddr Size Info Rev Module Name DEV 0 -1/161 cbdb0000 009c cbdd27c0 2 pf root@obana:~$ pfctl -sm No ALTQ support in kernel ALTQ related functions disabled states hard limit 10000 src-nodes hard limit 10000 frags hard limit 5000 root@obana:~$ modunload pf root@obana:~$ modstat Type Id Offset Loadaddr Size Info Rev Module Name root@obana:~$
Parfait, tout semble fonctionner à merveille. Mais quid du chargement automatique d’un module lors de l’allumage de notre machine ? Tout est prévu. Pour activer ce chargement automatique, il est tout d’abord nécessaire de renseigner la valeur suivante dans le fichier /etc/rc.conf
lkm=YES
1. Pour de plus amples informations sur l’utilisation des divers types de macros de gestion de listes chaînées (simples, doubles, circulaires…), reportez-vous à la page de manuel queue(3). Ces macros sont d’une puissance et d’une pratique telles que vous ne pourrez plus jamais vous en passer.
qui signifie :
1) Charger le module /usr/lkm/pf.o.
2) - : sans option à passer à modload(8).
3) - : le symbole d’entrée sera de la forme xxxinit().
4) - : aucun script de post-installation ne sera démarré.
5) - : aucune sortie de ld(1) ne sera loguée.
6) AFTERMOUNT : le chargement du module s’effectuera après le montage des systèmes de fichier.
Je vous renvoie à la lecture du man lkm.conf pour l’intégralité des options possibles dans ce fichier. Désormais, à chaque démarrage de votre machine, le module pf.o sera chargé automatiquement après le montage des filesystems spécifiés dans /etc/fstab.
2. Bon, ça commence ?
Il va de soi que pour entrer dans le vif du sujet, vous aurez besoin, au minimum, d’avoir installé le set comp.tgz qui contient le nécessaire de compilation, ainsi que d’un éditeur de texte.
Un dernier mot sur un aspect extrêmement important dans le monde BSD en général, et NetBSD en particulier. Avant de publier quoi que ce soit en direction d’une quelconque liste de diffusion ou autre système de soumission de code, je ne saurais que trop vous conseiller la lecture du fichier /usr/share/misc/style qui décrit la KNF ou " Kernel Normal Form ", véritable guide de bonnes manières sur l’art de la présentation d’un code source en société.
Sur ces avertissements d’usage, nous pouvons commencer.
Nous l’avons dit plus haut, chaque module chargé doit posséder un point d’entrée qui sera passé à ld(1) afin que ce dernier le lie au noyau. Le point d’entrée par défaut est xxxinit(), et si ce dernier n’est pas trouvé, c’est une fonction de la forme module_lkmentry() qui sera recherchée, où module est le nom du module dans le système de fichier. Par exemple, pour un module hello.o, le point d’entrée associé sera hello_lkmentry().
En pratique, le point d’entrée est constitué d’un simple appel à la macro DISPATCH, définie dans le header /usr/include/sys/lkm.h. Cette macro a la charge de répartir sur d’autres fonctions les actions de chargement, déchargement et récupération de statistiques. Nous retrouvons ici nos trois outils modload(8), modunload(8) et modstat(8).
Mais voyons à quoi ressemble l’appel à notre point d’entrée :
int
blah_lkmentry(struct lkm_table *lkmtp, int cmd, int ver)
{
DISPATCH(lkmtp, cmd, ver, blah_lkmload, blah_lkmunload, blah_lkmstat);
}
Évidemment, nous devons nous arrêter un instant sur cet amas tombé de nulle part. Jetons d’abord un coup d’œil aux arguments passés à cette fonction.
- l
km_table, comme son nom l’indique, n’est autre qu’une liste chaînée de type TAILQ1 contenant l’ensemble des modules chargés, ainsi que les informations relatives à ces modules. La structure est également visible dans le header/usr/include/sys/lkm.h. cmd, on peut s’en douter, est un entier décrivant une commande :LKM_E_LOAD,LKM_E_UNLOADouLKM_E_STAT.ver, quant à lui, permet de vérifier que le versionning de l’environnement de compilation du module correspond bien au noyau auquel il essaye de se lier.
La macro DISPATCH distribue les appels, fonction de la commande invoquée, à :
blah_lkmload()dans le cas d’une commandeLKM_E_LOAD;blah_lkmunload()dans le cas d’une commandeLKM_E_UNLOAD;blah_lkmstat()dans le cas d’une commandeLKM_E_STAT.
Les arguments lkmtp, cmd et ver sont également publiés dans cette macro. Notez que la macro DISPATCH est un #define sur une autre macro, LKM_DISPATCH, qui à travers un switch(), distribue l’activité fonction de la commande LKM_E_* passée.
Une dernière macro " générique " est nécessaire avant de démarrer l’écriture de nos gestionnaires de commandes : il s’agit de la déclaration du type de module. Cette déclaration est réalisée à l’aide des macros MOD_*. Ces macros peuvent être de type :
MOD_SYSCALL, pour l’écriture d’un module implémentant un appel système ;MOD_VFS, si l’on souhaite implémenter un nouveau type de système de fichier virtuel ;MOD_DEV, probablement l’un des plus utilisés, vise l’écriture de drivers de typeblockoucharacter;MOD_COMPAT, pour le support de systèmes d’exploitation étrangers ;MOD_EXEC, pour l’écriture d’un nouveau type de binaire ;MOD_MISC, " Miscellaneous modules ", pour faire à peu près ce que l’on veut, ce sera la macro de choix pour notre premier exemple.
Nous ajoutons donc au-dessus de la déclaration du point d’entrée :
MOD_MISC("blah");
Pour réaliser une première version de notre LKM, nous allons utiliser la fonction lkm_nofunc(), toujours définie dans sys/lkm.h, et remplacer les handlers load, unload et stat par cette dernière. Vous l’aurez compris, la fonction lkm_nofunc() ne fait simplement rien.
Voici le (petit) code de notre premier exemple :
imil@obana:~/src/lkm$ cat lkminit_blah.c
#include <sys/param.h>
#include <sys/systm.h>
#include <sys/lkm.h>
int blah_lkmentry(struct lkm_table *, int , int );
MOD_MISC("blah");
int
blah_lkmentry(struct lkm_table *lkmtp, int cmd, int ver)
{
DISPATCH(lkmtp, cmd, ver, lkm_nofunc, lkm_nofunc, lkm_nofunc);
}
Reste bien sûr à écrire le Makefile associé, et, là encore, NetBSD va grandement nous simplifier la tâche. Grâce à l’important travail d’abstraction réalisé par les divers projets BSD, notre fichier Makefile se résume à :
imil@obana:~/src/lkm$ cat Makefile S!= cd /usr/src/sys;pwd KMOD= blah SRCS= lkminit_blah.c NOMAN= # defined WARNS= 1 .include <bsd.kmod.mk>
Explication dans le texte :
- L’include directory est évalué à partir d’une suite de commandes shell.
- Le nom du module généré sera
blah. - Le code source se résume dans l’immédiat au fichier
lkminit_blah.c(ce formalisme est nécessaire au bon fonctionnement du framework de compilation des LKM). - Nous ne génèrerons pas de manpage.
- Lorsque WARNS vaut la valeur 1, on passe à gcc les arguments
-Wall -Wstrict-prototypes -Wmissing-prototypes -Wpointer-arith -Wno-sign-compare -Wno-traditional(voir/usr/share/mk/bsd.sys.mkpour la liste des valeurs possibles et leur implication).
Le cœur battant, nous lançons fièrement un make, à l’issue duquel, un fichier blah.o vient de faire son apparition dans le répertoire courant. Nous savons ce qu’il nous reste à faire :
imil@obana:~/src/lkm$ sudo modload ./blah.o Module loaded as ID 0 imil@obana:~/src/lkm$ sudo modstat Type Id Offset Loadaddr Size Info Rev Module Name MISC 0 - cb327000 0004 cb3270bc 2 blah imil@obana:~/src/lkm$ sudo modunload blah imil@obana:~/src/lkm$ sudo modstat Type Id Offset Loadaddr Size Info Rev Module Name imil@obana:~/src/lkm$
Victoire !
3. Un peu d’activité
Nous sommes donc en mesure de compiler, charger et décharger un module qui ne fait rien. Il est maintenant grand temps de créer des gestionnaires dignes de ce nom (ou presque) afin de mettre en place un véritable module qui " faitdestrucs ".
Ce que nous nous proposons d’écrire est un bête LKM qui écrira quelque chose à son chargement, à son déchargement et à la réception de la commande stat.
Il est de bon ton de vérifier l’existence d’un module afin de ne pas le charger plusieurs fois grâce à la fonction lkmexists(). Nous inclurons donc dans notre handler de chargement du module le code suivant :
if (lkmexists(lkmtp))
return EEXIST; /* l’exit code EEXIST est defini dans sys/errno.h */
Voici à quoi ressemblent nos gestionnaires :
static int
blah_lkmload(struct lkm_table *lkmtp, int cmd)
{
if (lkmexists(lkmtp))
return EEXIST;
printf("blah: chulaaaaaa\n");
return 0;
}
static int
blah_lkmunload(struct lkm_table *lkmtp, int cmd)
{
printf("blah: chupulaaaaaa\n");
return 0;
}
static int
blah_lkmstat(struct lkm_table *lkmtp, int cmd)
{
printf(“blah: eeeh, tu m\’chatouilles\n”);
return 0;
}
Remplaçons maintenant les appels à lkm_nofunc() par de vrais appels aux fonctions de commande :
int
blah_lkmentry(struct lkm_table *lkmtp, int cmd, int ver)
{
DISPATCH(lkmtp, cmd, ver, blah_lkmload, blah_lkmunload, blah_lkmstat);
}
Compilons, puis manipulons notre LKM :
imil@obana:~/src/lkm$ make # compile lkm/lkminit_blah.o cc -O2 -ffreestanding -fno-strict-aliasing -Wno-pointer-sign -Wall -Wstrict-prototypes -Wmissing-prototypes -Wpointer-arith -Wno-sign-compare -Wno-traditional -Werror -nostdinc -I. -I/home/imil/src/lkm -isystem /usr/src/sys -isystem /usr/src/sys/arch -isystem /usr/src/sys/../common/include -D_KERNEL -D_LKM -c lkminit_blah.c # link lkm/blah.o ld -r -o tmp.o lkminit_blah.o mv tmp.o blah.o imil@obana:~/src/lkm$ sudo modload ./blah.o Module loaded as ID 0 imil@obana:~/src/lkm$ dmesg|tail -1 blah: chulaaaaaa imil@obana:~/src/lkm$ sudo modstat Type Id Offset Loadaddr Size Info Rev Module Name MISC 0 - cb327000 0004 cb327158 2 blah imil@obana:~/src/lkm$ dmesg|tail -1 blah: eeeh, tu m’chatouilles imil@obana:~/src/lkm$ sudo modunload blah imil@obana:~/src/lkm$ dmesg|tail -1 blah: chupulaaaaaa
C’est quand même la sacrée classe ce printf() !
Le code de nos handlers n’étant pas spécialement conséquent, il serait du meilleur effet de les regrouper dans une seule fonction et de choisir l’action à mener à l’aide de switch() :
static int
blah_lkmhandle(struct lkm_table *lkmtp, int cmd)
{
int err = 0; /* valeur par defaut, pas d’erreur */
switch(cmd) {
case LKM_E_LOAD:
if (lkmexists(lkmtp))
return EEXIST;
printf("blah: chulaaaaaa\n");
return 0;
case LKM_E_UNLOAD:
printf("blah: chupulaaaaaa\n");
return 0;
case LKM_E_STAT:
printf(„blah: eeeh, tu m\‘chatouilles\n“);
return 0;
default:
err = EINVAL; /* Argument invalide, valeur spécifiée dans sys/errno.h */
break;
}
return err;
}
Il suffit maintenant de retoucher l’appel à DISPATCH de cette façon :
int
blah_lkmentry(struct lkm_table *lkmtp, int cmd, int ver)
{
DISPATCH(lkmtp, cmd, ver, blah_lkmhandle, blah_lkmhandle, blah_lkmhandle);
}
Beaucoup plus sexy.
4. Je-t’chatte, tu-t’chattes
Après cette petite mise en bouche, il est temps de s’attaquer aux choses sérieuses. Déblayons le terrain afin de préparer l’écriture de notre Nirvana, le Device Driver.
Que souhaitons nous faire à l’aide de ce dernier ? Rien de bien méchant, voici le comportement visé :
$ echo pwet > /dev/blah $ dmesg|tail -1 blah: eeeh, j’ai reçu pwet $ cat /dev/blah pwet pwet pwet [...]
Cette interaction suppose que :
- Notre device doit pouvoir être lu.
- On doit pouvoir écrire dans notre device.
Pour publier ces fonctions de lecture/écriture, nous devons déclarer une structure de type cdevsw ou Character Device Switch, définie dans sys/conf.h, qui fait pointer les actions typiques d’un char device vers des fonctions adaptées. Intuitivement, nous imaginons qu’en plus des opérations de lecture/écriture, des fonctions d’ouverture et fermeture du device devront également être disponibles.
Un rapide tour d’horizon du header sus-cité nous informe sur le contenu de la structure cdevsw :
struct cdevsw {
int (*d_open)(dev_t, int, int, struct lwp *);
int (*d_close)(dev_t, int, int, struct lwp *);
int (*d_read)(dev_t, struct uio *, int);
int (*d_write)(dev_t, struct uio *, int);
int (*d_ioctl)(dev_t, u_long, caddr_t, int, struct lwp *);
void (*d_stop)(struct tty *, int);
struct tty * (*d_tty)(dev_t);
int (*d_poll)(dev_t, int, struct lwp *);
paddr_t (*d_mmap)(dev_t, off_t, int);
int (*d_kqfilter)(dev_t, struct knote *);
int d_type;
};
Plus bas dans le fichier, nous voyons comment annuler certaines opérations :
#define noopen ((dev_type_open((*)))enodev) #define noclose ((dev_type_close((*)))enodev) #define noread ((dev_type_read((*)))enodev) #define nowrite ((dev_type_write((*)))enodev) #define noioctl ((dev_type_ioctl((*)))enodev) #define nostop ((dev_type_stop((*)))enodev) #define notty NULL #define nopoll seltrue #define nommap ((dev_type_mmap((*)))enodev) #define nodump ((dev_type_dump((*)))enodev) #define nosize NULL #define nokqfilter seltrue_kqfilter
Enfin, toujours dans sys/conf.h, on trouvera les valeurs applicables au champ d_type :
/* * Types for d_type */ #define D_OTHER 0 #define D_TAPE 1 #define D_DISK 2 #define D_TTY 3
Muni de ces informations, nous pouvons écrire la structure de données associée à notre futur device :
static struct cdevsw blah_dev = {
blah_open,
blah_close,
blah_read,
blah_write,
noioctl,
nostop,
notty,
nopoll,
nommap,
nokqfilter,
D_OTHER
};
La dernière étape de la préparation consiste à déclarer le type de module que nous allons écrire. Vous l’aurez certainement deviné, nous allons remplacer la macro MOD_MISC par MOD_DEV, dont voici les arguments expliqués :
name: sous cette appellation incompréhensible, nous devrons indiquer le nom du module, blah dans notre cas.devname: ici nous indiquons le nom du device, nous souhaitons travailler sur/dev/blah, ce sera donc à nouveau blah.bdevp: il ne s’agit pas d’un block device, ce champ aura NULL pour valeur.bdevm: nous plaçons le major à -1, même si ce dernier ne sera pas utilisé.cdevp: ici, nous écrivons l’adresse vers la structure de donnée du device,&blah_dev.cdevm: nous souhaitons que le major du device soit attribué dynamiquement, nous inscrivons -1.
Ce qui nous donne :
MOD_DEV("blah", "blah", NULL, -1, &blah_dev, -1);
Les pré-requis étant maintenant implémentés, nous pouvons passer à l’étape la plus intéressante de notre périple, l’écriture des fonctions associées au driver. Commençons par les plus triviales, blah_open() et blah_close().
Nous n’avons pour ainsi dire besoin de rien de particulier dans ces fonctions, mais pour la beauté du geste, rendons-les un peu verbeuses.
static int
blah_open(dev_t dev, int flag, int mode, struct lwp *l)
{
printf("OH HAI, PRUCESS %d HAS OPNED MEH\n", l->l_proc->p_pid);
return 0;
}
Comme on peut s’en douter, dev correspond au device courant, flag et mode, aux mêmes arguments utilisés par open(2), et l au lightweight process dans lequel nous nous trouvons. Le numéro de processus est retrouvé via le champ l_proc qui n’est autre qu’un pointeur sur le processus auquel est associé le thread.
Attention
Le prototype des fonctions open et close n’est pas backward compatible pour les versions antérieures à NetBSD 4.0. Il faudra remplacer struct lwp par struct proc, et p représentera alors le pointeur direct vers le processus appelant.
Le code de fermeture du device est sensiblement identique :
static int
blah_close(dev_t dev, int flag, int mode, struct lwp *l)
{
printf("KTHXBY\n");
return 0;
}
J’ai évidemment gardé le meilleur pour la fin, les fonctions blah_read() et blah_write(). Pour aborder ces fonctions, il faut noter que NetBSD met à notre disposition une structure, struct uio, permettant précisément de transporter des informations de l’espace noyau vers l’espace utilisateur et vice versa. De cette structure, nous utiliserons " consciemment " :
- explicitement, le champ
uio_resid; - implicitement, le champ
uio_rw; - implicitement, le champ
iov_basede la sous-structureuio_iov.
La lecture du header sys/uio.h nous éclaire sur l’utilité de ces variables :
struct iovec {
void *iov_base; /* Base address. */
size_t iov_len; /* Length. */
};
/* ... */
enum uio_rw { UIO_READ, UIO_WRITE };
/* ... */
struct uio {
struct iovec *uio_iov; /* pointer to array of iovecs */
int uio_iovcnt; /* number of iovecs in array */
off_t uio_offset; /* offset into file this uio corresponds to */
size_t uio_resid; /* residual i/o count */
enum uio_rw uio_rw; /* see above */
struct vmspace *uio_vmspace;
};
Ainsi, nous comprenons que uio_resid représente la quantité de données en transit, uio_rw le mode d’accès à la structure, et uio_iov->iov_base l’adresse de base des données en transit.
Ajoutons préalablement ces lignes en tête de notre code source :
#include <sys/lwp.h> /* struct lwp */ #include <sys/proc.h> /* nous allons manipuler une variable de type struct proc */ #define MAXLEN 256 int blah_lkmentry(struct lkm_table *, int , int ); static int blah_lkmhandle(struct lkm_table *, int ); /* déclaration de nos fonctions associées */ static int blah_open(dev_t, int, int, struct lwp *); static int blah_close(dev_t, int, int, struct lwp *); static int blah_read(dev_t, struct uio *, int); static int blah_write(dev_t, struct uio *, int); static char inside[MAXLEN];
Pour faire circuler nos données d’un espace à l’autre, nous utiliserons la fonction uiomove(9). Nous pouvons alors écrire la fonction blah_read() de la façon suivante :
/* déplacement de inside vers uio pour présentation à l’espace utilisateur */
static int
blah_read(dev_t dev, struct uio *uio, int ioflag)
{
int n, error = 0;
while (uio->uio_resid > 0 && error == 0) {
n = min(MAXLEN, uio->uio_resid);
error = uiomove(inside, n, uio);
}
return error;
}
Nous sommes ici dans une opération de lecture. Aussi, le champ uio_rw de la structure uio sera initialisé avec la valeur UIO_READ, ce qui aura pour conséquence d’indiquer à la fonction uiomove(9) d’effectuer un copyout(9), soit une copie de l’espace noyau vers l’espace utilisateur, donc de la variable inside vers le champ uio_iov->iov_base de la structure uio. Comme on peut le voir dans notre fonction, tant qu’il reste des données à transférer (uio_resid) et que le code d’erreur est nul, on continue de faire transiter des informations entre le noyau et l’espace utilisateur en s’assurant de transférer la plus petite quantité entre la taille maximale, MAXLEN, que nous avons spécifiée, et la taille restant à traiter dans la structure uio, uio_resid.
Livrons-nous maintenant au même exercice pour la fonction blah_write :
/* déplacement de uio vers inside pour copie dans l’espace noyau */
static int
blah_write(dev_t dev, struct uio *uio, int ioflag)
{
int n, error = 0;
memset(inside, 0, MAXLEN);
while (uio->uio_resid > 0 && error == 0) {
n = min(MAXLEN, uio->uio_resid);
error = uiomove(inside, uio->uio_resid, uio);
printf(“blah: eeeh, j’ai recu %s”, inside);
}
return error;
}
Avant d’écrire quoi que ce soit dans la variable interne, inside, nous mettons à 0 l’ensemble des octets de cette dernière, puis utilisons à nouveau la fonction uiomove(9), qui, puisque nous sommes dans une fonction d’écriture, placera son champ uio_rw à UIO_WRITE, indiquant ainsi que son rôle consiste maintenant à copier des données de l’espace utilisateur vers l’espace noyau.
Le code complet de notre premier character device est le suivant :
#include <sys/param.h>
#include <sys/systm.h>
#include <sys/conf.h>
#include <sys/lkm.h>
#include <sys/lwp.h>
#include <sys/proc.h>
#include <sys/errno.h>
#define MAXLEN 256
int blah_lkmentry(struct lkm_table *, int , int );
static int blah_lkmhandle(struct lkm_table *, int );
static int blah_open(dev_t, int, int, struct lwp *);
static int blah_close(dev_t, int, int, struct lwp *);
static int blah_read(dev_t, struct uio *, int);
static int blah_write(dev_t, struct uio *, int);
static char inside[MAXLEN];
static struct cdevsw blah_dev = {
blah_open,
blah_close,
blah_read,
blah_write,
noioctl,
nostop,
notty,
nopoll,
nommap,
nokqfilter,
D_OTHER
};
MOD_DEV("blah", "blah", NULL, -1, &blah_dev, -1);
static int
blah_open(dev_t dev, int flag, int mode, struct lwp *l)
{
printf("OH HAI, PRUCESS %d HAS OPNED MEH\n", l->l_proc->p_pid);
return 0;
}
static int
blah_close(dev_t dev, int flag, int mode, struct lwp *l)
{
printf("KTHXBY\n");
return 0;
}
/* copy from kernel (inside) to userland (uio) */
static int
blah_read(dev_t dev, struct uio *uio, int ioflag)
{
int n, error = 0;
while (uio->uio_resid > 0 && error == 0) {
n = min(MAXLEN, uio->uio_resid);
error = uiomove(inside, n, uio);
}
return error;
}
/* copy from userland (uio) to kernel (inside) */
static int
blah_write(dev_t dev, struct uio *uio, int ioflag)
{
int n, error = 0;
memset(inside, 0, MAXLEN);
while (uio->uio_resid > 0 && error == 0) {
n = min(MAXLEN, uio->uio_resid);
error = uiomove(inside, uio->uio_resid, uio);
printf(“blah: eeeh, j’ai recu %s”, inside);
}
return error;
}
static int
blah_lkmhandle(struct lkm_table *lkmtp, int cmd)
{
int err = 0;
switch(cmd) {
case LKM_E_LOAD:
if (lkmexists(lkmtp))
return EEXIST;
strncpy(inside, "I IZ TEH INIT\n", MAXLEN - 1);
printf("blah: chulaaaaaa\n");
return 0;
case LKM_E_UNLOAD:
printf("blah: chupulaaaaaa\n");
return 0;
case LKM_E_STAT:
printf(„blah: eeeh, tu m\‘chatouilles\n“);
return 0;
default:
err = EINVAL; /* Argument invalide, valeur spécifiée dans sys/errno.h */
break;
}
return err;
}
int
blah_lkmentry(struct lkm_table *lkmtp, int cmd, int ver)
{
DISPATCH(lkmtp, cmd, ver, blah_lkmhandle, blah_lkmhandle, blah_lkmhandle);
}
Et maintenant ? C’est l’heure du test final bien sûr !
Vous l’aurez noté, l’utilitaire modload(8) prend plusieurs arguments, et, parmi eux, un script d’initialisation quelconque. Comme vous vous en rappelez, le choix du major de notre character device est effectué dynamiquement. Aussi, il reviendra à modload de passer en argument à notre mini-script ce numéro de device. Le script s’articule ainsi :
imil@obana:~/src/lkm$ cat postblah.sh #!/bin/sh rm -f /dev/blah mknod /dev/blah c $3 0 chmod 666 /dev/blah exit 0
Il suffit alors d’invoquer modload(8) de la sorte :
sudo modload -p ./postblah.sh ./blah.o
et de constater :
imil@obana:~/src/lkm$ ls -l /dev/blah crw-rw-rw- 1 root wheel 188, 0 Mar 11 00:08 /dev/blah
Enfin, le moment de vérité :
imil@obana:~/src/lkm$ echo "HAI, YOU IZ TEH LIVE ?" > /dev/blah imil@obana:~/src/lkm$ dmesg|tail -3 OH HAI, PRUCESS 10698 HAS OPNED MEH blah: eeeh, j’ai recu HAI, YOU IZ TEH LIVE ? KTHXBY imil@obana:~/src/lkm$ cat /dev/blah HAI, YOU IZ TEH LIVE ? HAI, YOU IZ TEH LIVE ? HAI, YOU IZ TEH LIVE ? HAI, YOU IZ TEH LIVE ? ^C
Et soudain, un sentiment de toute-puissance parcourt votre échine.
5. Ouuuh, j’me ferais bien un driver wifi moi
À travers ce minuscule exemple, c’est tout l’univers des mystérieux pilotes du noyau qui s’ouvre à vous. Bien des contrées restent à explorer, et bien des astuces manquent à l’appel, mais, d’ores et déjà, munis de ces quelques lignes, vous êtes en mesure de réaliser moult prouesses, des plus dignes aux plus obscures. Dans la lignée de cette introduction, je vous invite à vous pencher sur les différents types de macros MOD_*, mais, aussi et surtout, à parcourir l’arbre des sources du système NetBSD, source infinie d’inspiration. Notez, pour finir, que les exemples présents dans cet article s’adapteront sans peine au monde OpenBSD, le framework LKM ayant finalement peu évolué entre les deux OS.
Liens
- FAQ sur la programmation noyau NetBSD : http://www.netbsd.org/fr/Documentation/kernel/programming.html
- Le guide de programmation de pilotes pour NetBSD : http://www.netbsd.org/docs/kernel/ddwg.html
- Lolcats : http://icanhascheezburger.com/
Retrouvez cet article dans : Linux Magazine 105


