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 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
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
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.
ce qui aura pour effet d’interpréter le fichier /etc/lkm.conf dont voici un exemple
|
|
/usr/lkm/pf.o - - - - AFTERMOUNT |
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.
- lkm_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_UNLOAD ou LKM_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 commande LKM_E_LOAD ;
- blah_lkmunload() dans le cas d’une commande LKM_E_UNLOAD ;
- blah_lkmstat() dans le cas d’une commande LKM_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 type block ou character ;
- 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 :
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.mk pour 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 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
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 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
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() :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
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_base de la sous-structure uio_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 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94
|
#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
Retrouvez cet article dans : Linux Magazine 105